Files
012-kaopeilian/frontend/src/views/exam/practice.vue
yuliang_guo ccf6af6a4a
Some checks failed
continuous-integration/drone/push Build is failing
style(exam): 优化多选题正确答案显示为换行格式
2026-01-28 14:32:36 +08:00

1939 lines
56 KiB
Vue
Raw 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.
<template>
<div class="practice-container">
<!-- 加载状态 -->
<div v-if="isGenerating" class="loading-container">
<el-icon class="is-loading" :size="48"><Loading /></el-icon>
<h3>试题动态生成中...</h3>
<p>正在根据课程知识点智能生成考试试题请稍候</p>
<p class="time-estimate">大约需要 13 分钟</p>
</div>
<!-- 考试界面 -->
<div v-else-if="currentQuestion" class="practice-content">
<div class="practice-header">
<div class="header-info">
<h2>动态考试</h2>
<div class="progress-info">
<span>{{ currentRound }} - {{ currentRound === 1 ? '正式考试' : '错题重考' }}</span>
<span class="separator">|</span>
<span> {{ currentQuestionIndex + 1 }} / {{ totalQuestions }} </span>
<span class="separator">|</span>
<span>正确{{ correctCount }}</span>
<span class="separator">|</span>
<span>错误{{ wrongCount }}</span>
</div>
</div>
<el-button type="danger" size="small" @click="exitPractice">
退出考试
</el-button>
</div>
<div class="practice-main">
<!-- 题目内容 -->
<div class="question-section card">
<div class="question-type">
<el-tag :type="getQuestionTypeTag(currentQuestion.type)">
{{ getQuestionTypeText(currentQuestion.type) }}
</el-tag>
<span class="question-score">{{ currentQuestion.score }} </span>
</div>
<div class="question-content">
<h3>{{ currentQuestionIndex + 1 }}. {{ currentQuestion.title }}</h3>
<!-- 单选题/多选题 -->
<div v-if="['single', 'multiple'].includes(currentQuestion.type)" class="options-list">
<div
v-for="(option, index) in currentQuestion.options"
:key="index"
class="option-item"
:class="{
selected: isOptionSelected(index),
correct: showAnswer && option.isCorrect,
wrong: showAnswer && isOptionSelected(index) && !option.isCorrect
}"
@click="selectOption(index)"
>
<span class="option-label">{{ String.fromCharCode(65 + index) }}</span>
<span class="option-content">{{ option.content }}</span>
<el-icon v-if="showAnswer && option.isCorrect" class="result-icon correct">
<CircleCheck />
</el-icon>
<el-icon v-else-if="showAnswer && isOptionSelected(index) && !option.isCorrect" class="result-icon wrong">
<CircleClose />
</el-icon>
</div>
</div>
<!-- 判断题 -->
<div v-else-if="currentQuestion.type === 'judge'" class="judge-options">
<div class="judge-cards">
<div
class="judge-card"
:class="{
active: userAnswer.judge === true,
disabled: showAnswer,
correct: showAnswer && currentQuestion.correctAnswer === true,
wrong: showAnswer && userAnswer.judge === true && currentQuestion.correctAnswer !== true
}"
@click="!showAnswer && selectJudge(true)"
>
<div class="judge-icon correct-icon">
<el-icon :size="32"><CircleCheck /></el-icon>
</div>
<span class="judge-text">正确</span>
</div>
<div
class="judge-card"
:class="{
active: userAnswer.judge === false,
disabled: showAnswer,
correct: showAnswer && currentQuestion.correctAnswer === false,
wrong: showAnswer && userAnswer.judge === false && currentQuestion.correctAnswer !== false
}"
@click="!showAnswer && selectJudge(false)"
>
<div class="judge-icon wrong-icon">
<el-icon :size="32"><CircleClose /></el-icon>
</div>
<span class="judge-text">错误</span>
</div>
</div>
</div>
<!-- 填空题 -->
<div v-else-if="currentQuestion.type === 'blank'" class="blank-answer">
<div class="blank-input-wrapper">
<el-input
v-model="userAnswer.blank"
placeholder="请在此输入你的答案..."
:disabled="showAnswer"
@keyup.enter="handleBlankSubmit"
size="large"
clearable
>
<template #prefix>
<el-icon><Edit /></el-icon>
</template>
</el-input>
<p class="blank-hint"> Enter 键提交答案</p>
</div>
</div>
<!-- 问答题 -->
<div v-else-if="currentQuestion.type === 'essay'" class="essay-answer">
<el-input
v-model="userAnswer.essay"
type="textarea"
:rows="6"
placeholder="请输入答案"
:disabled="showAnswer"
/>
</div>
</div>
<!-- 答案解析 -->
<div v-if="showAnswer" class="answer-section">
<el-divider />
<!-- 正确答案的提示 -->
<div v-if="isAnswerCorrect" class="correct-feedback">
<div class="feedback-icon">
<el-icon :size="32" color="#67c23a"><CircleCheck /></el-icon>
</div>
<div class="feedback-text">
<h3>回答正确</h3>
<p>正在进入下一题...</p>
</div>
</div>
<!-- 错误答案的解析 -->
<div v-else>
<div class="answer-title">答案解析</div>
<div class="correct-answer markdown-content">
<strong>正确答案</strong>
<div v-html="renderMarkdown(formatCorrectAnswer(currentQuestion))"></div>
</div>
<div class="answer-explanation">
<strong>解析</strong>
<div class="markdown-content" v-html="renderMarkdown(currentQuestion.explanation)"></div>
</div>
</div>
</div>
</div>
<!-- 多选题判断题填空题和问答题的提交按钮 -->
<div v-if="(['multiple', 'judge', 'blank', 'essay'].includes(currentQuestion.type)) && !showAnswer" class="submit-section">
<el-button
type="primary"
@click="submitAnswer"
:disabled="!hasAnswer"
size="large"
>
{{ currentQuestion.type === 'multiple' ? '确认选择' : '提交答案' }}
</el-button>
</div>
</div>
</div>
<!-- 无题目状态 -->
<div v-else class="no-questions">
<el-empty description="暂无题目" />
</div>
<!-- 答案解析弹窗 -->
<el-dialog
v-model="answerDialogVisible"
title="答案解析"
width="600px"
:close-on-click-modal="false"
:close-on-press-escape="false"
:show-close="false"
class="answer-dialog"
>
<div v-if="dialogQuestion" class="answer-dialog-content">
<div class="result-section">
<div class="result-icon-large wrong">
<el-icon :size="40"><CircleClose /></el-icon>
</div>
<h3 class="result-text">回答错误</h3>
</div>
<div class="dialog-scroll-area">
<div class="question-review">
<h4>题目</h4>
<p>{{ dialogQuestion.title }}</p>
</div>
<div class="answer-review">
<h4>正确答案</h4>
<div class="correct-answer-text markdown-content" v-html="renderMarkdown(formatCorrectAnswer(dialogQuestion))"></div>
</div>
<div class="explanation-review">
<h4>解析</h4>
<div class="explanation-text markdown-content" v-html="renderMarkdown(dialogQuestion.explanation)"></div>
</div>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="handleRemembered" size="large">
记住了
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox, ElDialog, ElLoading } from 'element-plus'
import { Loading, CircleCheck, CircleClose, Edit } from '@element-plus/icons-vue'
import { courseApi } from '@/api/course'
import { generateExam, judgeAnswer, recordMistake, getMistakes, updateRoundScore, type MistakeRecordItem } from '@/api/exam'
import { marked } from 'marked'
import DOMPurify from 'dompurify'
// 路由相关
const route = useRoute()
const router = useRouter()
// 课程和考试ID
const courseId = ref<number>(0)
const currentExamId = ref<number>(0) // 当前考试ID(三轮考试共用一个exam记录)
// 考试设置
const examSettings = ref({
single_choice_count: 4,
multiple_choice_count: 2,
true_false_count: 1,
fill_blank_count: 2,
essay_count: 1,
difficulty_level: 3,
duration_minutes: 10,
passing_score: 60
})
// 考试状态
const currentIndex = ref(0)
const showAnswer = ref(false)
const correctCount = ref(0)
const wrongCount = ref(0)
// const isLoading = ref(false) // Unused
const isGenerating = ref(true) // 是否正在生成试题
// 多轮考试相关
const currentRound = ref(1) // 当前轮次
const maxRounds = ref(3) // 最大轮次(可配置)
const roundScores = ref<number[]>([]) // 每轮得分
const wrongQuestions = ref<any[]>([]) // 错题集合
const currentWrongIndex = ref(0) // 当前错题索引
const isWrongQuestionMode = ref(false) // 是否为错题模式
// 答题进度保存相关
const PRACTICE_STORAGE_KEY = 'exam_practice_progress'
const userAnswers = ref<any[]>([]) // 用户答题记录
// 弹窗状态
const answerDialogVisible = ref(false)
const isAnswerCorrect = ref(false)
const dialogQuestion = ref<any>(null) // 弹窗要显示的题目保存快照避免currentQuestion变化
// 用户答案
const userAnswer = reactive({
selected: [] as number[],
judge: null as boolean | null,
blank: '',
essay: '' // 新增问答题答案
})
// 试题数据
const questions = ref<any[]>([])
// 当前题目
const currentQuestion = computed(() => {
if (!questions.value || questions.value.length === 0) {
return null
}
return questions.value[currentIndex.value]
})
const totalQuestions = computed(() => {
return questions.value?.length || 0
})
const currentQuestionIndex = computed(() => {
return currentIndex.value
})
// 是否已答题
const hasAnswer = computed(() => {
const q = currentQuestion.value
if (!q) return false
if (q.type === 'single' || q.type === 'multiple') {
return userAnswer.selected.length > 0
} else if (q.type === 'judge') {
return userAnswer.judge !== null
} else if (q.type === 'blank') {
return userAnswer.blank.trim() !== ''
} else if (q.type === 'essay') {
return userAnswer.essay.trim() !== ''
}
return false
})
/**
* 退出考试
*/
const exitPractice = async () => {
try {
await ElMessageBox.confirm('确定要退出考试吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
// 可以跳转到其他页面或返回上一页
ElMessage.success('已退出考试')
} catch {}
}
/**
* 重置考试
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
/**
* 清空答案
*/
const clearAnswer = () => {
userAnswer.selected = []
userAnswer.judge = null
userAnswer.blank = ''
userAnswer.essay = ''
}
/**
* 数据格式转换Dify格式转前端格式
*/
const transformDifyQuestions = (difyQuestions: any[]): any[] => {
return difyQuestions.map((q, index) => {
const typeMap: Record<string, string> = {
'single_choice': 'single',
'multiple_choice': 'multiple',
'true_false': 'judge',
'fill_blank': 'blank',
'essay': 'essay'
}
const transformed: any = {
id: index + 1,
type: typeMap[q.type] || q.type,
title: q.topic?.title || q.topic || '',
score: 10 / difyQuestions.length, // 平均分配分值,总分10分
explanation: q.analysis || '',
knowledge_point_id: q.knowledge_point_id ? parseInt(q.knowledge_point_id) : null
}
// 处理选择题选项
if (q.type === 'single_choice' || q.type === 'multiple_choice') {
const options = q.topic?.options || {}
// 解析正确答案:支持 "A"、"A,B"、"A、B"、"Axxx" 等多种格式
const correctAnswerStr = String(q.correct || '').trim()
console.log(`📝 开始解析题目[${index + 1}]`)
console.log(` 原始correct: "${correctAnswerStr}"`)
console.log(` 原始options:`, options)
// 更精确地提取正确答案字母(避免误提取选项内容中的字母)
let correctLetters: string[] = []
// 情况1纯选项字母格式如 "A", "B", "A,B", "A、B", "A,B,C,D,E"
const pureLetterMatch = correctAnswerStr.match(/^[A-Za-z]([,、\s]+[A-Za-z])*$/)
if (pureLetterMatch) {
correctLetters = correctAnswerStr.match(/[A-Za-z]/g)?.map(l => l.toUpperCase()) || []
console.log(` ✓ 匹配情况1(纯字母): correctLetters=[${correctLetters.join(',')}]`)
} else {
// 情况2多选项带内容格式如 "A分组列,B聚合函数,C常量"
// 匹配所有 "X" 或 "X:" 格式,提取字母
const multiOptionMatch = correctAnswerStr.match(/[A-Za-z](?=[:])/g)
if (multiOptionMatch && multiOptionMatch.length > 0) {
correctLetters = multiOptionMatch.map(l => l.toUpperCase())
console.log(` ✓ 匹配情况2(多选项带内容): correctLetters=[${correctLetters.join(',')}]`)
} else {
// 情况3只有开头字母如 "A" 后面直接跟非字母内容)
const firstLetterMatch = correctAnswerStr.match(/^([A-Za-z])(?![A-Za-z])/)
if (firstLetterMatch) {
correctLetters = [firstLetterMatch[1].toUpperCase()]
console.log(` ✓ 匹配情况3(开头字母): correctLetters=[${correctLetters.join(',')}]`)
} else {
// 情况4无法解析尝试从选项中查找匹配的答案
console.log(` ⚠️ 无法从correct字符串解析字母尝试内容匹配`)
// 尝试在选项中查找完全匹配的内容
const optionValues = Object.values(options)
optionValues.forEach((optVal: any, idx: number) => {
const optContent = String(optVal || '')
if (optContent === correctAnswerStr || optContent.includes(correctAnswerStr)) {
const letter = String.fromCharCode(65 + idx)
correctLetters.push(letter)
console.log(` → 内容匹配: 选项${letter}="${optContent.substring(0, 30)}..."`)
}
})
}
}
}
// 确保至少有一个正确答案
if (correctLetters.length === 0) {
console.warn(` ❌ 警告:未能解析出正确答案字母,默认使用第一个选项`)
correctLetters = ['A']
}
console.log(`🔍 解析题目[${index + 1}] - correct: "${q.correct}", correctLetters: [${correctLetters.join(', ')}]`)
// 获取所有选项的键并排序,确保顺序一致
const optionKeys = Object.keys(options).sort((a, b) => {
// 如果是数字键,按数字排序
const numA = parseInt(a)
const numB = parseInt(b)
if (!isNaN(numA) && !isNaN(numB)) return numA - numB
// 如果是字母键,按字母排序
return a.localeCompare(b)
})
console.log(` 选项键排序后: [${optionKeys.join(', ')}]`)
transformed.options = optionKeys.map((key, idx) => {
const opt = options[key]
const optStr = String(opt || '')
// 提取选项内容(去掉 "A" 或 "A:" 前缀)
let content = optStr
// 匹配 "Axxx" 或 "A:xxx" 格式支持中英文冒号支持A-Z所有字母
const contentPrefixMatch = optStr.match(/^[A-Za-z]\s*[:]\s*(.*)$/)
if (contentPrefixMatch) {
content = contentPrefixMatch[1].trim()
}
// 提取当前选项的字母
// 优先级1. 选项内容前缀 > 2. key字母 > 3. 索引推断
let optionLetter = ''
let letterSource = ''
// 1. 尝试从选项内容前缀提取(如 "Axxx" 或 "A:xxx"
const prefixMatch = optStr.match(/^([A-Za-z])\s*[:]/)
if (prefixMatch) {
optionLetter = prefixMatch[1].toUpperCase()
letterSource = '内容前缀'
}
// 2. 尝试从 key 提取(如果 key 是单个字母 A-Z
else if (key.match(/^[A-Za-z]$/)) {
optionLetter = key.toUpperCase()
letterSource = 'key字母'
}
// 3. 根据索引推断选项字母0->A, 1->B, 2->C, 3->D
else {
optionLetter = String.fromCharCode(65 + idx) // 65 = 'A'
letterSource = '索引推断'
}
// 判断当前选项是否正确:检查选项字母是否在正确答案列表中
const isCorrect = correctLetters.includes(optionLetter)
console.log(` 选项[${key}/${idx}]: letter=${optionLetter}(${letterSource}), isCorrect=${isCorrect}, correctLetters=[${correctLetters.join(',')}], content="${content.substring(0, 25)}..."`)
return { content, isCorrect, optionLetter }
})
// 同时保存原始的correct字段用于显示
transformed.correctAnswer = q.correct
}
// 处理判断题
if (q.type === 'true_false') {
// 保存原始的correct字符串用于显示
transformed.correctAnswerText = q.correct // 用于显示:"正确"或"错误"
// 保存boolean值用于判断
transformed.correctAnswer = q.correct === '正确' || q.correct === 'true' || q.correct === true
}
// 处理填空题和问答题
if (q.type === 'fill_blank' || q.type === 'essay') {
transformed.correctAnswer = q.correct
}
// 确保explanation字段存在
if (!transformed.explanation && q.analysis) {
transformed.explanation = q.analysis
}
return transformed
})
}
/**
* 初始化考试
*/
const initExam = async () => {
try {
// 获取courseId
courseId.value = Number(route.query.courseId)
if (!courseId.value) {
ElMessage.error('缺少课程ID参数')
router.back()
return
}
// 获取考试设置
const settingsRes = await courseApi.getExamSettings(courseId.value)
console.log('📊 获取考试设置响应:', settingsRes)
if (settingsRes.code === 200 && settingsRes.data) {
const settings = settingsRes.data
examSettings.value = {
single_choice_count: settings.single_choice_count || 4,
multiple_choice_count: settings.multiple_choice_count || 2,
true_false_count: settings.true_false_count || 1,
fill_blank_count: settings.fill_blank_count || 2,
essay_count: settings.essay_count || 1,
difficulty_level: settings.difficulty_level || 3,
duration_minutes: settings.duration_minutes || 10,
passing_score: settings.passing_score || 60
}
console.log('📊 应用的考试设置:', examSettings.value)
} else {
console.warn('⚠️ 未获取到考试设置,使用默认值:', examSettings.value)
}
// 生成第一轮考试试题
await loadQuestions()
} catch (error: any) {
console.error('初始化考试失败:', error)
ElMessage.error(error.message || '初始化考试失败')
isGenerating.value = false
}
}
/**
* 加载试题
*/
const loadQuestions = async () => {
try {
isGenerating.value = true
const requestData: any = {
course_id: courseId.value,
current_round: currentRound.value, // 新增:当前轮次
exam_id: currentRound.value === 1 ? undefined : currentExamId.value, // 第2、3轮传入exam_id
single_choice_count: examSettings.value.single_choice_count,
multiple_choice_count: examSettings.value.multiple_choice_count,
true_false_count: examSettings.value.true_false_count,
fill_blank_count: examSettings.value.fill_blank_count,
essay_count: examSettings.value.essay_count,
difficulty_level: examSettings.value.difficulty_level
}
// 第一轮考试不传mistake_records参数
// 第二、三轮考试传入上一轮的错题记录
if (currentRound.value > 1) {
// 用同一个exam_id获取上一轮的错题传入round参数只获取上一轮的错题
const previousRound = currentRound.value - 1
console.log(`📋 获取第${previousRound}轮错题记录 - exam_id: ${currentExamId.value}, round: ${previousRound}`)
const mistakesRes: any = await getMistakes(currentExamId.value, previousRound)
console.log('getMistakes原始响应:', mistakesRes)
console.log('getMistakes响应结构:', {
'mistakesRes.code': mistakesRes.code,
'mistakesRes.data': mistakesRes.data,
'mistakesRes.data.mistakes': mistakesRes.data?.mistakes,
'mistakes数量': mistakesRes.data?.mistakes?.length
})
// 响应格式:{code: 200, data: {mistakes: [...]}}
const mistakesData = mistakesRes.data?.mistakes || []
if (mistakesRes.code === 200 && mistakesData.length > 0) {
// 将错题记录转换为JSON字符串
const mistakeRecords = mistakesData.map((m: MistakeRecordItem) => ({
question_id: m.question_id || null,
knowledge_point_id: m.knowledge_point_id || null,
question_content: m.question_content,
correct_answer: m.correct_answer,
user_answer: m.user_answer
}))
requestData.mistake_records = JSON.stringify(mistakeRecords)
console.log(`✅ 错题记录已准备 - 数量: ${mistakeRecords.length}`)
} else {
// 没有错题,结束考试
console.log('⚠️ 没有错题记录,结束考试')
ElMessage.success('恭喜!没有错题,考试完成!')
finishAllRounds()
return
}
}
// 调用Dify生成试题
const response: any = await generateExam(requestData)
// 调试日志:输出完整响应
console.log('=== Dify响应调试 ===')
console.log('response.code:', response.code)
console.log('response.message:', response.message)
console.log('response.data:', response.data)
console.log('response.data.result类型:', typeof response.data?.result)
console.log('response.data.result长度:', response.data?.result?.length)
console.log('response.data.result前100字符:', response.data?.result?.substring(0, 100))
console.log('==================')
if (response.code === 200 && response.data?.result) {
// 解析result字段中的JSON字符串
const difyQuestions = JSON.parse(response.data.result)
// 调试查看Dify返回的原始题目数据
console.log('🔍 Dify返回的原始题目前2题:', difyQuestions.slice(0, 2))
console.log('🔍 第一题的correct字段:', difyQuestions[0]?.correct)
console.log('🔍 第一题的analysis字段:', difyQuestions[0]?.analysis)
// 转换为前端需要的格式
questions.value = transformDifyQuestions(difyQuestions)
// 调试:查看转换后的题目数据
console.log('✅ 转换后的题目前2题:', questions.value.slice(0, 2))
console.log('✅ 第一题的correctAnswer:', questions.value[0]?.correctAnswer)
console.log('✅ 第一题的explanation:', questions.value[0]?.explanation)
isGenerating.value = false
// 第一轮保存exam_id第二三轮复用相同的exam_id
if (currentRound.value === 1) {
currentExamId.value = response.data.exam_id
console.log(`✅ 第1轮考试记录创建成功 - exam_id: ${currentExamId.value}`)
} else {
console.log(`✅ 第${currentRound.value}轮:复用考试记录 - exam_id: ${currentExamId.value}`)
}
ElMessage.success(`${currentRound.value}轮考试试题生成成功,共${questions.value.length}`)
} else {
console.error('❌ 试题生成失败 - response.code:', response.code, 'result为空:', !response.data?.result)
throw new Error(response.message || '试题生成失败')
}
} catch (error: any) {
console.error('加载试题失败:', error)
ElMessage.error(error.message || '加载试题失败,请重试')
isGenerating.value = false
}
}
/**
* 选择选项
*/
const selectOption = (index: number) => {
if (showAnswer.value) return
const q = currentQuestion.value
if (q.type === 'single') {
userAnswer.selected = [index]
// 单选题立即判断答案
checkAndHandleAnswer()
} else if (q.type === 'multiple') {
const idx = userAnswer.selected.indexOf(index)
if (idx > -1) {
userAnswer.selected.splice(idx, 1)
} else {
userAnswer.selected.push(index)
}
// 保存答题进度
saveProgress()
}
}
/**
* 判断选项是否被选中
*/
const isOptionSelected = (index: number) => {
return userAnswer.selected.includes(index)
}
/**
* 检查并处理答案
*/
const checkAndHandleAnswer = async () => {
const q = currentQuestion.value
let isCorrect = false
console.log(`🔍 开始检查答案 - 题型: ${q.type}, 题目: ${q.title.substring(0, 30)}...`)
// 立即保存题目快照防止异步判断期间currentQuestion变化
const questionSnapshot = { ...q }
// 主观题(填空题、问答题)需要调用AI判断
if (q.type === 'blank' || q.type === 'essay') {
isCorrect = await checkSubjectiveAnswer(questionSnapshot) // ← 传递快照
console.log(`📝 主观题AI判断结果: ${isCorrect}`)
} else {
// 客观题(单选、多选、判断)直接判断
isCorrect = checkObjectiveAnswer()
console.log(`📋 客观题本地判断结果: ${isCorrect}`)
}
isAnswerCorrect.value = isCorrect
console.log(`✅ 最终判断: ${isCorrect ? '正确✓' : '错误✗'}`)
if (isCorrect) {
correctCount.value++
// 显示正确提示
showAnswer.value = true
ElMessage.success('回答正确!')
// 延迟进入下一题
setTimeout(() => {
showAnswer.value = false
clearAnswer()
goToNextQuestion()
}, 1500)
} else {
wrongCount.value++
console.log(`❌ 答错了,准备记录错题`)
console.log('题目快照数据:', {
'type': questionSnapshot.type,
'title': questionSnapshot.title.substring(0, 30),
'correctAnswer': questionSnapshot.correctAnswer,
'explanation': questionSnapshot.explanation,
'explanation长度': questionSnapshot.explanation?.length
})
// 使用题目快照而不是q因为q可能已经变化
dialogQuestion.value = questionSnapshot
console.log('保存到dialogQuestion:', {
'correctAnswer': dialogQuestion.value.correctAnswer,
'explanation': dialogQuestion.value.explanation,
'explanation前50字符': dialogQuestion.value.explanation?.substring(0, 50)
})
// 记录错题到数据库
await recordWrongAnswer()
// 答错显示解析弹窗
showAnswer.value = true
answerDialogVisible.value = true
console.log(`📢 显示答案解析弹窗 - answerDialogVisible: ${answerDialogVisible.value}`)
console.log('formatCorrectAnswer结果:', formatCorrectAnswer(dialogQuestion.value))
}
}
/**
* 提交答案(用于多选题、填空题、问答题)
*/
const submitAnswer = () => {
checkAndHandleAnswer()
}
/**
* 检查客观题答案
*/
const checkObjectiveAnswer = (): boolean => {
const q = currentQuestion.value
if (q.type === 'single' || q.type === 'multiple') {
const correctIndexes = q.options
.map((opt: any, idx: number) => opt.isCorrect ? idx : -1)
.filter((idx: number) => idx !== -1)
// 调试日志:显示正确答案索引和用户选择
console.log(`🔍 客观题判断 - 题目: "${q.title.substring(0, 30)}..."`)
console.log(` 选项详情:`, q.options.map((opt: any, idx: number) => ({
index: idx,
letter: String.fromCharCode(65 + idx),
isCorrect: opt.isCorrect,
content: opt.content?.substring(0, 20) + '...'
})))
console.log(` 正确答案索引: [${correctIndexes.join(', ')}]`)
console.log(` 用户选择索引: [${userAnswer.selected.join(', ')}]`)
const isCorrect = JSON.stringify(userAnswer.selected.sort()) === JSON.stringify(correctIndexes.sort())
console.log(` 判断结果: ${isCorrect ? '✅正确' : '❌错误'}`)
return isCorrect
} else if (q.type === 'judge') {
console.log(`🔍 判断题判断 - 正确答案: ${q.correctAnswer}, 用户答案: ${userAnswer.judge}`)
return userAnswer.judge === q.correctAnswer
}
return false
}
/**
* 检查主观题答案(调用AI判断)
*/
const checkSubjectiveAnswer = async (question: any): Promise<boolean> => {
const q = question // 使用传入的题目快照而不是currentQuestion.value
const answer = q.type === 'blank' ? userAnswer.blank : userAnswer.essay
console.log('🔍 主观题检查 - 使用题目快照:', {
'type': q.type,
'title': q.title?.substring(0, 30),
'correctAnswer': q.correctAnswer,
'explanation': q.explanation?.substring(0, 50)
})
if (!answer || !answer.trim()) {
ElMessage.warning('请输入答案')
return false
}
// 显示AI分析提示
const loadingInstance = ElLoading.service({
lock: true,
text: 'AI 正在分析您的回答...',
background: 'rgba(0, 0, 0, 0.7)',
})
try {
const response = await judgeAnswer({
question: q.title,
correct_answer: q.correctAnswer || '',
user_answer: answer.trim(),
analysis: q.explanation || '' // Dify答案判断工作流需要analysis参数
})
const resp: any = response
console.log('🔍 AI答案判断结果:', {
'response.code': resp.code,
'response.data': resp.data,
'is_correct': resp.data?.is_correct
})
if (resp.code === 200) {
const isCorrect = resp.data?.is_correct || false
console.log(`✅ AI最终判断: ${isCorrect ? '正确' : '错误'}`)
loadingInstance.close()
return isCorrect
} else {
loadingInstance.close()
ElMessage.error('答案判断失败: ' + resp.message)
return false
}
} catch (error: any) {
loadingInstance.close()
console.error('答案判断失败:', error)
ElMessage.error('答案判断失败,请重试')
return false
}
}
/**
* 记录错题到数据库
*/
const recordWrongAnswer = async () => {
const q = currentQuestion.value
// 获取用户答案
let userAnswerText = ''
if (q.type === 'single' || q.type === 'multiple') {
userAnswerText = userAnswer.selected.map(idx => String.fromCharCode(65 + idx)).join(',')
} else if (q.type === 'judge') {
userAnswerText = userAnswer.judge ? '正确' : '错误'
} else if (q.type === 'blank') {
userAnswerText = userAnswer.blank
} else if (q.type === 'essay') {
userAnswerText = userAnswer.essay
}
try {
await recordMistake({
exam_id: currentExamId.value,
round: currentRound.value, // 记录当前轮次
question_id: null, // AI生成的题目没有question_id
knowledge_point_id: q.knowledge_point_id || null,
question_content: q.title,
correct_answer: formatCorrectAnswer(q),
user_answer: userAnswerText,
question_type: q.type // 新增:记录题型
})
} catch (error: any) {
console.error('记录错题失败:', error)
// 记录错题失败不影响答题流程,只打印日志
}
}
// 已被其他函数替代,保留以备后用
// const previousQuestion = () => {
// if (currentIndex.value > 0) {
// currentIndex.value--
// showAnswer.value = false
// clearAnswer()
// }
// }
/**
* 进入下一题
*/
const goToNextQuestion = () => {
if (currentIndex.value < questions.value.length - 1) {
currentIndex.value++
// 只有在没有被外部处理时才清空答案和隐藏答案
if (showAnswer.value) {
showAnswer.value = false
clearAnswer()
}
} else {
// 当前轮次考试结束
finishRound()
}
}
// 已被goToNextQuestion替代
// const nextQuestion = () => {
// goToNextQuestion()
// }
/**
* 记住了按钮点击处理
*/
const handleRemembered = () => {
answerDialogVisible.value = false
showAnswer.value = false
clearAnswer()
goToNextQuestion()
}
/**
* 判断题选择处理
*/
const handleJudgeChange = () => {
// 判断题选择后立即判断
checkAndHandleAnswer()
}
/**
* 判断题卡片点击
*/
const selectJudge = (value: boolean) => {
userAnswer.judge = value
checkAndHandleAnswer()
}
/**
* 填空题回车提交处理
*/
const handleBlankSubmit = () => {
if (userAnswer.blank.trim()) {
checkAndHandleAnswer()
}
}
/**
* 开始错题重考
*/
const startWrongQuestionRound = async () => {
// 计算当前轮次得分
const score = Math.round((correctCount.value / questions.value.length) * 100)
// 注意roundScores 已在 finishRound 中更新,这里不再 push
// 调用API更新当前轮次的得分
try {
await updateRoundScore({
exam_id: currentExamId.value,
round: currentRound.value,
score: score
})
console.log(`✅ 更新第${currentRound.value}轮得分成功: ${score}`)
} catch (error: any) {
console.error('更新轮次得分失败:', error)
ElMessage.error('保存本轮成绩失败,请检查网络')
}
if (currentRound.value >= maxRounds.value) {
// 达到最大轮次,结束考试
finishAllRounds()
return
}
// 检查是否有错题
if (wrongCount.value === 0) {
ElMessage.success('恭喜!本轮考试全部正确!')
finishAllRounds()
return
}
// 计算正确率
const accuracy = Math.round((correctCount.value / questions.value.length) * 100)
// 显示第一轮成绩并提示进入第二轮
const roundMessage = `${currentRound.value}轮考试完成!\n\n` +
`📊 本轮得分:${score}满分100分\n` +
`✅ 答对:${correctCount.value}\n` +
`❌ 答错:${wrongCount.value}\n` +
`📈 正确率:${accuracy}%\n\n` +
`即将进入第${currentRound.value + 1}轮考试(错题重考)\n` +
`10秒后自动开始或点击"立即开始"按钮`
// 开始下一轮错题考试
currentRound.value++
currentIndex.value = 0
correctCount.value = 0
wrongCount.value = 0
// 显示成绩并倒计时
let countdown = 10
const countdownInterval = setInterval(() => {
countdown--
}, 1000)
// 显示提示信息
ElMessageBox.alert(
roundMessage,
`${currentRound.value - 1}轮考试结束`,
{
confirmButtonText: '立即开始第' + currentRound.value + '轮',
type: accuracy >= 80 ? 'success' : 'info',
showClose: false,
beforeClose: (_action, _instance, done) => {
clearInterval(countdownInterval)
done()
}
}
).then(async () => {
clearInterval(countdownInterval)
// 设置生成中状态
isGenerating.value = true
// 重新生成试题(基于上一轮的错题记录)
await loadQuestions()
})
// 10秒后自动开始
setTimeout(() => {
clearInterval(countdownInterval)
if (isGenerating.value) return // 如果已经开始,不重复触发
isGenerating.value = true
ElMessageBox.close()
loadQuestions()
}, 10000)
}
/**
* 完成当前轮次
*/
const finishRound = async () => {
const accuracy = Math.round((correctCount.value / questions.value.length) * 100)
roundScores.value[currentRound.value - 1] = accuracy
if (wrongCount.value === 0 || currentRound.value >= maxRounds.value) {
// 没有错题或达到最大轮次,结束考试
finishAllRounds()
} else {
// 继续下一轮错题重考
await startWrongQuestionRound()
}
}
/**
* 完成所有轮次
*/
const finishAllRounds = async () => {
// 保存最后一轮的得分到数据库
const currentScore = Math.round((correctCount.value / questions.value.length) * 100)
try {
await updateRoundScore({
exam_id: currentExamId.value,
round: currentRound.value,
score: currentScore,
is_final: true
})
console.log(`✅ 更新第${currentRound.value}轮得分成功: ${currentScore}`)
} catch (error: any) {
console.error('更新最后一轮得分失败:', error)
ElMessage.error('保存考试成绩失败,请重试')
}
// 计算平均得分
const avgScore = Math.round(roundScores.value.reduce((sum, score) => sum + score, 0) / roundScores.value.length)
// 如果第一轮就满分,特殊恭喜
if (currentRound.value === 1 && wrongCount.value === 0) {
ElMessageBox.alert(
`🎉 恭喜!考试满分!\n\n第1轮正确率100%\n\n您的考试成绩已记录`,
'考试完成',
{
confirmButtonText: '返回课程中心',
type: 'success',
callback: () => {
router.push('/trainee/course-center')
}
}
)
return
}
// 多轮考试结果
let resultMessage = `考试完成!共进行了 ${currentRound.value} 轮考试\n\n`
roundScores.value.forEach((score, index) => {
resultMessage += `${index + 1}轮正确率:${score}%\n`
})
resultMessage += `\n平均正确率${avgScore}%\n\n您的考试成绩已记录`
ElMessageBox.alert(resultMessage, '考试结果', {
confirmButtonText: '返回课程中心',
type: avgScore >= 80 ? 'success' : 'info',
callback: () => {
router.push('/trainee/course-center')
}
})
}
// 已被finishAllRounds替代
// const finishPractice = () => {
// finishAllRounds()
// }
/**
* 获取题型标签类型
*/
const getQuestionTypeTag = (type: string) => {
const typeMap: Record<string, string> = {
single: 'primary',
multiple: 'success',
judge: 'warning',
blank: 'danger',
essay: 'info'
}
return typeMap[type] || 'primary'
}
/**
* 获取题型文本
*/
const getQuestionTypeText = (type: string) => {
const typeMap: Record<string, string> = {
single: '单选题',
multiple: '多选题',
judge: '判断题',
blank: '填空题',
essay: '问答题'
}
return typeMap[type] || ''
}
/**
* 渲染markdown内容
*/
const renderMarkdown = (content: string): string => {
if (!content) return ''
try {
// 配置marked选项支持GFMGitHub Flavored Markdown
marked.setOptions({
breaks: true, // 支持GFM换行单个换行符转为<br>
gfm: true // 启用GitHub Flavored Markdown
// headerIds, mangle 等选项已废弃
})
const html = marked.parse(content) as string
return DOMPurify.sanitize(html)
} catch (error) {
console.error('Markdown渲染失败:', error)
return content
}
}
/**
* 格式化正确答案
*/
const formatCorrectAnswer = (question: any) => {
if (question.type === 'single' || question.type === 'multiple') {
// 如果有correctAnswer字段来自Dify的原始correct
if (question.correctAnswer) {
// 多选题格式优化:将 "Axxx,Bxxx" 改为换行显示
// 匹配 ",A" 或 ",B" 等格式,在逗号后的选项字母前换行
const formatted = question.correctAnswer.replace(/,([A-Za-z][:])/g, '\n$1')
return formatted
}
// 否则从options中提取
return question.options
.map((opt: any, idx: number) => opt.isCorrect ? String.fromCharCode(65 + idx) : null)
.filter((v: any) => v)
.join('、')
} else if (question.type === 'judge') {
// 优先使用correctAnswerTextDify原始值"正确"或"错误"
if (question.correctAnswerText) {
return question.correctAnswerText
}
// 备用从boolean转换
return question.correctAnswer ? '正确' : '错误'
} else if (question.type === 'blank' || question.type === 'essay') {
return question.correctAnswer || ''
}
return ''
}
/**
* 保存答题进度
*/
const saveProgress = () => {
const progress = {
currentIndex: currentIndex.value,
currentRound: currentRound.value,
correctCount: correctCount.value,
wrongCount: wrongCount.value,
roundScores: roundScores.value,
wrongQuestions: wrongQuestions.value,
currentWrongIndex: currentWrongIndex.value,
isWrongQuestionMode: isWrongQuestionMode.value,
userAnswers: userAnswers.value,
timestamp: Date.now()
}
try {
localStorage.setItem(PRACTICE_STORAGE_KEY, JSON.stringify(progress))
} catch (error) {
console.warn('无法保存答题进度:', error)
}
}
/**
* 恢复答题进度
*/
// 组件挂载时初始化考试
onMounted(() => {
initExam()
})
// 页面卸载前清理
onUnmounted(() => {
// 可以在这里清理资源
})
</script>
<style lang="scss" scoped>
.practice-container {
max-width: 1200px;
margin: 0 auto;
// 加载状态
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 60vh;
padding: 60px 20px;
h3 {
margin: 24px 0 12px;
font-size: 20px;
font-weight: 600;
color: #333;
}
p {
font-size: 14px;
color: #666;
margin: 0;
}
}
// 无题目状态
.no-questions {
display: flex;
align-items: center;
justify-content: center;
min-height: 60vh;
}
// 考试内容
.practice-content {
.practice-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
padding: 16px 24px;
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
.header-info {
h2 {
font-size: 20px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.progress-info {
font-size: 14px;
color: #666;
.separator {
margin: 0 12px;
color: #ddd;
}
}
}
}
.practice-main {
.question-section {
margin-bottom: 24px;
padding: 32px;
.question-type {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
.question-score {
font-size: 14px;
color: #666;
}
}
.question-content {
h3 {
font-size: 18px;
font-weight: 500;
color: #333;
margin-bottom: 24px;
line-height: 1.6;
}
.options-list {
.option-item {
display: flex;
align-items: center;
padding: 16px 20px;
margin-bottom: 12px;
background: #f5f7fa;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
&:hover:not(.selected) {
background: #e6e8eb;
}
&.selected {
background: rgba(102, 126, 234, 0.1);
border: 1px solid #667eea;
}
&.correct {
background: rgba(103, 194, 58, 0.1);
border: 1px solid #67c23a;
}
&.wrong {
background: rgba(245, 108, 108, 0.1);
border: 1px solid #f56c6c;
}
.option-label {
width: 32px;
height: 32px;
background: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
color: #333;
margin-right: 16px;
}
.option-content {
flex: 1;
font-size: 16px;
color: #333;
}
.result-icon {
position: absolute;
right: 20px;
font-size: 24px;
&.correct {
color: #67c23a;
}
&.wrong {
color: #f56c6c;
}
}
}
}
.judge-options {
.judge-cards {
display: flex;
gap: 20px;
justify-content: center;
.judge-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 140px;
height: 120px;
border: 2px solid #e4e7ed;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
background: #fff;
&:hover:not(.disabled) {
border-color: #409eff;
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
}
&.active {
border-color: #409eff;
background: linear-gradient(135deg, #ecf5ff 0%, #f5f9ff 100%);
.judge-icon {
transform: scale(1.1);
}
}
&.disabled {
cursor: not-allowed;
opacity: 0.8;
}
&.correct {
border-color: #67c23a;
background: linear-gradient(135deg, #f0f9eb 0%, #f5faf2 100%);
.judge-text {
color: #67c23a;
}
}
&.wrong {
border-color: #f56c6c;
background: linear-gradient(135deg, #fef0f0 0%, #fdf5f5 100%);
.judge-text {
color: #f56c6c;
}
}
.judge-icon {
margin-bottom: 10px;
transition: transform 0.3s ease;
&.correct-icon {
color: #67c23a;
}
&.wrong-icon {
color: #f56c6c;
}
}
.judge-text {
font-size: 16px;
font-weight: 500;
color: #606266;
}
}
}
}
.blank-answer {
.blank-input-wrapper {
max-width: 500px;
.el-input {
:deep(.el-input__wrapper) {
padding: 8px 15px;
box-shadow: 0 0 0 1px #dcdfe6 inset;
border-radius: 8px;
transition: all 0.3s ease;
&:hover {
box-shadow: 0 0 0 1px #c0c4cc inset;
}
&.is-focus {
box-shadow: 0 0 0 1px #409eff inset;
}
}
:deep(.el-input__inner) {
font-size: 16px;
}
:deep(.el-input__prefix) {
color: #909399;
}
}
.blank-hint {
margin-top: 8px;
font-size: 12px;
color: #909399;
text-align: right;
}
}
}
.essay-answer {
:deep(.el-textarea__inner) {
font-size: 14px;
line-height: 1.6;
}
}
}
.answer-section {
margin-top: 24px;
.correct-feedback {
display: flex;
align-items: center;
gap: 16px;
padding: 20px;
background: rgba(103, 194, 58, 0.1);
border: 1px solid #67c23a;
border-radius: 12px;
animation: correctPulse 0.6s ease-in-out;
.feedback-icon {
flex-shrink: 0;
}
.feedback-text {
h3 {
font-size: 18px;
font-weight: 600;
color: #67c23a;
margin: 0 0 4px 0;
}
p {
font-size: 14px;
color: #67c23a;
margin: 0;
opacity: 0.8;
}
}
}
.answer-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
}
.correct-answer {
font-size: 14px;
color: #67c23a;
margin-bottom: 12px;
}
.answer-explanation {
font-size: 14px;
color: #666;
line-height: 1.6;
background: #f5f7fa;
padding: 12px 16px;
border-radius: 8px;
}
}
}
.submit-section {
display: flex;
justify-content: center;
margin-top: 32px;
.el-button {
min-width: 160px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
}
}
}
}
}
// 动画效果
@keyframes correctPulse {
0% {
transform: scale(0.95);
opacity: 0;
}
50% {
transform: scale(1.02);
}
100% {
transform: scale(1);
opacity: 1;
}
}
// 答案解析弹窗样式
:deep(.answer-dialog) {
.el-dialog {
margin-top: 15vh !important;
margin-bottom: auto;
}
.el-dialog__body {
padding: 10px 20px;
max-height: none;
overflow: visible;
}
}
.answer-dialog-content {
.result-section {
text-align: center;
margin-bottom: 16px;
.result-icon-large {
width: 64px;
height: 64px;
margin: 0 auto 10px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
&.wrong {
background: rgba(245, 108, 108, 0.1);
color: #f56c6c;
}
}
.result-text {
font-size: 18px;
font-weight: 600;
color: #f56c6c;
margin: 0;
}
}
.dialog-scroll-area {
max-height: 60vh;
overflow-y: auto;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #dcdfe6;
border-radius: 3px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
}
.question-review,
.answer-review,
.explanation-review {
margin-bottom: 16px;
h4 {
font-size: 14px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
p {
font-size: 14px;
line-height: 1.6;
margin: 0;
color: #666;
}
.correct-answer-text {
font-size: 14px;
line-height: 1.6;
color: #333;
background: #f0f9ff;
border-left: 4px solid #67c23a;
padding: 10px 14px;
border-radius: 6px;
strong {
color: #333;
font-weight: 600;
}
}
.explanation-text {
font-size: 13px;
line-height: 1.6;
color: #666;
background: #f5f7fa;
padding: 12px;
border-radius: 8px;
strong {
color: #333;
font-weight: 600;
}
}
}
}
// Markdown内容样式
.markdown-content {
line-height: 1.8;
p {
margin: 8px 0;
}
h1, h2, h3, h4, h5, h6 {
margin: 16px 0 8px 0;
font-weight: 600;
}
ul, ol {
margin: 8px 0 8px 8px; // 增加左边距,避免压到绿边
padding-left: 20px;
}
li {
margin: 4px 0;
padding-left: 4px; // 列表项增加左内边距
}
code {
background: rgba(0, 0, 0, 0.05);
padding: 2px 6px;
border-radius: 4px;
font-family: 'Monaco', 'Courier New', monospace;
font-size: 13px;
}
pre {
background: rgba(0, 0, 0, 0.05);
padding: 12px;
border-radius: 8px;
overflow-x: auto;
code {
background: none;
padding: 0;
}
}
blockquote {
border-left: 4px solid #409eff;
padding-left: 16px;
margin: 12px 0;
color: #666;
}
strong, b {
font-weight: 700 !important;
color: #333 !important;
}
em, i {
font-style: italic !important;
}
a {
color: #409eff;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
.dialog-footer {
text-align: center;
.el-button {
min-width: 120px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
}
}
// 响应式
@media (max-width: 768px) {
.practice-container {
padding: 0 12px;
.practice-header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
padding: 12px 16px;
.el-button {
width: 100%;
}
}
.question-section {
padding: 16px !important;
.option-item {
padding: 12px 16px !important;
.option-label {
width: 28px !important;
height: 28px !important;
font-size: 14px;
}
.option-content {
font-size: 14px !important;
}
}
}
// 修复"确认选择"按钮 - 确保完全可见且易于点击
.submit-section {
margin-top: 16px !important;
padding: 0 8px 24px; // 底部增加安全距离
position: sticky;
bottom: 0;
background: linear-gradient(to top, #f5f7fa 80%, transparent);
padding-top: 20px;
z-index: 10;
.el-button {
width: 100%;
min-width: 0 !important;
min-height: 48px; // 增大按钮高度,便于手机点击
font-size: 16px;
font-weight: 600;
border-radius: 24px;
}
}
}
// 修复"记住了"按钮所在弹窗的移动端显示
.answer-dialog-content {
.result-section {
.result-icon-large {
width: 60px;
height: 60px;
.el-icon {
font-size: 36px !important;
}
}
.result-text {
font-size: 18px;
}
}
.question-review,
.answer-review,
.explanation-review {
margin-bottom: 16px;
h4 {
font-size: 14px;
margin-bottom: 8px;
}
p, .correct-answer-text, .explanation-text {
font-size: 13px;
line-height: 1.6;
}
}
}
.dialog-footer {
padding: 16px 20px 20px !important;
.el-button {
width: 100% !important;
min-width: 0 !important;
min-height: 48px; // 增大按钮高度,便于手机点击
font-size: 16px;
font-weight: 600;
border-radius: 24px;
}
}
}
// 移动端弹窗全局样式覆盖(需要穿透到 el-dialog
:deep(.el-dialog) {
@media (max-width: 768px) {
width: 92% !important;
max-width: none !important;
margin: 10vh auto !important;
.el-dialog__header {
padding: 16px 20px 12px;
.el-dialog__title {
font-size: 16px;
font-weight: 600;
}
}
.el-dialog__body {
padding: 12px 16px;
max-height: 60vh;
overflow-y: auto;
}
.el-dialog__footer {
padding: 12px 16px 20px;
}
}
}
</style>