26 KiB
考试管理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 之后添加:
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 之后添加:
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行),将其整体替换为:
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) 所在区块,在其附近添加:
const isGeneratingPaper = ref(false)
Task 3: 智能组卷 - 更新查看试卷弹窗
Files:
-
Modify:
src/components/ExamModule.vue -
Step 1: 更新查看试卷弹窗以展示真实题目
找到 showViewPaperModal 对应弹窗的模板(约964-1018行),将弹窗内容替换为:
<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行),替换为:
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行):
<div class="stat-value">{{ trainingData.checkpoints.filter(c => c.status === 'completed').length }}</div>
改为:
<div class="stat-value">{{ completedCheckpointCount }}</div>
- Step 3: 添加计算属性
在 script 区添加计算属性以适配模板中的统计展示:
const completedCheckpointCount = computed(() => {
return trainingData.value.checkpoints.filter(c => c.status === 'completed').length
})
- Step 4: 替换闯关模式模板的关卡列表渲染
找到闯关模式面板的模板(约659-693行),替换为:
<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行),替换为:
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 声明区添加:
const currentCheckpointQuestions = ref([])
Task 5: 互动训练 - 闯关弹窗改造
Files:
-
Modify:
src/components/ExamModule.vue -
Step 1: 替换闯关弹窗模板
找到 showCheckpointModal 对应弹窗(约1020-1048行),替换为:
<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 区添加:
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行),替换为:
<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 区添加:
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行),在其后添加:
// 缓存错题
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 访问的等价表达式:
<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 计算属性
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行),替换为:
<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 工具函数
const formatAnswer = (answer) => {
if (answer === null || answer === undefined) return '未作答'
if (Array.isArray(answer)) return answer.join(', ')
return String(answer)
}