Compare commits

..

No commits in common. "0464223d003730c2e4da35d2bc112fbb26bcce80" and "c4a8b28abe82456e8fa90ab8aad1b1e791fcaa38" have entirely different histories.

11 changed files with 210 additions and 350 deletions

View File

@ -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": 4.5, "ordinary-essay": 5.0,
"management-essay": 4.5, "management-essay": 5.0,
} }
// 评分 // 评分

View File

@ -44,6 +44,12 @@ 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分
@ -51,8 +57,7 @@ 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: "ordinary-essay", Count: 1, Score: 4.5}, // 4.5分(普通涉密人员论述题) {Type: essayType, Count: 1, Score: 5.0}, // 5分根据用户类型选择论述题
{Type: "management-essay", Count: 1, Score: 4.5}, // 4.5分(保密管理人员论述题)
} }
// 按题型配置随机抽取题目 // 按题型配置随机抽取题目
@ -261,39 +266,6 @@ 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 {
@ -306,15 +278,6 @@ 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{

View File

@ -574,15 +574,9 @@ 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格式
@ -592,20 +586,6 @@ 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
}
}
} }
} }
} }

View File

@ -25,7 +25,6 @@ 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 练习题答案提交

View File

@ -4,6 +4,7 @@ import {
Card, Card,
Button, Button,
Typography, Typography,
Tag,
Space, Space,
Spin, Spin,
Row, Row,
@ -12,14 +13,17 @@ import {
message message
} from 'antd' } from 'antd'
import { import {
HomeOutlined FileTextOutlined,
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 } = Typography const { Text, Paragraph } = Typography
// 题型名称映射 // 题型名称映射
const TYPE_NAME: Record<string, string> = { const TYPE_NAME: Record<string, string> = {
@ -56,7 +60,7 @@ const ExamAnswerView: React.FC = () => {
// 处理打印功能 // 处理打印功能
const handlePrint = () => { const handlePrint = () => {
// 设置打印标题 // 设置打印标题
document.title = `试卷答案_打印版` document.title = `${examData?.exam?.title || '试卷答案'}_打印版`
// 触发打印 // 触发打印
window.print() window.print()
@ -111,8 +115,29 @@ 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) => { const renderAnswerDetail = (question: Question, index: number, type: string) => {
// 格式化答案显示 // 格式化答案显示
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 === '') {
@ -207,7 +232,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>
@ -249,7 +274,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)} {renderAnswerDetail(q, index, type)}
</div> </div>
))} ))}
</div> </div>
@ -264,7 +289,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)} {renderAnswerDetail(q, globalIndex, type)}
</td> </td>
); );
})} })}

View File

@ -26,8 +26,7 @@ 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'
@ -226,13 +225,6 @@ 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

View File

@ -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,87 +212,38 @@ const ExamOnline: React.FC = () => {
// 渲染填空题 // 渲染填空题
const renderFillInBlank = (question: Question, index: number) => { const renderFillInBlank = (question: Question, index: number) => {
// 获取答案数组 // 获取答案数量(如果有 answer 字段)
const answers = question.answer && Array.isArray(question.answer) const answerCount = question.answer
? question.answer ? Array.isArray(question.answer)
: question.answer ? question.answer.length
? [String(question.answer)] : 1
: [] : 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 style={{ marginBottom: 12, display: 'block' }}> <Text strong>
{index + 1}. {index + 1}. {question.content}
</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(blankCount).fill('')} initialValue={Array(answerCount).fill('')}
style={{ display: 'none' }} // 隐藏原来的表单项,因为我们用内联输入框了
> >
<Input /> <Space direction="vertical" style={{ width: '100%' }}>
{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>
) )

View File

@ -208,7 +208,7 @@
// A4纸张设置 // A4纸张设置
@page { @page {
size: A4; size: A4;
margin: 0.8cm; margin: 2cm;
} }
.container { .container {
@ -223,128 +223,88 @@
} }
.paperHeader { .paperHeader {
margin-bottom: 6px; margin-bottom: 20px;
padding-bottom: 4px; padding-bottom: 12px;
.paperTitle { .paperTitle {
font-size: 16pt !important; font-size: 24pt !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: 9pt; font-size: 12pt;
white-space: nowrap;
flex: 1;
text-align: center;
} }
} }
} }
.instructionCard { .instructionCard {
margin-bottom: 6px; margin-bottom: 20px;
border: 1px solid #000; border: 1px solid #000;
:global(.ant-card-body) { :global(.ant-card-body) {
padding: 6px; padding: 16px;
} }
h4 { h4 {
font-size: 10pt; font-size: 14pt;
margin-bottom: 2px;
} }
ul { ul {
margin: 0;
padding-left: 12px;
li { li {
font-size: 8pt; font-size: 11pt;
margin-bottom: 1px;
line-height: 1.2;
} }
} }
} }
.questionGroup { .questionGroup {
margin-bottom: 8px; margin-bottom: 28px;
page-break-inside: avoid;
.groupHeader { .groupHeader {
margin-bottom: 4px;
padding-bottom: 2px;
.groupTitle { .groupTitle {
font-size: 12pt; font-size: 16pt;
} }
.groupScore { .groupScore {
font-size: 10pt; font-size: 12pt;
} }
} }
} }
.questionItem { .questionItem {
margin-bottom: 6px; margin-bottom: 20px;
page-break-inside: avoid;
.questionContent { .questionContent {
margin-bottom: 3px;
line-height: 1.3;
span { span {
font-size: 10pt; font-size: 12pt;
font-family: 'SimSun', '宋体', serif !important;
font-weight: normal !important;
} }
} }
.optionsList { .optionsList {
margin: 4px 0;
padding-left: 14px;
.optionItem { .optionItem {
margin-bottom: 1px; font-size: 11pt;
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: 9pt; font-size: 11pt;
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;
} }
} }
} }
@ -355,16 +315,17 @@
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;
} }
// 保留边框 // 保留边框

View File

@ -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 } from 'antd' import { Card, Button, Typography, message, Spin, Space } from 'antd'
import { ArrowLeftOutlined, FileTextOutlined } from '@ant-design/icons' import { ArrowLeftOutlined, PrinterOutlined, 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,14 +9,6 @@ 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,
@ -46,8 +38,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': 4.5, 'ordinary-essay': 25.0,
'management-essay': 4.5, 'management-essay': 25.0,
} }
const ExamPrint: React.FC = () => { const ExamPrint: React.FC = () => {
@ -104,9 +96,24 @@ const ExamPrint: React.FC = () => {
return grouped return grouped
} }
// 打印试卷 // 打印试卷(不显示答案)
const handlePrint = () => { const handlePrintPaper = () => {
window.print() if (showAnswer) {
// 重新加载不显示答案的页面
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()
}
} }
// 格式化答案显示 // 格式化答案显示
@ -144,76 +151,31 @@ const ExamPrint: React.FC = () => {
// 渲染填空题 // 渲染填空题
const renderFillInBlank = (question: Question, index: number) => { const renderFillInBlank = (question: Question, index: number) => {
// 获取答案数组 // 获取答案数量
const answers = question.answer && Array.isArray(question.answer) const answerCount = question.answer && Array.isArray(question.answer)
? question.answer ? question.answer.length
: question.answer : 1
? [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> <Text strong>
{index + 1}.{' '} {index + 1}. {question.content}
</Text> </Text>
<span
dangerouslySetInnerHTML={{
__html: renderQuestionContent(question.content || '')
}}
/>
</div> </div>
{showAnswer && answers.length > 0 && ( <div className={styles.answerArea}>
<div className={styles.answerArea}> {showAnswer ? (
<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>
) )
} }
@ -223,10 +185,17 @@ 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> <Text strong>
{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>
) )
} }
@ -236,7 +205,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> <Text strong>
{index + 1}. {question.content} {index + 1}. {question.content}
</Text> </Text>
</div> </div>
@ -247,6 +216,13 @@ const ExamPrint: React.FC = () => {
</div> </div>
))} ))}
</div> </div>
<div className={styles.answerArea}>
{showAnswer ? (
<Text>{formatAnswer(question)}</Text>
) : (
<Text>____</Text>
)}
</div>
</div> </div>
) )
} }
@ -256,7 +232,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> <Text strong>
{index + 1}. {question.content} {index + 1}. {question.content}
</Text> </Text>
</div> </div>
@ -267,6 +243,13 @@ const ExamPrint: React.FC = () => {
</div> </div>
))} ))}
</div> </div>
<div className={styles.answerArea}>
{showAnswer ? (
<Text>{formatAnswer(question)}</Text>
) : (
<Text>____</Text>
)}
</div>
</div> </div>
) )
} }
@ -276,21 +259,26 @@ 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> <Text strong>
{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></Text> <Text strong></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} style={{ height: '25px' }}> <div key={i} className={styles.answerLine}>
&nbsp; _____________________________________________________________________________
</div> </div>
))} ))}
</div> </div>
@ -302,36 +290,24 @@ 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> <Text strong>
{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></Text> <Text strong></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: 10 }).map((_, i) => (
<div key={i} className={styles.answerLine} style={{ height: '25px' }}> <div key={i} className={styles.answerLine}>
&nbsp; _____________________________________________________________________________
</div> </div>
))} ))}
</div> </div>
@ -343,8 +319,13 @@ const ExamPrint: React.FC = () => {
// 渲染题目组 // 渲染题目组
const renderQuestionGroup = (type: string, questions: Question[]) => { const renderQuestionGroup = (type: string, questions: Question[]) => {
// 每个题型分类都从1开始编号 let startIndex = 0
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]
@ -352,7 +333,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 className={styles.groupTitle}> <Text strong className={styles.groupTitle}>
{TYPE_NAME[type]} {TYPE_NAME[type]}
</Text> </Text>
{TYPE_SCORE[type] > 0 && ( {TYPE_SCORE[type] > 0 && (
@ -416,13 +397,22 @@ const ExamPrint: React.FC = () => {
> >
</Button> </Button>
<Button <Space>
type="primary" <Button
icon={<FileTextOutlined />} type="default"
onClick={handlePrint} icon={<FileTextOutlined />}
> onClick={handlePrintPaper}
>
</Button>
</Button>
<Button
type="primary"
icon={<PrinterOutlined />}
onClick={handlePrintAnswer}
>
</Button>
</Space>
</div> </div>
{/* 打印内容区 */} {/* 打印内容区 */}
@ -432,11 +422,12 @@ const ExamPrint: React.FC = () => {
<Title level={2} className={styles.paperTitle}> <Title level={2} className={styles.paperTitle}>
{showAnswer ? '(答案)' : ''} {showAnswer ? '(答案)' : ''}
</Title> </Title>
<div style={{ fontFamily: 'SimSun, 宋体, serif', fontSize: '9pt' }}> <div className={styles.examInfo}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', width: '100%' }}> <div className={styles.infoItem}>
<span style={{ flex: '0 0 auto', textAlign: 'left' }}>{formatDate(new Date())}</span> __________________
<span style={{ flex: 1, textAlign: 'center' }}>________________</span> </div>
<span style={{ flex: '0 0 auto', textAlign: 'right' }}>________________</span> <div className={styles.infoItem}>
__________________
</div> </div>
</div> </div>
</div> </div>
@ -446,9 +437,10 @@ const ExamPrint: React.FC = () => {
<Card className={styles.instructionCard}> <Card className={styles.instructionCard}>
<Title level={4}></Title> <Title level={4}></Title>
<ul> <ul>
<li>10060</li> <li>10090</li>
<li>8</li> <li></li>
<li>921</li> <li></li>
<li>21</li>
</ul> </ul>
</Card> </Card>
)} )}
@ -463,11 +455,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 className={styles.groupTitle}> <Text strong 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}>
219 2125
</Text> </Text>
</div> </div>
<div className={styles.questionsList}> <div className={styles.questionsList}>

View File

@ -116,14 +116,12 @@ export interface StartExamResponse {
// 提交试卷响应 // 提交试卷响应
export interface SubmitExamResponse { export interface SubmitExamResponse {
record_id?: number // 后端返回的考试记录ID score: number
score?: number total_score: number
total_score?: number is_passed: boolean
is_passed?: boolean time_spent: number
time_spent?: number answers: ExamAnswer[]
status?: string detailed_results: Record<string, {
answers?: ExamAnswer[]
detailed_results?: Record<string, {
correct: boolean correct: boolean
score: number score: number
message?: string message?: string

View File

@ -16,7 +16,6 @@ export interface Question {
options: Option[] options: Option[]
category: string category: string
answer?: any // 正确答案(用于题目管理编辑) answer?: any // 正确答案(用于题目管理编辑)
answer_lengths?: number[] // 答案长度数组(用于打印时计算横线长度)
} }
// 提交答案 // 提交答案