import React, { useState, useEffect } from 'react' import { useParams, useNavigate, useSearchParams } from 'react-router-dom' import { Card, Button, Typography, message, Spin } from 'antd' import { ArrowLeftOutlined, FileTextOutlined } from '@ant-design/icons' import * as examApi from '../api/exam' import type { Question } from '../types/question' import type { GetExamResponse } from '../types/exam' import styles from './ExamPrint.module.less' 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 = { 'fill-in-blank': 1, 'true-false': 2, 'multiple-choice': 3, 'multiple-selection': 4, 'short-answer': 5, 'ordinary-essay': 6, 'management-essay': 6, } // 题型名称映射 const TYPE_NAME: Record = { 'fill-in-blank': '一、填空题', 'true-false': '二、判断题', 'multiple-choice': '三、单选题', 'multiple-selection': '四、多选题', 'short-answer': '五、简答题', 'ordinary-essay': '六、论述题', 'management-essay': '六、论述题', } // 题型分值映射 const TYPE_SCORE: Record = { 'fill-in-blank': 2.0, 'true-false': 2.0, 'multiple-choice': 1.0, 'multiple-selection': 2.5, 'short-answer': 0, // 不计分 'ordinary-essay': 4.5, 'management-essay': 4.5, } const ExamPrint: React.FC = () => { const { examId } = useParams<{ examId: string }>() const navigate = useNavigate() const [searchParams] = useSearchParams() const showAnswer = searchParams.get('show_answer') === 'true' const [loading, setLoading] = useState(false) const [examData, setExamData] = useState(null) const [groupedQuestions, setGroupedQuestions] = useState>({}) // 加载考试详情 useEffect(() => { if (!examId) { message.error('考试ID不存在') navigate('/exam/management') return } const loadExam = async () => { setLoading(true) try { const res = await examApi.getExam(Number(examId), showAnswer) if (res.success && res.data) { setExamData(res.data) // 按题型分组 const grouped = groupQuestionsByType(res.data.questions) setGroupedQuestions(grouped) } else { message.error('加载考试失败') navigate('/exam/management') } } catch (error: any) { message.error(error.response?.data?.message || '加载考试失败') navigate('/exam/management') } finally { setLoading(false) } } loadExam() }, [examId, showAnswer, navigate]) // 按题型分组题目 const groupQuestionsByType = (questions: Question[]) => { const grouped: Record = {} questions.forEach((q) => { if (!grouped[q.type]) { grouped[q.type] = [] } grouped[q.type].push(q) }) return grouped } // 打印试卷 const handlePrint = () => { window.print() } // 格式化答案显示 const formatAnswer = (question: Question): string => { if (!question.answer) return '' switch (question.type) { case 'fill-in-blank': if (Array.isArray(question.answer)) { return question.answer.join('、') } return String(question.answer) case 'true-false': return question.answer === 'true' || question.answer === true ? '正确' : '错误' case 'multiple-choice': return String(question.answer) case 'multiple-selection': if (Array.isArray(question.answer)) { return question.answer.sort().join('') } return String(question.answer) case 'short-answer': case 'ordinary-essay': case 'management-essay': return String(question.answer) default: return String(question.answer) } } // 渲染填空题 const renderFillInBlank = (question: Question, index: number) => { // 获取答案数组 const answers = question.answer && Array.isArray(question.answer) ? question.answer : 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 (
{index + 1}.{' '}
{showAnswer && answers.length > 0 && (
参考答案:{formatAnswer(question)}
)}
) } // 渲染判断题 const renderTrueFalse = (question: Question, index: number) => { return (
{index + 1}. {question.content}
) } // 渲染单选题 const renderMultipleChoice = (question: Question, index: number) => { return (
{index + 1}. {question.content}
{(question.options || []).map((opt) => (
{opt.key}. {opt.value}
))}
) } // 渲染多选题 const renderMultipleSelection = (question: Question, index: number) => { return (
{index + 1}. {question.content}
{(question.options || []).map((opt) => (
{opt.key}. {opt.value}
))}
) } // 渲染简答题 const renderShortAnswer = (question: Question, index: number) => { return (
{index + 1}. {question.content}
{showAnswer ? (
参考答案: {formatAnswer(question)}
) : (
{Array.from({ length: 8 }).map((_, i) => (
 
))}
)}
) } // 渲染论述题 const renderEssay = (question: Question, index: number) => { const getUserTypeHint = () => { if (question.type === 'ordinary-essay') { return '(普通涉密人员作答)' } else if (question.type === 'management-essay') { return '(保密管理人员作答)' } return '' } return (
{index + 1}. {question.content} {getUserTypeHint()}
{showAnswer ? (
参考答案: {formatAnswer(question)}
) : (
{Array.from({ length: 10 }).map((_, i) => (
 
))}
)}
) } // 渲染题目组 const renderQuestionGroup = (type: string, questions: Question[]) => { // 每个题型分类都从1开始编号 const startIndex = 0 // 计算该题型总分 const totalScore = questions.length * TYPE_SCORE[type] return (
{TYPE_NAME[type]} {TYPE_SCORE[type] > 0 && ( (共{questions.length}题,每题{TYPE_SCORE[type]}分,共{totalScore}分) )}
{questions.map((question, index) => { switch (type) { case 'fill-in-blank': return renderFillInBlank(question, startIndex + index) case 'true-false': return renderTrueFalse(question, startIndex + index) case 'multiple-choice': return renderMultipleChoice(question, startIndex + index) case 'multiple-selection': return renderMultipleSelection(question, startIndex + index) case 'short-answer': return renderShortAnswer(question, startIndex + index) case 'ordinary-essay': case 'management-essay': return renderEssay(question, startIndex + index) default: return null } })}
) } if (loading) { return (
加载考试中...
) } if (!examData) { return null } // 获取论述题(合并普通和管理两类) const essayQuestions = [ ...(groupedQuestions['ordinary-essay'] || []), ...(groupedQuestions['management-essay'] || []), ] return (
{/* 操作按钮区 - 打印时隐藏 */}
{/* 打印内容区 */}
{/* 试卷头部 */}
保密知识模拟考试{showAnswer ? '(答案)' : ''}
日期:{formatDate(new Date())} 姓名:________________ 职位:________________ 成绩:________________
{/* 考试说明 */} {!showAnswer && ( 考试说明
  • 本试卷满分100分,考试时间为60分钟
  • 简答题每题8分,请在答题区域内作答,字迹清晰工整
  • 论述题每题9分,从以下2道题目中任选1道作答
)} {/* 按题型渲染题目 */} {Object.keys(groupedQuestions) .filter((type) => type !== 'ordinary-essay' && type !== 'management-essay') .sort((a, b) => TYPE_ORDER[a] - TYPE_ORDER[b]) .map((type) => renderQuestionGroup(type, groupedQuestions[type]))} {/* 论述题部分 */} {essayQuestions.length > 0 && (
{TYPE_NAME['ordinary-essay']} (以下2道论述题任选1道作答,共9分)
{essayQuestions.map((question, index) => renderEssay(question, index))}
)}
) } export default ExamPrint