# 考试管理API对接实施计划 > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. **Goal:** 将「考察与训练」模块中「智能组卷」和「互动训练」子功能对接后端考试管理API,替换模拟数据 **架构:** - 智能组卷使用 `POST /exam/paper/generate` 从题库自动组卷,结果仅前端暂存 - 互动训练闯关模式使用预设5级固定关卡,每关调用组卷接口生成题目 - 错题本通过 `POST /exam/answers/query` 查询答题记录,筛选错题展示 **Tech Stack:** Vue 3, Axios, Ant Design Vue --- ### Task 1: exam.js 新增 API 方法 **Files:** - Modify: `src/api/exam.js` - [ ] **Step 1: 新增 generatePaper 方法** 在 `exam.js` 的 `examAPI` 对象末尾、`gradeAnswers` 之后添加: ```javascript generatePaper: (params) => { console.log('[examAPI] 生成试卷 - 参数:', params) const requestBody = { single_choice_count: params.single_choice_count || 0, multiple_choice_count: params.multiple_choice_count || 0, true_false_count: params.true_false_count || 0, fill_blank_count: params.fill_blank_count || 0, subjective_count: params.subjective_count || 0, include_personal: params.include_personal || false, difficulty: params.difficulty || 2, collection: params.collection, collection_name: params.collection_name, file_ids: params.file_ids || [] } return apiClient.post('/exam/paper/generate', requestBody, { timeout: 120000 }).then(handleExamResponse).then(response => { console.log('[examAPI] 试卷生成成功:', response) return response }) }, ``` - [ ] **Step 2: 新增 queryUserAnswers 方法** 在 `generatePaper` 之后添加: ```javascript queryUserAnswers: (request) => { console.log('[examAPI] 查询答题记录 - 参数:', request) return apiClient.post('/exam/answers/query', request, { timeout: 30000 }).then(response => { const data = response.data if (data.code === 200 && data.data) { return data.data } throw new Error(data.message || '查询答题记录失败') }) }, ``` --- ### Task 2: 智能组卷 - 改造 handleGeneratePaper **Files:** - Modify: `src/components/ExamModule.vue` - [ ] **Step 1: 替换 handleGeneratePaper 函数** 找到 `const handleGeneratePaper = async () => {` 函数(约2281行),将其整体替换为: ```javascript const handleGeneratePaper = async () => { if (!paperConfig.doc) { message.warning('请先选择制度文件') return } const totalCount = paperConfig.singleCount + paperConfig.multipleCount + paperConfig.judgmentCount + paperConfig.essayCount if (totalCount <= 0) { message.warning('请至少设置一种题型的数量') return } isGeneratingPaper.value = true showGeneratePaperModal.value = true try { let collection = 'public_kb' let collectionName = '' let fileIds = [] if (paperConfig.doc) { if (typeof paperConfig.doc === 'object') { if (paperConfig.doc.collectionName) { collection = paperConfig.doc.collectionName collectionName = paperConfig.doc.collectionName } if (paperConfig.doc.id) { fileIds = [paperConfig.doc.id] } } } // 难度百分比 → 1-5 加权计算 const weightedDifficulty = Math.round( (paperConfig.easyPercent * 1 + paperConfig.mediumPercent * 3 + paperConfig.hardPercent * 5) / 100 ) const params = { single_choice_count: paperConfig.singleCount, multiple_choice_count: paperConfig.multipleCount, true_false_count: paperConfig.judgmentCount, subjective_count: paperConfig.essayCount, fill_blank_count: 0, include_personal: false, difficulty: weightedDifficulty, collection: collection, collection_name: collectionName, file_ids: fileIds } const result = await examAPI.generatePaper(params) const questions = (result.questions || []).map((q, index) => ({ id: index + 1, questionId: q.question_id, type: q.question_type, typeLabel: q.question_type_name || q.question_type, difficulty: q.difficulty, score: q.score, stem: q.content?.stem || '', options: q.content?.data?.options || q.content?.options || [], answer: q.content?.answer || '', explanation: q.content?.explanation || '', studentAnswer: null })) const newPaper = { id: Date.now(), paperId: result.paper_id, name: paperConfig.name || '智能组卷', doc: typeof paperConfig.doc === 'object' ? (paperConfig.doc.title || paperConfig.doc.rawFileName || '') : paperConfig.doc, questions: questions.length, totalScore: result.total_score || 100, duration: paperConfig.duration, status: 'draft', questionList: questions } papers.value.unshift(newPaper) message.success(`试卷生成成功!共 ${questions.length} 道题`) } catch (error) { console.error('智能组卷失败:', error) message.error('智能组卷失败: ' + (error.message || '请检查网络连接')) } finally { isGeneratingPaper.value = false setTimeout(() => { showGeneratePaperModal.value = false }, 500) } } ``` - [ ] **Step 2: 添加 isGeneratingPaper ref** 在文件 script 部分的 ref 声明区(约1550行附近),找到 `const isGenerating = ref(false)` 所在区块,在其附近添加: ```javascript const isGeneratingPaper = ref(false) ``` --- ### Task 3: 智能组卷 - 更新查看试卷弹窗 **Files:** - Modify: `src/components/ExamModule.vue` - [ ] **Step 1: 更新查看试卷弹窗以展示真实题目** 找到 `showViewPaperModal` 对应弹窗的模板(约964-1018行),将弹窗内容替换为: ```html
{{ currentPaper.name }} {{ currentPaper.doc }} {{ currentPaper.questions }} 题 {{ currentPaper.totalScore || 100 }} 分 {{ currentPaper.duration }} 分钟

试卷题目预览

{{ q.typeLabel || q.type }} 第 {{ idx + 1 }} 题({{ q.score || 5 }} 分)
{{ q.stem }}
{{ opt.key }}. {{ opt.content }}
暂无题目预览
``` --- ### Task 4: 互动训练 - 预设闯关关卡 **Files:** - Modify: `src/components/ExamModule.vue` - [ ] **Step 1: 替换 trainingData 为 ref 响应式数据** 找到 `const trainingData = {` 定义(约1819行),替换为: ```javascript const trainingData = ref({ checkpoints: [ { id: 1, name: '入门挑战', status: 'in-progress', score: 0, singleCount: 3, multipleCount: 0, trueFalseCount: 2, fillBlankCount: 0, subjectiveCount: 0, difficulty: 1 }, { id: 2, name: '基础巩固', status: 'pending', score: 0, singleCount: 4, multipleCount: 2, trueFalseCount: 2, fillBlankCount: 0, subjectiveCount: 0, difficulty: 2 }, { id: 3, name: '进阶提升', status: 'pending', score: 0, singleCount: 4, multipleCount: 3, trueFalseCount: 3, fillBlankCount: 0, subjectiveCount: 0, difficulty: 3 }, { id: 4, name: '高级挑战', status: 'pending', score: 0, singleCount: 3, multipleCount: 3, trueFalseCount: 2, fillBlankCount: 0, subjectiveCount: 2, difficulty: 4 }, { id: 5, name: '大师试炼', status: 'pending', score: 0, singleCount: 4, multipleCount: 4, trueFalseCount: 2, fillBlankCount: 0, subjectiveCount: 2, difficulty: 5 } ], dailyExercises: [], wrongQuestions: [] }) ``` - [ ] **Step 2: 替换闯关模式模板中涉及 trainingData 的属性访问** 找到模板中所有 `trainingData.` 开头的属性访问,替换为 `trainingData.` -> `trainingData.`(Vue ref 在模板中自动解包,用 `.value` 或在 script 中通过 computed 适配)。 将模板中的统计部分(约613-634行): ```html
{{ trainingData.checkpoints.filter(c => c.status === 'completed').length }}
``` 改为: ```html
{{ completedCheckpointCount }}
``` - [ ] **Step 3: 添加计算属性** 在 script 区添加计算属性以适配模板中的统计展示: ```javascript const completedCheckpointCount = computed(() => { return trainingData.value.checkpoints.filter(c => c.status === 'completed').length }) ``` - [ ] **Step 4: 替换闯关模式模板的关卡列表渲染** 找到闯关模式面板的模板(约659-693行),替换为: ```html

🗺️ 闯关地图

共 {{ trainingData.value.checkpoints.length }} 关
{{ checkpoint.id }}
{{ checkpoint.name }}
{{ checkpoint.status === 'completed' ? `✅ 得分:${checkpoint.score}分` : checkpoint.status === 'in-progress' ? '🔄 进行中...' : '⏳ 未开始' }}
``` - [ ] **Step 5: 更新 handleStartCheckpoint 函数** 找到该函数(约2386行),替换为: ```javascript const handleStartCheckpoint = async (checkpoint) => { try { const params = { single_choice_count: checkpoint.singleCount || 0, multiple_choice_count: checkpoint.multipleCount || 0, true_false_count: checkpoint.trueFalseCount || 0, fill_blank_count: checkpoint.fillBlankCount || 0, subjective_count: checkpoint.subjectiveCount || 0, include_personal: false, difficulty: checkpoint.difficulty || 2, collection: 'public_kb' } message.loading({ content: '正在生成闯关题目...', key: 'checkpoint' }) const result = await examAPI.generatePaper(params) currentCheckpointQuestions.value = (result.questions || []).map((q, index) => ({ id: index + 1, questionId: q.question_id, type: q.question_type, typeLabel: q.question_type_name || q.question_type, difficulty: q.difficulty, score: q.score || 5, stem: q.content?.stem || '', options: q.content?.data?.options || q.content?.options || [], answer: q.content?.answer || '', explanation: q.content?.explanation || '', studentAnswer: null })) currentCheckpoint.value = { ...checkpoint, paperId: result.paper_id, totalScore: result.total_score || 100 } showCheckpointModal.value = true message.destroy('checkpoint') } catch (error) { console.error('闯关题目生成失败:', error) message.error('题目加载失败,请重试') } } ``` - [ ] **Step 6: 添加 currentCheckpointQuestions ref** 在 script 区的 ref 声明区添加: ```javascript const currentCheckpointQuestions = ref([]) ``` --- ### Task 5: 互动训练 - 闯关弹窗改造 **Files:** - Modify: `src/components/ExamModule.vue` - [ ] **Step 1: 替换闯关弹窗模板** 找到 `showCheckpointModal` 对应弹窗(约1020-1048行),替换为: ```html

{{ currentCheckpoint.name }}

{{ q.typeLabel }} 第 {{ index + 1 }} 题({{ q.score }} 分)
{{ q.stem }}
正在加载题目...
``` - [ ] **Step 2: 添加闯关辅助函数和 handleSubmitCheckpoint** 在 script 区添加: ```javascript const allCheckpointAnswered = computed(() => { return currentCheckpointQuestions.value.length > 0 && currentCheckpointQuestions.value.every(q => q.studentAnswer != null && q.studentAnswer !== '') }) const isMultiCheckpointSelected = (q, key) => { if (!Array.isArray(q.studentAnswer)) { q.studentAnswer = [] } return q.studentAnswer.includes(key) } const toggleMultiCheckpointAnswer = (q, key) => { if (!Array.isArray(q.studentAnswer)) { q.studentAnswer = [] } const idx = q.studentAnswer.indexOf(key) if (idx >= 0) { q.studentAnswer.splice(idx, 1) } else { q.studentAnswer.push(key) } } const handleSubmitCheckpoint = async () => { if (!allCheckpointAnswered.value) return isGrading.value = true try { const answers = currentCheckpointQuestions.value.map(q => ({ question_id: q.questionId || `q_${q.id}`, question_type: q.type, question_content: { stem: q.stem, data: q.options && q.options.length > 0 ? { options: q.options } : undefined, answer: q.answer }, student_answer: q.type === 'multiple_choice' ? (q.studentAnswer || []).join(',') : q.studentAnswer, max_score: q.score || 5 })) const result = await examAPI.gradeAnswers({ request_id: `cp_${Date.now()}`, answers }) const totalScore = result.total_score || 0 const totalMax = result.total_max_score || 0 // 更新关卡状态 const checkpoint = trainingData.value.checkpoints.find(c => c.id === currentCheckpoint.value?.id) if (checkpoint) { checkpoint.status = 'completed' checkpoint.score = totalScore } // 缓存错题 if (result.results) { result.results.forEach((item, idx) => { if (item.correct === false) { const q = currentCheckpointQuestions.value[idx] if (q) { const existing = trainingData.value.wrongQuestions.find(w => w.questionId === q.questionId) if (!existing) { trainingData.value.wrongQuestions.push({ id: Date.now() + idx, questionId: q.questionId, content: q.stem, type: q.type, typeLabel: q.typeLabel, options: q.options, correctAnswer: q.answer, studentAnswer: q.studentAnswer, score: item.score || 0, maxScore: item.max_score || q.score || 5, feedback: item.feedback || '', doc: currentCheckpoint.value?.name || '', times: 1, answeredAt: new Date().toISOString() }) } } } }) } gradeResult.value = { ...result, checkpointName: currentCheckpoint.value?.name } showAnswerModal.value = true message.success(`闯关完成!得分: ${totalScore}/${totalMax}`) } catch (error) { console.error('闯关提交失败:', error) message.error('提交失败: ' + (error.message || '请重试')) } finally { isGrading.value = false } } ``` --- ### Task 6: 互动训练 - 错题本对接 **Files:** - Modify: `src/components/ExamModule.vue` - [ ] **Step 1: 替换错题本模板** 找到错题本面板模板(约770-810行),替换为: ```html

📚 我的错题本

共 {{ trainingData.value.wrongQuestions.length }} 道错题
{{ item.type === 'single_choice' ? '单选题' : item.type === 'multiple_choice' ? '多选题' : item.type === 'true_false' ? '判断题' : item.type === 'subjective' ? '简答题' : item.type }}
{{ item.content }}
关联关卡: {{ item.doc }}
错误次数: {{ item.times }} 次
批改反馈: {{ item.feedback }}
🎉
太棒了!目前没有错题
继续保持,加油学习!
``` - [ ] **Step 2: 添加 handleRetryWrongQuestion 函数** 在 script 区添加: ```javascript const handleRetryWrongQuestion = (item) => { // 将该错题的对应关卡设置为 in-progress 状态 const checkpoint = trainingData.value.checkpoints.find(c => c.name === item.doc) if (checkpoint) { checkpoint.status = 'in-progress' checkpoint.score = 0 message.success(`已重置关卡「${checkpoint.name}」,前往闯关模式重新挑战`) trainTab.value = 'checkpoint' } else { message.info('请前往闯关模式重新挑战') trainTab.value = 'checkpoint' } } ``` --- ### Task 7: 更新每日一练提交后的错题缓存 **Files:** - Modify: `src/components/ExamModule.vue` - [ ] **Step 1: 在 handleSubmitDailyExercise 成功后添加错题缓存** 找到 `handleSubmitDailyExercise` 函数中的 `gradeResult.value = result` 行(约2441行),在其后添加: ```javascript // 缓存错题 if (result.results) { result.results.forEach((item, idx) => { if (item.correct === false) { const q = generatedQuestions.value[idx] if (q) { const existing = trainingData.value.wrongQuestions.find(w => w.questionId === `q_${q.id}`) if (!existing) { trainingData.value.wrongQuestions.push({ id: Date.now() + idx, questionId: `q_${q.id}`, content: q.stem, type: q.type, typeLabel: q.typeLabel, options: q.options, correctAnswer: q.answer, studentAnswer: q.studentAnswer, score: item.score || 0, maxScore: item.max_score || q.maxScore || 5, feedback: item.feedback || '', doc: '每日一练', times: 1, answeredAt: new Date().toISOString() }) } } } }) } ``` --- ### Task 8: 更新互动训练统计展示 **Files:** - Modify: `src/components/ExamModule.vue` - [ ] **Step 1: 替换模板中的 trainingData 属性引用** 找到模板中所有剩余的 `trainingData.xxx` 引用(约618-634行,训练统计部分),替换为通过 `trainingData.value` 访问的等价表达式: ```html
{{ completedCheckpointCount }}
...
{{ trainingData.value.dailyExercises.length || generatedQuestions.length }}
...
{{ trainingData.value.wrongQuestions.length }}
``` - [ ] **Step 2: 添加 dailyExerciseCount 计算属性 ```javascript const dailyExerciseCount = computed(() => { return trainingData.value.dailyExercises.length || generatedQuestions.value.length }) ``` 然后模板中用 `{{ dailyExerciseCount }}` 替换 `{{ trainingData.value.dailyExercises.length || generatedQuestions.length }}`。 --- ### Task 9: 错题本查看解析弹窗改造 **Files:** - Modify: `src/components/ExamModule.vue` - [ ] **Step 1: 替换查看解析弹窗** 找到 `showAnalysisModal` 对应弹窗(约1116-1145行),替换为: ```html

{{ currentWrongQuestion.content }}

题目选项:
{{ opt.key }}. {{ opt.content }}
你的答案:

{{ formatAnswer(currentWrongQuestion.studentAnswer) }}

正确答案:

{{ formatAnswer(currentWrongQuestion.correctAnswer) }}

AI评语:

{{ currentWrongQuestion.feedback }}

``` - [ ] **Step 2: 添加 formatAnswer 工具函数 ```javascript const formatAnswer = (answer) => { if (answer === null || answer === undefined) return '未作答' if (Array.isArray(answer)) return answer.join(', ') return String(answer) } ```