Files
aue/docs/superpowers/plans/2026-05-10-exam-api-integration.md
2026-06-03 13:16:30 +08:00

785 lines
26 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 考试管理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
<a-modal
title="查看试卷"
v-model:open="showViewPaperModal"
@cancel="showViewPaperModal = false"
width="800px"
:footer="null"
>
<div v-if="currentPaper">
<a-descriptions bordered :column="1" size="middle">
<a-descriptions-item label="试卷名称">
{{ currentPaper.name }}
</a-descriptions-item>
<a-descriptions-item label="关联制度">
{{ currentPaper.doc }}
</a-descriptions-item>
<a-descriptions-item label="题目数量">
{{ currentPaper.questions }} 题
</a-descriptions-item>
<a-descriptions-item label="总分">
{{ currentPaper.totalScore || 100 }} 分
</a-descriptions-item>
<a-descriptions-item label="考试时长">
{{ currentPaper.duration }} 分钟
</a-descriptions-item>
<a-descriptions-item label="试卷状态">
<a-badge :status="currentPaper.status === 'published' ? 'success' : 'warning'" :text="currentPaper.status === 'published' ? '已发布' : '草稿'" />
</a-descriptions-item>
</a-descriptions>
<div style="margin-top: 24px;">
<h4>试卷题目预览</h4>
<div v-if="currentPaper.questionList && currentPaper.questionList.length > 0">
<div v-for="(q, idx) in currentPaper.questionList" :key="idx" class="preview-question-item" style="margin-top: 16px; padding: 16px; background-color: #fafafa; border-radius: 8px; border: 1px solid #e8e8e8;">
<div style="margin-bottom: 8px;">
<span class="type-badge">{{ q.typeLabel || q.type }}</span>
<strong style="margin-left: 8px;">第 {{ idx + 1 }} 题({{ q.score || 5 }} 分)</strong>
</div>
<div style="margin-bottom: 8px;">{{ q.stem }}</div>
<div v-if="q.options && q.options.length > 0" style="margin-left: 16px;">
<div v-for="opt in q.options" :key="opt.key" style="margin-bottom: 4px;">
{{ opt.key }}. {{ opt.content }}
</div>
</div>
</div>
</div>
<div v-else style="text-align: center; padding: 40px; color: #999;">
暂无题目预览
</div>
</div>
</div>
</a-modal>
```
---
### 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
<div class="stat-value">{{ trainingData.checkpoints.filter(c => c.status === 'completed').length }}</div>
```
改为:
```html
<div class="stat-value">{{ completedCheckpointCount }}</div>
```
- [ ] **Step 3: 添加计算属性**
在 script 区添加计算属性以适配模板中的统计展示:
```javascript
const completedCheckpointCount = computed(() => {
return trainingData.value.checkpoints.filter(c => c.status === 'completed').length
})
```
- [ ] **Step 4: 替换闯关模式模板的关卡列表渲染**
找到闯关模式面板的模板约659-693行替换为
```html
<div v-if="trainTab === 'checkpoint'" class="exam-card">
<div class="card-header">
<h2>🗺️ 闯关地图</h2>
<span class="status-badge status-info">共 {{ trainingData.value.checkpoints.length }} 关</span>
</div>
<div class="checkpoint-map">
<div
v-for="(checkpoint, index) in trainingData.value.checkpoints"
:key="checkpoint.id"
:class="['checkpoint-item', checkpoint.status]"
>
<div class="checkpoint-node" @click="handleStartCheckpoint(checkpoint)">
<div class="node-circle">
{{ checkpoint.id }}
</div>
<div class="node-info">
<div class="node-name">{{ checkpoint.name }}</div>
<div :class="['node-status', checkpoint.status]">
{{ checkpoint.status === 'completed' ? `✅ 得分:${checkpoint.score}分` :
checkpoint.status === 'in-progress' ? '🔄 进行中...' : '⏳ 未开始' }}
</div>
</div>
<button
:class="['btn', 'btn-primary']"
@click.stop="handleStartCheckpoint(checkpoint)"
>
{{ checkpoint.status === 'completed' ? '重新挑战' : checkpoint.status === 'in-progress' ? '继续挑战' : '🔒 开始挑战' }}
</button>
</div>
<div v-if="index < trainingData.value.checkpoints.length - 1" class="checkpoint-line"></div>
</div>
</div>
</div>
```
- [ ] **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
<a-modal
title="闯关挑战"
v-model:open="showCheckpointModal"
@cancel="showCheckpointModal = false"
width="700px"
>
<template #footer>
<a-button @click="showCheckpointModal = false">退出</a-button>
<a-button type="primary" @click="handleSubmitCheckpoint" :loading="isGrading" :disabled="!allCheckpointAnswered">
{{ isGrading ? '批改中...' : '提交答案' }}
</a-button>
</template>
<div v-if="currentCheckpoint && currentCheckpointQuestions.length > 0">
<h3 style="margin-bottom: 16px; color: #1890ff;">{{ currentCheckpoint.name }}</h3>
<div v-for="(q, index) in currentCheckpointQuestions" :key="q.id" style="margin-bottom: 20px; padding: 16px; background: #fafafa; border-radius: 8px; border: 1px solid #e8e8e8;">
<div class="question-header" style="margin-bottom: 8px;">
<span :class="['type-badge', q.type]">{{ q.typeLabel }}</span>
<span class="question-number" style="margin-left: 8px;">第 {{ index + 1 }} 题({{ q.score }} 分)</span>
</div>
<div class="question-content" style="margin-bottom: 12px;">{{ q.stem }}</div>
<div v-if="q.type === 'single_choice' && q.options?.length" class="question-options answer-options">
<label v-for="opt in q.options" :key="opt.key" class="option-radio" :class="{ selected: q.studentAnswer === opt.key }" style="display: block; margin-bottom: 6px;">
<input type="radio" :name="'cp_' + q.id" :value="opt.key" v-model="q.studentAnswer" />
<span class="option-letter">{{ opt.key }}.</span>
<span class="option-text">{{ opt.content }}</span>
</label>
</div>
<div v-if="q.type === 'multiple_choice' && q.options?.length" class="question-options answer-options">
<label v-for="opt in q.options" :key="opt.key" class="option-checkbox" :class="{ selected: isMultiCheckpointSelected(q, opt.key) }" style="display: block; margin-bottom: 6px;">
<input type="checkbox" :value="opt.key" @change="toggleMultiCheckpointAnswer(q, opt.key)" :checked="isMultiCheckpointSelected(q, opt.key)" />
<span class="option-letter">{{ opt.key }}.</span>
<span class="option-text">{{ opt.content }}</span>
</label>
</div>
<div v-if="q.type === 'true_false'" class="question-options answer-options">
<label class="option-radio" :class="{ selected: q.studentAnswer === 'true' }" style="display: inline-block; margin-right: 16px;">
<input type="radio" :name="'cp_' + q.id" value="true" v-model="q.studentAnswer" />
<span class="option-letter">A.</span>
<span class="option-text">正确</span>
</label>
<label class="option-radio" :class="{ selected: q.studentAnswer === 'false' }" style="display: inline-block;">
<input type="radio" :name="'cp_' + q.id" value="false" v-model="q.studentAnswer" />
<span class="option-letter">B.</span>
<span class="option-text">错误</span>
</label>
</div>
<div v-if="q.type === 'subjective'" class="subjective-answer">
<label class="form-label">请作答:</label>
<textarea v-model="q.studentAnswer" class="form-input essay-textarea" rows="3" placeholder="请输入您的答案..."></textarea>
</div>
</div>
</div>
<div v-else style="text-align: center; padding: 40px; color: #999;">
正在加载题目...
</div>
</a-modal>
```
- [ ] **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
<div v-if="trainTab === 'wrong'" class="exam-card">
<div class="card-header">
<h2>📚 我的错题本</h2>
<span class="status-badge status-warning">共 {{ trainingData.value.wrongQuestions.length }} 道错题</span>
</div>
<div class="questions-list" v-if="trainingData.value.wrongQuestions.length > 0">
<div v-for="item in trainingData.value.wrongQuestions" :key="item.id" class="question-item wrong-item">
<div class="question-header">
<span :class="['type-badge', item.type]">
{{ item.type === 'single_choice' ? '单选题' : item.type === 'multiple_choice' ? '多选题' : item.type === 'true_false' ? '判断题' : item.type === 'subjective' ? '简答题' : item.type }}
</span>
<div class="question-actions">
<button class="btn btn-primary btn-xs" @click="handleRetryWrongQuestion(item)">重练</button>
<button class="btn btn-success btn-xs" @click="handleViewWrongQuestion(item)">查看解析</button>
</div>
</div>
<div class="question-content">{{ item.content }}</div>
<div class="wrong-meta">
<div class="meta-item">
<span class="meta-label">关联关卡:</span>
<span class="meta-value">{{ item.doc }}</span>
</div>
<div class="meta-item">
<span class="meta-label">错误次数:</span>
<span class="meta-value error-count">{{ item.times }} 次</span>
</div>
<div v-if="item.feedback" class="meta-item">
<span class="meta-label">批改反馈:</span>
<span class="meta-value">{{ item.feedback }}</span>
</div>
</div>
</div>
</div>
<div v-else class="empty-state">
<div class="empty-icon">🎉</div>
<div class="empty-text">太棒了!目前没有错题</div>
<div class="empty-hint">继续保持,加油学习!</div>
</div>
</div>
```
- [ ] **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
<div class="stat-value">{{ completedCheckpointCount }}</div>
...
<div class="stat-value">{{ trainingData.value.dailyExercises.length || generatedQuestions.length }}</div>
...
<div class="stat-value">{{ trainingData.value.wrongQuestions.length }}</div>
```
- [ ] **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
<a-modal
title="查看解析"
v-model:open="showAnalysisModal"
@cancel="showAnalysisModal = false"
width="700px"
>
<template #footer>
<a-button @click="showAnalysisModal = false">关闭</a-button>
</template>
<div v-if="currentWrongQuestion">
<h4 style="margin-bottom: 16px;">{{ currentWrongQuestion.content }}</h4>
<div v-if="currentWrongQuestion.options && currentWrongQuestion.options.length > 0" style="margin-bottom: 16px;">
<h6>题目选项:</h6>
<div v-for="opt in currentWrongQuestion.options" :key="opt.key" style="margin-bottom: 4px;">
{{ opt.key }}. {{ opt.content }}
</div>
</div>
<div style="margin-bottom: 16px;">
<h6>你的答案:</h6>
<p style="color: #ff4d4f;">{{ formatAnswer(currentWrongQuestion.studentAnswer) }}</p>
</div>
<div style="margin-bottom: 16px;">
<h6>正确答案:</h6>
<p style="color: #52c41a;">{{ formatAnswer(currentWrongQuestion.correctAnswer) }}</p>
</div>
<div v-if="currentWrongQuestion.feedback" style="margin-bottom: 16px;">
<h6>AI评语</h6>
<p>{{ currentWrongQuestion.feedback }}</p>
</div>
</div>
</a-modal>
```
- [ ] **Step 2: 添加 formatAnswer 工具函数
```javascript
const formatAnswer = (answer) => {
if (answer === null || answer === undefined) return '未作答'
if (Array.isArray(answer)) return answer.join(', ')
return String(answer)
}
```