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

26 KiB
Raw Permalink Blame History

考试管理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.jsexamAPI 对象末尾、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)
}