Compare commits
4 Commits
c4a8b28abe
...
0464223d00
| Author | SHA1 | Date | |
|---|---|---|---|
| 0464223d00 | |||
| 78413e98d7 | |||
| 2c090d5fbd | |||
| 43680cce22 |
@ -73,8 +73,8 @@ func gradeExam(recordID uint, examID uint, userID uint) {
|
|||||||
"multiple-choice": 1.0,
|
"multiple-choice": 1.0,
|
||||||
"multiple-selection": 2.5,
|
"multiple-selection": 2.5,
|
||||||
"short-answer": 10.0,
|
"short-answer": 10.0,
|
||||||
"ordinary-essay": 5.0,
|
"ordinary-essay": 4.5,
|
||||||
"management-essay": 5.0,
|
"management-essay": 4.5,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 评分
|
// 评分
|
||||||
|
|||||||
@ -44,12 +44,6 @@ func CreateExam(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据用户类型决定论述题类型
|
|
||||||
essayType := "ordinary-essay" // 默认普通涉密人员论述题
|
|
||||||
if user.UserType == "management-person" {
|
|
||||||
essayType = "management-essay" // 保密管理人员论述题
|
|
||||||
}
|
|
||||||
|
|
||||||
// 使用固定的题型配置(总分100分)
|
// 使用固定的题型配置(总分100分)
|
||||||
questionTypes := []models.QuestionTypeConfig{
|
questionTypes := []models.QuestionTypeConfig{
|
||||||
{Type: "fill-in-blank", Count: 10, Score: 2.0}, // 20分
|
{Type: "fill-in-blank", Count: 10, Score: 2.0}, // 20分
|
||||||
@ -57,7 +51,8 @@ func CreateExam(c *gin.Context) {
|
|||||||
{Type: "multiple-choice", Count: 10, Score: 1.0}, // 10分
|
{Type: "multiple-choice", Count: 10, Score: 1.0}, // 10分
|
||||||
{Type: "multiple-selection", Count: 10, Score: 2.5}, // 25分
|
{Type: "multiple-selection", Count: 10, Score: 2.5}, // 25分
|
||||||
{Type: "short-answer", Count: 2, Score: 10.0}, // 20分
|
{Type: "short-answer", Count: 2, Score: 10.0}, // 20分
|
||||||
{Type: essayType, Count: 1, Score: 5.0}, // 5分(根据用户类型选择论述题)
|
{Type: "ordinary-essay", Count: 1, Score: 4.5}, // 4.5分(普通涉密人员论述题)
|
||||||
|
{Type: "management-essay", Count: 1, Score: 4.5}, // 4.5分(保密管理人员论述题)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按题型配置随机抽取题目
|
// 按题型配置随机抽取题目
|
||||||
@ -266,6 +261,39 @@ func GetExamDetail(c *gin.Context) {
|
|||||||
for _, q := range questions {
|
for _, q := range questions {
|
||||||
questionMap[q.ID] = q
|
questionMap[q.ID] = q
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查是否包含论述题,如果没有则添加两种论述题
|
||||||
|
hasOrdinaryEssay := false
|
||||||
|
hasManagementEssay := false
|
||||||
|
for _, q := range questions {
|
||||||
|
if q.Type == "ordinary-essay" {
|
||||||
|
hasOrdinaryEssay = true
|
||||||
|
}
|
||||||
|
if q.Type == "management-essay" {
|
||||||
|
hasManagementEssay = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果缺少论述题,则补充
|
||||||
|
var additionalQuestions []models.PracticeQuestion
|
||||||
|
if !hasOrdinaryEssay {
|
||||||
|
var ordinaryEssay models.PracticeQuestion
|
||||||
|
if err := db.Where("type = ?", "ordinary-essay").First(&ordinaryEssay).Error; err == nil {
|
||||||
|
additionalQuestions = append(additionalQuestions, ordinaryEssay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasManagementEssay {
|
||||||
|
var managementEssay models.PracticeQuestion
|
||||||
|
if err := db.Where("type = ?", "management-essay").First(&managementEssay).Error; err == nil {
|
||||||
|
additionalQuestions = append(additionalQuestions, managementEssay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将补充的题目添加到题目映射中
|
||||||
|
for _, q := range additionalQuestions {
|
||||||
|
questionMap[q.ID] = q
|
||||||
|
}
|
||||||
|
|
||||||
orderedDTOs := make([]models.PracticeQuestionDTO, 0, len(questionIDs))
|
orderedDTOs := make([]models.PracticeQuestionDTO, 0, len(questionIDs))
|
||||||
for _, id := range questionIDs {
|
for _, id := range questionIDs {
|
||||||
if q, ok := questionMap[id]; ok {
|
if q, ok := questionMap[id]; ok {
|
||||||
@ -278,6 +306,15 @@ func GetExamDetail(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加补充的论述题到结果中
|
||||||
|
for _, q := range additionalQuestions {
|
||||||
|
dto := convertToDTO(q)
|
||||||
|
if !showAnswer {
|
||||||
|
dto.Answer = nil // 不显示答案
|
||||||
|
}
|
||||||
|
orderedDTOs = append(orderedDTOs, dto)
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"data": gin.H{
|
"data": gin.H{
|
||||||
|
|||||||
@ -574,9 +574,15 @@ func convertToDTO(question models.PracticeQuestion) models.PracticeQuestionDTO {
|
|||||||
if err := json.Unmarshal([]byte(question.AnswerData), &answer); err != nil {
|
if err := json.Unmarshal([]byte(question.AnswerData), &answer); err != nil {
|
||||||
// JSON解析失败,直接使用原始字符串
|
// JSON解析失败,直接使用原始字符串
|
||||||
dto.Answer = question.AnswerData
|
dto.Answer = question.AnswerData
|
||||||
|
// 计算答案长度
|
||||||
|
dto.AnswerLengths = []int{len(question.AnswerData)}
|
||||||
} else {
|
} else {
|
||||||
// JSON解析成功
|
// JSON解析成功
|
||||||
dto.Answer = answer
|
dto.Answer = answer
|
||||||
|
// 计算答案长度
|
||||||
|
if answerStr, ok := answer.(string); ok {
|
||||||
|
dto.AnswerLengths = []int{len(answerStr)}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 其他题型必须是JSON格式
|
// 其他题型必须是JSON格式
|
||||||
@ -586,6 +592,20 @@ func convertToDTO(question models.PracticeQuestion) models.PracticeQuestionDTO {
|
|||||||
question.ID, question.Type, err)
|
question.ID, question.Type, err)
|
||||||
} else {
|
} else {
|
||||||
dto.Answer = answer
|
dto.Answer = answer
|
||||||
|
// 计算填空题答案长度
|
||||||
|
if question.Type == "fill-in-blank" {
|
||||||
|
if answers, ok := answer.([]interface{}); ok {
|
||||||
|
lengths := make([]int, len(answers))
|
||||||
|
for i, ans := range answers {
|
||||||
|
if ansStr, ok := ans.(string); ok {
|
||||||
|
lengths[i] = len(ansStr)
|
||||||
|
} else {
|
||||||
|
lengths[i] = len(fmt.Sprintf("%v", ans))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dto.AnswerLengths = lengths
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,6 +25,7 @@ type PracticeQuestionDTO struct {
|
|||||||
Options []Option `json:"options"` // 选择题选项数组
|
Options []Option `json:"options"` // 选择题选项数组
|
||||||
Category string `json:"category"` // 题目分类
|
Category string `json:"category"` // 题目分类
|
||||||
Answer interface{} `json:"answer"` // 正确答案(用于题目管理编辑)
|
Answer interface{} `json:"answer"` // 正确答案(用于题目管理编辑)
|
||||||
|
AnswerLengths []int `json:"answer_lengths,omitempty"` // 答案长度数组(用于打印时计算横线长度)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PracticeAnswerSubmit 练习题答案提交
|
// PracticeAnswerSubmit 练习题答案提交
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import {
|
|||||||
Card,
|
Card,
|
||||||
Button,
|
Button,
|
||||||
Typography,
|
Typography,
|
||||||
Tag,
|
|
||||||
Space,
|
Space,
|
||||||
Spin,
|
Spin,
|
||||||
Row,
|
Row,
|
||||||
@ -13,17 +12,14 @@ import {
|
|||||||
message
|
message
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import {
|
import {
|
||||||
FileTextOutlined,
|
HomeOutlined
|
||||||
HomeOutlined,
|
|
||||||
CheckCircleOutlined,
|
|
||||||
CloseCircleOutlined
|
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import * as examApi from '../api/exam'
|
import * as examApi from '../api/exam'
|
||||||
import type { Question } from '../types/question'
|
import type { Question } from '../types/question'
|
||||||
import type { GetExamResponse } from '../types/exam'
|
import type { GetExamResponse } from '../types/exam'
|
||||||
import styles from './ExamAnswerView.module.less'
|
import styles from './ExamAnswerView.module.less'
|
||||||
|
|
||||||
const { Text, Paragraph } = Typography
|
const { Text } = Typography
|
||||||
|
|
||||||
// 题型名称映射
|
// 题型名称映射
|
||||||
const TYPE_NAME: Record<string, string> = {
|
const TYPE_NAME: Record<string, string> = {
|
||||||
@ -60,7 +56,7 @@ const ExamAnswerView: React.FC = () => {
|
|||||||
// 处理打印功能
|
// 处理打印功能
|
||||||
const handlePrint = () => {
|
const handlePrint = () => {
|
||||||
// 设置打印标题
|
// 设置打印标题
|
||||||
document.title = `${examData?.exam?.title || '试卷答案'}_打印版`
|
document.title = `试卷答案_打印版`
|
||||||
|
|
||||||
// 触发打印
|
// 触发打印
|
||||||
window.print()
|
window.print()
|
||||||
@ -115,29 +111,8 @@ const ExamAnswerView: React.FC = () => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 格式化答案显示
|
|
||||||
const formatAnswer = (answer: any, type: string): string => {
|
|
||||||
if (answer === null || answer === undefined || answer === '') {
|
|
||||||
return '未设置答案'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(answer)) {
|
|
||||||
if (answer.length === 0) return '未设置答案'
|
|
||||||
return answer.filter(a => a !== null && a !== undefined && a !== '').join('、')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === 'true-false') {
|
|
||||||
// 处理判断题:支持字符串和布尔值
|
|
||||||
const answerStr = String(answer).toLowerCase()
|
|
||||||
return answerStr === 'true' ? '正确' : '错误'
|
|
||||||
}
|
|
||||||
|
|
||||||
return String(answer)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 渲染答案详情
|
// 渲染答案详情
|
||||||
const renderAnswerDetail = (question: Question, index: number, type: string) => {
|
const renderAnswerDetail = (question: Question, index: number) => {
|
||||||
// 格式化答案显示
|
// 格式化答案显示
|
||||||
const formatAnswer = (answer: any, type: string): string => {
|
const formatAnswer = (answer: any, type: string): string => {
|
||||||
if (answer === null || answer === undefined || answer === '') {
|
if (answer === null || answer === undefined || answer === '') {
|
||||||
@ -232,7 +207,7 @@ const ExamAnswerView: React.FC = () => {
|
|||||||
</Col>
|
</Col>
|
||||||
<Col flex="auto" style={{ textAlign: 'center' }}>
|
<Col flex="auto" style={{ textAlign: 'center' }}>
|
||||||
<Text strong style={{ fontSize: 20 }}>
|
<Text strong style={{ fontSize: 20 }}>
|
||||||
{examData.exam?.title || '试卷答案'}
|
试卷答案
|
||||||
</Text>
|
</Text>
|
||||||
</Col>
|
</Col>
|
||||||
<Col>
|
<Col>
|
||||||
@ -274,7 +249,7 @@ const ExamAnswerView: React.FC = () => {
|
|||||||
<div className={styles.fillBlankContainer}>
|
<div className={styles.fillBlankContainer}>
|
||||||
{qs.map((q, index) => (
|
{qs.map((q, index) => (
|
||||||
<div key={q.id} className={styles.fillBlankItem}>
|
<div key={q.id} className={styles.fillBlankItem}>
|
||||||
{renderAnswerDetail(q, index, type)}
|
{renderAnswerDetail(q, index)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -289,7 +264,7 @@ const ExamAnswerView: React.FC = () => {
|
|||||||
const globalIndex = rowIndex * 5 + colIndex;
|
const globalIndex = rowIndex * 5 + colIndex;
|
||||||
return (
|
return (
|
||||||
<td key={q.id} style={{ width: '20%' }}>
|
<td key={q.id} style={{ width: '20%' }}>
|
||||||
{renderAnswerDetail(q, globalIndex, type)}
|
{renderAnswerDetail(q, globalIndex)}
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@ -26,7 +26,8 @@ import {
|
|||||||
ClockCircleOutlined,
|
ClockCircleOutlined,
|
||||||
CheckCircleOutlined,
|
CheckCircleOutlined,
|
||||||
TrophyOutlined,
|
TrophyOutlined,
|
||||||
HistoryOutlined
|
HistoryOutlined,
|
||||||
|
PrinterOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import * as examApi from '../api/exam'
|
import * as examApi from '../api/exam'
|
||||||
import styles from './ExamManagement.module.less'
|
import styles from './ExamManagement.module.less'
|
||||||
@ -225,6 +226,13 @@ const ExamManagement: React.FC = () => {
|
|||||||
>
|
>
|
||||||
查看答案
|
查看答案
|
||||||
</Button>,
|
</Button>,
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
icon={<PrinterOutlined />}
|
||||||
|
onClick={() => navigate(`/exam/${exam.id}/print`)}
|
||||||
|
>
|
||||||
|
打印试卷
|
||||||
|
</Button>,
|
||||||
<Button
|
<Button
|
||||||
type="link"
|
type="link"
|
||||||
danger
|
danger
|
||||||
|
|||||||
@ -195,7 +195,7 @@ const ExamOnline: React.FC = () => {
|
|||||||
// 清除进度
|
// 清除进度
|
||||||
localStorage.removeItem(`exam_progress_${examId}`)
|
localStorage.removeItem(`exam_progress_${examId}`)
|
||||||
// 跳转到成绩页,传递提交结果
|
// 跳转到成绩页,传递提交结果
|
||||||
navigate(`/exam/result/${res.data.record_id}`, {
|
navigate(`/exam/result/${res.data?.record_id}`, {
|
||||||
state: { submitResult: res.data }
|
state: { submitResult: res.data }
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@ -212,38 +212,87 @@ const ExamOnline: React.FC = () => {
|
|||||||
|
|
||||||
// 渲染填空题
|
// 渲染填空题
|
||||||
const renderFillInBlank = (question: Question, index: number) => {
|
const renderFillInBlank = (question: Question, index: number) => {
|
||||||
// 获取答案数量(如果有 answer 字段)
|
// 获取答案数组
|
||||||
const answerCount = question.answer
|
const answers = question.answer && Array.isArray(question.answer)
|
||||||
? Array.isArray(question.answer)
|
? question.answer
|
||||||
? question.answer.length
|
: question.answer
|
||||||
: 1
|
? [String(question.answer)]
|
||||||
: 1
|
: []
|
||||||
|
|
||||||
|
// 计算实际需要填空的数量
|
||||||
|
const blankCount = question.content ? (question.content.match(/\*{4,}/g) || []).length : answers.length
|
||||||
|
|
||||||
|
// 处理题目内容,将 **** 替换为输入框占位符
|
||||||
|
const renderQuestionContent = (content: string) => {
|
||||||
|
if (!content) return content
|
||||||
|
|
||||||
|
let processedContent = content
|
||||||
|
let inputIndex = 0
|
||||||
|
|
||||||
|
// 将所有的 **** 替换为输入框标识
|
||||||
|
processedContent = processedContent.replace(/\*{4,}/g, () => {
|
||||||
|
const id = `blank_${inputIndex}`
|
||||||
|
inputIndex++
|
||||||
|
return `[INPUT:${id}]`
|
||||||
|
})
|
||||||
|
|
||||||
|
return processedContent
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染包含输入框的题目内容
|
||||||
|
const renderContentWithInputs = () => {
|
||||||
|
const processedContent = renderQuestionContent(question.content || '')
|
||||||
|
const parts = processedContent.split(/\[INPUT:([^\]]+)\]/)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{parts.map((part, index) => {
|
||||||
|
if (index % 2 === 1) {
|
||||||
|
// 这是一个输入框标识符
|
||||||
|
const inputIndex = Math.floor(index / 2)
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
key={`input_${inputIndex}`}
|
||||||
|
style={{
|
||||||
|
width: 120,
|
||||||
|
margin: '0 4px',
|
||||||
|
display: 'inline-block',
|
||||||
|
verticalAlign: 'middle'
|
||||||
|
}}
|
||||||
|
placeholder={`第 ${inputIndex + 1} 空`}
|
||||||
|
onChange={(e) => {
|
||||||
|
const currentValue = form.getFieldValue(`question_${question.id}`) || []
|
||||||
|
currentValue[inputIndex] = e.target.value
|
||||||
|
form.setFieldValue(`question_${question.id}`, currentValue)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// 这是普通文本
|
||||||
|
return part
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className={styles.questionCard} key={question.id}>
|
<Card className={styles.questionCard} key={question.id}>
|
||||||
<div className={styles.questionHeader}>
|
<div className={styles.questionHeader}>
|
||||||
<Text strong>
|
<Text strong style={{ marginBottom: 12, display: 'block' }}>
|
||||||
{index + 1}. {question.content}
|
{index + 1}.
|
||||||
</Text>
|
</Text>
|
||||||
|
<div style={{ marginBottom: 16, lineHeight: '1.8' }}>
|
||||||
|
{renderContentWithInputs()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name={`question_${question.id}`}
|
name={`question_${question.id}`}
|
||||||
rules={[{ required: true, message: '请填写答案' }]}
|
rules={[{ required: true, message: '请填写所有空格' }]}
|
||||||
initialValue={Array(answerCount).fill('')}
|
initialValue={Array(blankCount).fill('')}
|
||||||
|
style={{ display: 'none' }} // 隐藏原来的表单项,因为我们用内联输入框了
|
||||||
>
|
>
|
||||||
<Space direction="vertical" style={{ width: '100%' }}>
|
<Input />
|
||||||
{Array.from({ length: answerCount }).map((_, i) => (
|
|
||||||
<Input
|
|
||||||
key={i}
|
|
||||||
placeholder={`第 ${i + 1} 空`}
|
|
||||||
onChange={(e) => {
|
|
||||||
const value = form.getFieldValue(`question_${question.id}`) || []
|
|
||||||
value[i] = e.target.value
|
|
||||||
form.setFieldValue(`question_${question.id}`, value)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Space>
|
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -208,7 +208,7 @@
|
|||||||
// A4纸张设置
|
// A4纸张设置
|
||||||
@page {
|
@page {
|
||||||
size: A4;
|
size: A4;
|
||||||
margin: 2cm;
|
margin: 0.8cm;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
@ -223,88 +223,128 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.paperHeader {
|
.paperHeader {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 6px;
|
||||||
padding-bottom: 12px;
|
padding-bottom: 4px;
|
||||||
|
|
||||||
.paperTitle {
|
.paperTitle {
|
||||||
font-size: 24pt !important;
|
font-size: 16pt !important;
|
||||||
|
margin-bottom: 4px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.examInfo {
|
.examInfo {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 4px;
|
||||||
|
width: 100%;
|
||||||
|
font-family: 'SimSun', '宋体', serif;
|
||||||
|
|
||||||
.infoItem {
|
.infoItem {
|
||||||
font-size: 12pt;
|
font-size: 9pt;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.instructionCard {
|
.instructionCard {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 6px;
|
||||||
border: 1px solid #000;
|
border: 1px solid #000;
|
||||||
|
|
||||||
:global(.ant-card-body) {
|
:global(.ant-card-body) {
|
||||||
padding: 16px;
|
padding: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h4 {
|
h4 {
|
||||||
font-size: 14pt;
|
font-size: 10pt;
|
||||||
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 12px;
|
||||||
|
|
||||||
li {
|
li {
|
||||||
font-size: 11pt;
|
font-size: 8pt;
|
||||||
|
margin-bottom: 1px;
|
||||||
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.questionGroup {
|
.questionGroup {
|
||||||
margin-bottom: 28px;
|
margin-bottom: 8px;
|
||||||
page-break-inside: avoid;
|
|
||||||
|
|
||||||
.groupHeader {
|
.groupHeader {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
|
||||||
.groupTitle {
|
.groupTitle {
|
||||||
font-size: 16pt;
|
font-size: 12pt;
|
||||||
}
|
}
|
||||||
|
|
||||||
.groupScore {
|
.groupScore {
|
||||||
font-size: 12pt;
|
font-size: 10pt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.questionItem {
|
.questionItem {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 6px;
|
||||||
page-break-inside: avoid;
|
|
||||||
|
|
||||||
.questionContent {
|
.questionContent {
|
||||||
|
margin-bottom: 3px;
|
||||||
|
line-height: 1.3;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
font-size: 12pt;
|
font-size: 10pt;
|
||||||
|
font-family: 'SimSun', '宋体', serif !important;
|
||||||
|
font-weight: normal !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.optionsList {
|
.optionsList {
|
||||||
|
margin: 4px 0;
|
||||||
|
padding-left: 14px;
|
||||||
|
|
||||||
.optionItem {
|
.optionItem {
|
||||||
font-size: 11pt;
|
margin-bottom: 1px;
|
||||||
|
font-size: 9pt;
|
||||||
|
font-family: 'SimSun', '宋体', serif !important;
|
||||||
|
font-weight: normal !important;
|
||||||
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.answerArea {
|
.answerArea {
|
||||||
|
margin-top: 4px;
|
||||||
|
|
||||||
span {
|
span {
|
||||||
font-size: 11pt;
|
font-size: 9pt;
|
||||||
|
font-family: 'SimSun', '宋体', serif !important;
|
||||||
|
font-weight: normal !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blankLine {
|
.blankLine {
|
||||||
font-size: 11pt;
|
font-size: 11pt;
|
||||||
|
font-family: 'SimSun', '宋体', serif !important;
|
||||||
|
font-weight: normal !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.answerLines {
|
.answerLines {
|
||||||
.answerLine {
|
.answerLine {
|
||||||
font-size: 11pt;
|
font-size: 11pt;
|
||||||
|
font-family: 'SimSun', '宋体', serif !important;
|
||||||
|
font-weight: normal !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.essayAnswer {
|
.essayAnswer {
|
||||||
:global(.ant-typography) {
|
:global(.ant-typography) {
|
||||||
font-size: 11pt;
|
font-size: 11pt;
|
||||||
|
font-family: 'SimSun', '宋体', serif !important;
|
||||||
|
font-weight: normal !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -315,17 +355,16 @@
|
|||||||
page-break-after: always;
|
page-break-after: always;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 避免在题目中间分页
|
// 移除分页限制,允许更紧密的排版
|
||||||
.questionItem {
|
|
||||||
page-break-inside: avoid;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 黑白打印优化
|
// 黑白打印优化和字体设置
|
||||||
* {
|
* {
|
||||||
color: #000 !important;
|
color: #000 !important;
|
||||||
background: #fff !important;
|
background: #fff !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
text-shadow: none !important;
|
text-shadow: none !important;
|
||||||
|
font-family: 'SimSun', '宋体', serif !important;
|
||||||
|
font-weight: normal !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保留边框
|
// 保留边框
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { useParams, useNavigate, useSearchParams } from 'react-router-dom'
|
import { useParams, useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
import { Card, Button, Typography, message, Spin, Space } from 'antd'
|
import { Card, Button, Typography, message, Spin } from 'antd'
|
||||||
import { ArrowLeftOutlined, PrinterOutlined, FileTextOutlined } from '@ant-design/icons'
|
import { ArrowLeftOutlined, FileTextOutlined } from '@ant-design/icons'
|
||||||
import * as examApi from '../api/exam'
|
import * as examApi from '../api/exam'
|
||||||
import type { Question } from '../types/question'
|
import type { Question } from '../types/question'
|
||||||
import type { GetExamResponse } from '../types/exam'
|
import type { GetExamResponse } from '../types/exam'
|
||||||
@ -9,6 +9,14 @@ import styles from './ExamPrint.module.less'
|
|||||||
|
|
||||||
const { Title, Paragraph, Text } = Typography
|
const { Title, Paragraph, Text } = Typography
|
||||||
|
|
||||||
|
// 日期格式化函数
|
||||||
|
const formatDate = (date: Date): string => {
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
return `${year}-${month}-${day}`
|
||||||
|
}
|
||||||
|
|
||||||
// 题型顺序映射
|
// 题型顺序映射
|
||||||
const TYPE_ORDER: Record<string, number> = {
|
const TYPE_ORDER: Record<string, number> = {
|
||||||
'fill-in-blank': 1,
|
'fill-in-blank': 1,
|
||||||
@ -38,8 +46,8 @@ const TYPE_SCORE: Record<string, number> = {
|
|||||||
'multiple-choice': 1.0,
|
'multiple-choice': 1.0,
|
||||||
'multiple-selection': 2.5,
|
'multiple-selection': 2.5,
|
||||||
'short-answer': 0, // 不计分
|
'short-answer': 0, // 不计分
|
||||||
'ordinary-essay': 25.0,
|
'ordinary-essay': 4.5,
|
||||||
'management-essay': 25.0,
|
'management-essay': 4.5,
|
||||||
}
|
}
|
||||||
|
|
||||||
const ExamPrint: React.FC = () => {
|
const ExamPrint: React.FC = () => {
|
||||||
@ -96,24 +104,9 @@ const ExamPrint: React.FC = () => {
|
|||||||
return grouped
|
return grouped
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打印试卷(不显示答案)
|
// 打印试卷
|
||||||
const handlePrintPaper = () => {
|
const handlePrint = () => {
|
||||||
if (showAnswer) {
|
window.print()
|
||||||
// 重新加载不显示答案的页面
|
|
||||||
window.location.href = `/exam/${examId}/print?show_answer=false`
|
|
||||||
} else {
|
|
||||||
window.print()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 打印答案(显示答案)
|
|
||||||
const handlePrintAnswer = () => {
|
|
||||||
if (!showAnswer) {
|
|
||||||
// 重新加载显示答案的页面
|
|
||||||
window.location.href = `/exam/${examId}/print?show_answer=true`
|
|
||||||
} else {
|
|
||||||
window.print()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化答案显示
|
// 格式化答案显示
|
||||||
@ -151,31 +144,76 @@ const ExamPrint: React.FC = () => {
|
|||||||
|
|
||||||
// 渲染填空题
|
// 渲染填空题
|
||||||
const renderFillInBlank = (question: Question, index: number) => {
|
const renderFillInBlank = (question: Question, index: number) => {
|
||||||
// 获取答案数量
|
// 获取答案数组
|
||||||
const answerCount = question.answer && Array.isArray(question.answer)
|
const answers = question.answer && Array.isArray(question.answer)
|
||||||
? question.answer.length
|
? question.answer
|
||||||
: 1
|
: question.answer
|
||||||
|
? [String(question.answer)]
|
||||||
|
: []
|
||||||
|
|
||||||
|
// 计算下划线字符数量
|
||||||
|
const calculateUnderscoreCount = (blankIndex: number, totalBlanks: number) => {
|
||||||
|
// 优先使用 answer_lengths 字段(在 show_answer=false 时也会返回)
|
||||||
|
if (question.answer_lengths && question.answer_lengths[blankIndex] !== undefined) {
|
||||||
|
const answerLength = question.answer_lengths[blankIndex]
|
||||||
|
// 最少8个下划线字符
|
||||||
|
return Math.max(answerLength, 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有实际的答案数据,使用答案长度
|
||||||
|
if (answers[blankIndex]) {
|
||||||
|
const answerText = String(answers[blankIndex])
|
||||||
|
// 最少8个下划线字符
|
||||||
|
return Math.max(answerText.length, 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有任何答案数据,使用默认策略
|
||||||
|
if (totalBlanks === 1) {
|
||||||
|
return 8 // 单个填空:8个下划线字符
|
||||||
|
} else {
|
||||||
|
// 多个填空:8-12个下划线字符循环
|
||||||
|
const counts = [8, 10, 12, 9, 11]
|
||||||
|
return counts[blankIndex % counts.length]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理题目内容,将 **** 替换为下划线字符
|
||||||
|
const renderQuestionContent = (content: string) => {
|
||||||
|
if (!content) return content
|
||||||
|
|
||||||
|
let processedContent = content
|
||||||
|
let blankIndex = 0
|
||||||
|
|
||||||
|
// 先计算出总共有多少个填空
|
||||||
|
const totalBlanks = (content.match(/\*{4,}/g) || []).length
|
||||||
|
|
||||||
|
// 将所有的 **** 替换为下划线字符
|
||||||
|
processedContent = processedContent.replace(/\*{4,}/g, () => {
|
||||||
|
const underscoreCount = calculateUnderscoreCount(blankIndex, totalBlanks)
|
||||||
|
blankIndex++
|
||||||
|
return '_'.repeat(underscoreCount)
|
||||||
|
})
|
||||||
|
|
||||||
|
return processedContent
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={question.id} className={styles.questionItem}>
|
<div key={question.id} className={styles.questionItem}>
|
||||||
<div className={styles.questionContent}>
|
<div className={styles.questionContent}>
|
||||||
<Text strong>
|
<Text>
|
||||||
{index + 1}. {question.content}
|
{index + 1}.{' '}
|
||||||
</Text>
|
</Text>
|
||||||
|
<span
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: renderQuestionContent(question.content || '')
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.answerArea}>
|
{showAnswer && answers.length > 0 && (
|
||||||
{showAnswer ? (
|
<div className={styles.answerArea}>
|
||||||
<Text>答案:{formatAnswer(question)}</Text>
|
<Text>参考答案:{formatAnswer(question)}</Text>
|
||||||
) : (
|
</div>
|
||||||
<>
|
)}
|
||||||
{Array.from({ length: answerCount }).map((_, i) => (
|
|
||||||
<div key={i} className={styles.blankLine}>
|
|
||||||
第 {i + 1} 空:__________________________________________
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -185,17 +223,10 @@ const ExamPrint: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div key={question.id} className={styles.questionItem}>
|
<div key={question.id} className={styles.questionItem}>
|
||||||
<div className={styles.questionContent}>
|
<div className={styles.questionContent}>
|
||||||
<Text strong>
|
<Text>
|
||||||
{index + 1}. {question.content}
|
{index + 1}. {question.content}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.answerArea}>
|
|
||||||
{showAnswer ? (
|
|
||||||
<Text>答案:{formatAnswer(question)}</Text>
|
|
||||||
) : (
|
|
||||||
<Text>答案:____</Text>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -205,7 +236,7 @@ const ExamPrint: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div key={question.id} className={styles.questionItem}>
|
<div key={question.id} className={styles.questionItem}>
|
||||||
<div className={styles.questionContent}>
|
<div className={styles.questionContent}>
|
||||||
<Text strong>
|
<Text>
|
||||||
{index + 1}. {question.content}
|
{index + 1}. {question.content}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
@ -216,13 +247,6 @@ const ExamPrint: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.answerArea}>
|
|
||||||
{showAnswer ? (
|
|
||||||
<Text>答案:{formatAnswer(question)}</Text>
|
|
||||||
) : (
|
|
||||||
<Text>答案:____</Text>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -232,7 +256,7 @@ const ExamPrint: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div key={question.id} className={styles.questionItem}>
|
<div key={question.id} className={styles.questionItem}>
|
||||||
<div className={styles.questionContent}>
|
<div className={styles.questionContent}>
|
||||||
<Text strong>
|
<Text>
|
||||||
{index + 1}. {question.content}
|
{index + 1}. {question.content}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
@ -243,13 +267,6 @@ const ExamPrint: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.answerArea}>
|
|
||||||
{showAnswer ? (
|
|
||||||
<Text>答案:{formatAnswer(question)}</Text>
|
|
||||||
) : (
|
|
||||||
<Text>答案:____</Text>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -259,26 +276,21 @@ const ExamPrint: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div key={question.id} className={styles.questionItem}>
|
<div key={question.id} className={styles.questionItem}>
|
||||||
<div className={styles.questionContent}>
|
<div className={styles.questionContent}>
|
||||||
<Text strong>
|
<Text>
|
||||||
{index + 1}. {question.content}
|
{index + 1}. {question.content}
|
||||||
</Text>
|
</Text>
|
||||||
{!showAnswer && (
|
|
||||||
<Text type="secondary" style={{ fontSize: '12px', marginLeft: '8px' }}>
|
|
||||||
(仅供参考,不计分)
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.answerArea}>
|
<div className={styles.answerArea}>
|
||||||
{showAnswer ? (
|
{showAnswer ? (
|
||||||
<div className={styles.essayAnswer}>
|
<div className={styles.essayAnswer}>
|
||||||
<Text strong>参考答案:</Text>
|
<Text>参考答案:</Text>
|
||||||
<Paragraph>{formatAnswer(question)}</Paragraph>
|
<Paragraph>{formatAnswer(question)}</Paragraph>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.answerLines}>
|
<div className={styles.answerLines}>
|
||||||
{Array.from({ length: 5 }).map((_, i) => (
|
{Array.from({ length: 5 }).map((_, i) => (
|
||||||
<div key={i} className={styles.answerLine}>
|
<div key={i} className={styles.answerLine} style={{ height: '25px' }}>
|
||||||
_____________________________________________________________________________
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -290,24 +302,36 @@ const ExamPrint: React.FC = () => {
|
|||||||
|
|
||||||
// 渲染论述题
|
// 渲染论述题
|
||||||
const renderEssay = (question: Question, index: number) => {
|
const renderEssay = (question: Question, index: number) => {
|
||||||
|
const getUserTypeHint = () => {
|
||||||
|
if (question.type === 'ordinary-essay') {
|
||||||
|
return '(普通涉密人员作答)'
|
||||||
|
} else if (question.type === 'management-essay') {
|
||||||
|
return '(保密管理人员作答)'
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={question.id} className={styles.questionItem}>
|
<div key={question.id} className={styles.questionItem}>
|
||||||
<div className={styles.questionContent}>
|
<div className={styles.questionContent}>
|
||||||
<Text strong>
|
<Text>
|
||||||
{index + 1}. {question.content}
|
{index + 1}. {question.content}
|
||||||
</Text>
|
</Text>
|
||||||
|
<Text type="secondary" style={{ fontSize: '12px', marginLeft: '8px' }}>
|
||||||
|
{getUserTypeHint()}
|
||||||
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.answerArea}>
|
<div className={styles.answerArea}>
|
||||||
{showAnswer ? (
|
{showAnswer ? (
|
||||||
<div className={styles.essayAnswer}>
|
<div className={styles.essayAnswer}>
|
||||||
<Text strong>参考答案:</Text>
|
<Text>参考答案:</Text>
|
||||||
<Paragraph>{formatAnswer(question)}</Paragraph>
|
<Paragraph>{formatAnswer(question)}</Paragraph>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.answerLines}>
|
<div className={styles.answerLines}>
|
||||||
{Array.from({ length: 10 }).map((_, i) => (
|
{Array.from({ length: 5 }).map((_, i) => (
|
||||||
<div key={i} className={styles.answerLine}>
|
<div key={i} className={styles.answerLine} style={{ height: '25px' }}>
|
||||||
_____________________________________________________________________________
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -319,13 +343,8 @@ const ExamPrint: React.FC = () => {
|
|||||||
|
|
||||||
// 渲染题目组
|
// 渲染题目组
|
||||||
const renderQuestionGroup = (type: string, questions: Question[]) => {
|
const renderQuestionGroup = (type: string, questions: Question[]) => {
|
||||||
let startIndex = 0
|
// 每个题型分类都从1开始编号
|
||||||
// 计算该题型的起始序号
|
const startIndex = 0
|
||||||
Object.keys(groupedQuestions)
|
|
||||||
.filter((t) => TYPE_ORDER[t] < TYPE_ORDER[type])
|
|
||||||
.forEach((t) => {
|
|
||||||
startIndex += groupedQuestions[t].length
|
|
||||||
})
|
|
||||||
|
|
||||||
// 计算该题型总分
|
// 计算该题型总分
|
||||||
const totalScore = questions.length * TYPE_SCORE[type]
|
const totalScore = questions.length * TYPE_SCORE[type]
|
||||||
@ -333,7 +352,7 @@ const ExamPrint: React.FC = () => {
|
|||||||
return (
|
return (
|
||||||
<div key={type} className={styles.questionGroup}>
|
<div key={type} className={styles.questionGroup}>
|
||||||
<div className={styles.groupHeader}>
|
<div className={styles.groupHeader}>
|
||||||
<Text strong className={styles.groupTitle}>
|
<Text className={styles.groupTitle}>
|
||||||
{TYPE_NAME[type]}
|
{TYPE_NAME[type]}
|
||||||
</Text>
|
</Text>
|
||||||
{TYPE_SCORE[type] > 0 && (
|
{TYPE_SCORE[type] > 0 && (
|
||||||
@ -397,22 +416,13 @@ const ExamPrint: React.FC = () => {
|
|||||||
>
|
>
|
||||||
返回
|
返回
|
||||||
</Button>
|
</Button>
|
||||||
<Space>
|
<Button
|
||||||
<Button
|
type="primary"
|
||||||
type="default"
|
icon={<FileTextOutlined />}
|
||||||
icon={<FileTextOutlined />}
|
onClick={handlePrint}
|
||||||
onClick={handlePrintPaper}
|
>
|
||||||
>
|
打印试卷
|
||||||
打印试卷
|
</Button>
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
icon={<PrinterOutlined />}
|
|
||||||
onClick={handlePrintAnswer}
|
|
||||||
>
|
|
||||||
打印答案
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 打印内容区 */}
|
{/* 打印内容区 */}
|
||||||
@ -422,12 +432,11 @@ const ExamPrint: React.FC = () => {
|
|||||||
<Title level={2} className={styles.paperTitle}>
|
<Title level={2} className={styles.paperTitle}>
|
||||||
保密知识模拟考试{showAnswer ? '(答案)' : ''}
|
保密知识模拟考试{showAnswer ? '(答案)' : ''}
|
||||||
</Title>
|
</Title>
|
||||||
<div className={styles.examInfo}>
|
<div style={{ fontFamily: 'SimSun, 宋体, serif', fontSize: '9pt' }}>
|
||||||
<div className={styles.infoItem}>
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', width: '100%' }}>
|
||||||
姓名:__________________
|
<span style={{ flex: '0 0 auto', textAlign: 'left' }}>日期:{formatDate(new Date())}</span>
|
||||||
</div>
|
<span style={{ flex: 1, textAlign: 'center' }}>姓名:________________</span>
|
||||||
<div className={styles.infoItem}>
|
<span style={{ flex: '0 0 auto', textAlign: 'right' }}>成绩:________________</span>
|
||||||
日期:__________________
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -437,10 +446,9 @@ const ExamPrint: React.FC = () => {
|
|||||||
<Card className={styles.instructionCard}>
|
<Card className={styles.instructionCard}>
|
||||||
<Title level={4}>考试说明</Title>
|
<Title level={4}>考试说明</Title>
|
||||||
<ul>
|
<ul>
|
||||||
<li>本试卷满分100分,考试时间为90分钟</li>
|
<li>本试卷满分100分,考试时间为60分钟</li>
|
||||||
<li>请在答题区域内作答,字迹清晰工整</li>
|
<li>填空题每题8分,请在答题区域内作答,字迹清晰工整</li>
|
||||||
<li>简答题仅供参考,不计入总分</li>
|
<li>论述题每题9分,从以下2道题目中任选1道作答</li>
|
||||||
<li>论述题从以下2道题目中任选1道作答</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
@ -455,11 +463,11 @@ const ExamPrint: React.FC = () => {
|
|||||||
{essayQuestions.length > 0 && (
|
{essayQuestions.length > 0 && (
|
||||||
<div className={styles.questionGroup}>
|
<div className={styles.questionGroup}>
|
||||||
<div className={styles.groupHeader}>
|
<div className={styles.groupHeader}>
|
||||||
<Text strong className={styles.groupTitle}>
|
<Text className={styles.groupTitle}>
|
||||||
{TYPE_NAME['ordinary-essay']}
|
{TYPE_NAME['ordinary-essay']}
|
||||||
</Text>
|
</Text>
|
||||||
<Text type="secondary" className={styles.groupScore}>
|
<Text type="secondary" className={styles.groupScore}>
|
||||||
(以下2道论述题任选1道作答,共25分)
|
(以下2道论述题任选1道作答,共9分)
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.questionsList}>
|
<div className={styles.questionsList}>
|
||||||
|
|||||||
@ -116,12 +116,14 @@ export interface StartExamResponse {
|
|||||||
|
|
||||||
// 提交试卷响应
|
// 提交试卷响应
|
||||||
export interface SubmitExamResponse {
|
export interface SubmitExamResponse {
|
||||||
score: number
|
record_id?: number // 后端返回的考试记录ID
|
||||||
total_score: number
|
score?: number
|
||||||
is_passed: boolean
|
total_score?: number
|
||||||
time_spent: number
|
is_passed?: boolean
|
||||||
answers: ExamAnswer[]
|
time_spent?: number
|
||||||
detailed_results: Record<string, {
|
status?: string
|
||||||
|
answers?: ExamAnswer[]
|
||||||
|
detailed_results?: Record<string, {
|
||||||
correct: boolean
|
correct: boolean
|
||||||
score: number
|
score: number
|
||||||
message?: string
|
message?: string
|
||||||
|
|||||||
@ -16,6 +16,7 @@ export interface Question {
|
|||||||
options: Option[]
|
options: Option[]
|
||||||
category: string
|
category: string
|
||||||
answer?: any // 正确答案(用于题目管理编辑)
|
answer?: any // 正确答案(用于题目管理编辑)
|
||||||
|
answer_lengths?: number[] // 答案长度数组(用于打印时计算横线长度)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提交答案
|
// 提交答案
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user