From df717c6705652d9c9757db42784cbd45d62869d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E6=A2=A6?= Date: Tue, 16 Jun 2026 10:05:54 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=AE=B6=E9=95=BF?= =?UTF-8?q?=E6=8A=A5=E5=91=8A=E8=AF=A6=E6=83=85=E9=A1=B5=E9=9D=A2=E7=AD=94?= =?UTF-8?q?=E9=A2=98=E5=8D=A1=E6=95=B0=E6=8D=AE=E5=85=AC=E5=BC=8F=E9=97=AE?= =?UTF-8?q?=E9=A2=98=EF=BC=8C=E6=96=B0=E5=A2=9E=E9=80=89=E9=A1=B9=E6=A8=A1?= =?UTF-8?q?=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/parent/reports/detail.vue | 142 ++++++++++++++++++++++++++ src/pages/parent/utils/data.ts | 119 +++++++++++++++++++-- src/pages/parent/utils/paperDetail.ts | 73 ++++++++++++- src/utils/questionHtml.ts | 2 + 4 files changed, 325 insertions(+), 11 deletions(-) diff --git a/src/pages/parent/reports/detail.vue b/src/pages/parent/reports/detail.vue index 87869c2..35985f1 100644 --- a/src/pages/parent/reports/detail.vue +++ b/src/pages/parent/reports/detail.vue @@ -238,6 +238,30 @@ 暂无题干 + + + 选项 + + + {{ opt.label }} + + + {{ opt.value }} + + + + 正确答案 @@ -417,6 +441,7 @@ import { mapPaperQuestionSummaries, normalizeLegacyQuestion, } from '../utils/paperDetail'; +import { isHtmlContent } from '@/utils/questionHtml'; type DetailTab = 'analysis' | 'answers' | 'knowledge'; type KnowledgeLevel = 'all' | 'weak' | 'pass' | 'good'; @@ -709,6 +734,46 @@ const statusClass = (status?: ReportQuestion['status']) => { return 'status-wrong'; }; +/** 从正确答案 HTML 中提取选项字母(如 "A"、"ABD"、"C"),与 web 版 extractChoiceLettersFromAnswerHtml 逻辑一致 */ +const extractCorrectAnswerLetters = (): string[] => { + const detail = selectedQuestionDetail.value; + if (!detail) return []; + // 优先从 correctAnswerHtml 提取纯字母 + const answerHtml = detail.correctAnswerHtml || ''; + const stripped = answerHtml.replace(/<[^>]*>/g, '').replace(/ /gi, ' ').toUpperCase(); + const letters = stripped.match(/[A-Z]/g); + if (letters?.length) return [...new Set(letters)]; + // 退而求其次从 studentAnswer 反推(仅当状态为正确时) + if (detail.status === 'correct') { + const stuAns = (detail.studentAnswer || '').toUpperCase().match(/[A-Z]/g); + if (stuAns?.length) return [...new Set(stuAns)]; + } + return []; +}; + +/** 从学员答案中提取选项字母 */ +const extractStudentAnswerLetters = (): string[] => { + const detail = selectedQuestionDetail.value; + if (!detail) return []; + const ans = (detail.studentAnswer || '').toUpperCase(); + const letters = ans.match(/[A-Z]/g); + return letters ? [...new Set(letters)] : []; +}; + +/** 计算单个选项的样式类名,参考 web 端 SingleChoice/MultipleChoice 的 success/fail 样式 */ +const optionItemClass = (opt: { label: string }, _optIdx: number) => { + const correctLetters = extractCorrectAnswerLetters(); + const studentLetters = extractStudentAnswerLetters(); + const label = opt.label.toUpperCase(); + + const isCorrect = correctLetters.includes(label); + const studentSelected = studentLetters.includes(label); + + if (isCorrect) return 'option-correct'; + if (studentSelected) return 'option-wrong'; + return ''; +}; + const switchTab = (key: DetailTab) => { activeTab.value = key; if (key === 'answers') { @@ -1322,6 +1387,83 @@ const goHome = () => { border-color: #ddd6fe; } +/* 选项模块样式(参考 web 端 SingleChoice / MultipleChoice 组件) */ +.options-card { + border-color: #c7d2fe; +} + +.options-card .field-label { + background: #6366f1; +} + +.option-list { + display: flex; + flex-direction: column; + gap: 14rpx; + margin-top: 4rpx; +} + +.option-item { + display: flex; + align-items: flex-start; + padding: 14rpx 16rpx; + border-radius: 14rpx; + border: 2rpx solid #e0e7ff; + background: #f8faff; + gap: 12rpx; + transition: all 0.2s; +} + +.option-item.option-correct { + border-color: #86efac; + background: #f0fdf4; +} + +.option-item.option-correct .option-label { + background: #22c55e; + color: #ffffff; +} + +.option-item.option-wrong { + border-color: #fca5a5; + background: #fef2f2; +} + +.option-item.option-wrong .option-label { + background: #ef4444; + color: #ffffff; +} + +.option-label { + flex-shrink: 0; + width: 40rpx; + height: 40rpx; + border-radius: 10rpx; + display: flex; + align-items: center; + justify-content: center; + background: #e0e7ff; + color: #6366f1; + font-size: 22rpx; + font-weight: 900; + line-height: 1; +} + +.option-value { + flex: 1; + min-width: 0; + color: #334155; + font-size: 24rpx; + line-height: 1.55; + overflow-wrap: anywhere; + word-break: break-word; +} + +.option-value rich-text { + width: 100%; + max-width: 100%; +} + .field-label { display: inline-flex; margin-bottom: 8rpx; diff --git a/src/pages/parent/utils/data.ts b/src/pages/parent/utils/data.ts index ebc26f9..1f3bfd2 100644 --- a/src/pages/parent/utils/data.ts +++ b/src/pages/parent/utils/data.ts @@ -62,6 +62,13 @@ export type ReportQuestionSummary = Pick< 'no' | 'type' | 'score' | 'status' | 'classRate' | 'schoolRate' | 'knowledge' >; +export interface QuestionOption { + /** 选项标签(A、B、C、D...) */ + label: string; + /** 选项内容(HTML 格式) */ + value: string; +} + export interface ReportQuestion { no: number; type: string; @@ -71,6 +78,8 @@ export interface ReportQuestion { stemHtml: string; /** 题干中的图片(与学生端一致,从 HTML 中拆出单独展示) */ stemImages: string[]; + /** 选择题的选项列表 */ + options: QuestionOption[]; /** 供 rich-text 渲染的正确答案 HTML */ correctAnswerHtml: string; /** 纯文本学员答案(选择题等) */ @@ -250,16 +259,106 @@ export const reportDetails: Record = { { difficulty: '难题', range: '19-23', score: '18/24', rate: '75.0%' }, ], questions: [ - { no: 1, type: '选择题', score: '4/4', status: 'correct', stem: '请结合材料完成本题要求。', correctAnswer: 'A', studentAnswer: 'A', classRate: '88%', schoolRate: '84%', knowledge: '字音字形' }, - { no: 2, type: '选择题', score: '4/4', status: 'correct', stem: '根据题意选择最符合要求的一项。', correctAnswer: 'C', studentAnswer: 'C', classRate: '84%', schoolRate: '81%', knowledge: '词语运用' }, - { no: 3, type: '选择题', score: '4/4', status: 'correct', stem: '阅读材料并判断关键条件。', correctAnswer: 'B', studentAnswer: 'B', classRate: '80%', schoolRate: '78%', knowledge: '病句辨析' }, - { no: 4, type: '选择题', score: '4/4', status: 'correct', stem: '从选项中选出表达准确的一项。', correctAnswer: 'D', studentAnswer: 'D', classRate: '76%', schoolRate: '75%', knowledge: '文学常识' }, - { no: 5, type: '选择题', score: '4/4', status: 'correct', stem: '根据语境判断标点使用。', correctAnswer: 'A', studentAnswer: 'A', classRate: '72%', schoolRate: '72%', knowledge: '标点符号' }, - { no: 6, type: '选择题', score: '4/4', status: 'correct', stem: '判断句子修辞手法。', correctAnswer: 'B', studentAnswer: 'B', classRate: '88%', schoolRate: '69%', knowledge: '修辞手法' }, - { no: 7, type: '选择题', score: '4/4', status: 'correct', stem: '结合古诗文选择正确理解。', correctAnswer: 'C', studentAnswer: 'C', classRate: '84%', schoolRate: '84%', knowledge: '古诗文默写' }, - { no: 8, type: '选择题', score: '4/4', status: 'correct', stem: '阅读片段后概括中心信息。', correctAnswer: 'D', studentAnswer: 'D', classRate: '80%', schoolRate: '81%', knowledge: '名著阅读' }, - { no: 9, type: '选择题', score: '4/4', status: 'correct', stem: '判断句间衔接是否连贯。', correctAnswer: 'A', studentAnswer: 'A', classRate: '76%', schoolRate: '78%', knowledge: '语段衔接' }, - { no: 10, type: '选择题', score: '4/4', status: 'correct', stem: '完成句式转换。', correctAnswer: 'C', studentAnswer: 'C', classRate: '72%', schoolRate: '75%', knowledge: '句式转换' }, + { + no: 1, type: '选择题', score: '4/4', status: 'correct', stem: '请结合材料完成本题要求。', + correctAnswer: 'A', studentAnswer: 'A', classRate: '88%', schoolRate: '84%', knowledge: '字音字形', + options: [ + { label: 'A', value: '脍炙人口' }, + { label: 'B', value: '哄堂大笑' }, + { label: 'C', value: '锋芒毕露' }, + { label: 'D', value: '忍俊不禁' }, + ], + }, + { + no: 2, type: '选择题', score: '4/4', status: 'correct', stem: '根据题意选择最符合要求的一项。', + correctAnswer: 'C', studentAnswer: 'C', classRate: '84%', schoolRate: '81%', knowledge: '词语运用', + options: [ + { label: 'A', value: '通过这次活动,使我们开阔了眼界。' }, + { label: 'B', value: '他的写作水平有了明显的提高。' }, + { label: 'C', value: '春天的西湖是一个美丽的季节。' }, + { label: 'D', value: '我们要养成认真思考的习惯。' }, + ], + }, + { + no: 3, type: '选择题', score: '4/4', status: 'correct', stem: '阅读材料并判断关键条件。', + correctAnswer: 'B', studentAnswer: 'B', classRate: '80%', schoolRate: '78%', knowledge: '病句辨析', + options: [ + { label: 'A', value: '他是多少个死难者中幸免的一个。' }, + { label: 'B', value: '文章的主题确定以后,还要根据主题的需要组织材料。' }, + { label: 'C', value: '由于运用了科学的复习方法,他的学习效率有了很大改进。' }, + { label: 'D', value: '老工人的一席话深深触动了小明的心。' }, + ], + }, + { + no: 4, type: '选择题', score: '4/4', status: 'correct', stem: '从选项中选出表达准确的一项。', + correctAnswer: 'D', studentAnswer: 'D', classRate: '76%', schoolRate: '75%', knowledge: '文学常识', + options: [ + { label: 'A', value: '《诗经》是我国第一部诗歌总集。' }, + { label: 'B', value: '《论语》是孔子及其弟子的语录体著作。' }, + { label: 'C', value: '《史记》是我国第一部纪传体通史。' }, + { label: 'D', value: '《红楼梦》的作者是吴承恩。' }, + ], + }, + { + no: 5, type: '选择题', score: '4/4', status: 'correct', stem: '根据语境判断标点使用。', + correctAnswer: 'A', studentAnswer: 'A', classRate: '72%', schoolRate: '72%', knowledge: '标点符号', + options: [ + { label: 'A', value: '"你到哪里去?"她轻声问道。' }, + { label: 'B', value: '"你到哪里去"?她轻声问道。' }, + { label: 'C', value: '"你到哪里去,"她轻声问道。' }, + { label: 'D', value: '"你到哪里去"她轻声问道。' }, + ], + }, + { + no: 6, type: '选择题', score: '4/4', status: 'correct', stem: '判断句子修辞手法。', + correctAnswer: 'B', studentAnswer: 'B', classRate: '88%', schoolRate: '69%', knowledge: '修辞手法', + options: [ + { label: 'A', value: '比喻' }, + { label: 'B', value: '拟人' }, + { label: 'C', value: '排比' }, + { label: 'D', value: '夸张' }, + ], + }, + { + no: 7, type: '选择题', score: '4/4', status: 'correct', stem: '结合古诗文选择正确理解。', + correctAnswer: 'C', studentAnswer: 'C', classRate: '84%', schoolRate: '84%', knowledge: '古诗文默写', + options: [ + { label: 'A', value: '海内存知己,天涯若比邻——王勃《送杜少府之任蜀州》' }, + { label: 'B', value: '大漠孤烟直,长河落日圆——王维《使至塞上》' }, + { label: 'C', value: '感时花溅泪,恨别鸟惊心——杜甫《春望》' }, + { label: 'D', value: '落红不是无情物,化作春泥更护花——龚自珍《己亥杂诗》' }, + ], + }, + { + no: 8, type: '选择题', score: '4/4', status: 'correct', stem: '阅读片段后概括中心信息。', + correctAnswer: 'D', studentAnswer: 'D', classRate: '80%', schoolRate: '81%', knowledge: '名著阅读', + options: [ + { label: 'A', value: '《朝花夕拾》——鲁迅回忆童年生活的散文集' }, + { label: 'B', value: '《骆驼祥子》——老舍描写北京底层劳动人民的小说' }, + { label: 'C', value: '《钢铁是怎样炼成的》——奥斯特洛夫斯基自传体小说' }, + { label: 'D', value: '《海底两万里》——凡尔纳的科幻小说,描写了海底探险' }, + ], + }, + { + no: 9, type: '选择题', score: '4/4', status: 'correct', stem: '判断句间衔接是否连贯。', + correctAnswer: 'A', studentAnswer: 'A', classRate: '76%', schoolRate: '78%', knowledge: '语段衔接', + options: [ + { label: 'A', value: '首先……其次……最后……' }, + { label: 'B', value: '然后……接着……最后……' }, + { label: 'C', value: '不但……而且……还……' }, + { label: 'D', value: '因为……所以……因此……' }, + ], + }, + { + no: 10, type: '选择题', score: '4/4', status: 'correct', stem: '完成句式转换。', + correctAnswer: 'C', studentAnswer: 'C', classRate: '72%', schoolRate: '75%', knowledge: '句式转换', + options: [ + { label: 'A', value: '把字句:我把作业写完了。' }, + { label: 'B', value: '被字句:作业被我写完了。' }, + { label: 'C', value: '反问句:难道这不是事实吗?' }, + { label: 'D', value: '感叹句:这真是太好了!' }, + ], + }, { no: 11, type: '填空题', score: '5/5', status: 'correct', stem: '补全古文实词解释。', correctAnswer: '明察', studentAnswer: '明察', classRate: '88%', schoolRate: '72%', knowledge: '古文实词' }, { no: 12, type: '填空题', score: '5/5', status: 'correct', stem: '补全古文虚词用法。', correctAnswer: '之', studentAnswer: '之', classRate: '84%', schoolRate: '69%', knowledge: '古文虚词' }, { no: 13, type: '填空题', score: '5/5', status: 'correct', stem: '判断文言断句位置。', correctAnswer: 'B', studentAnswer: 'B', classRate: '80%', schoolRate: '84%', knowledge: '文言断句' }, diff --git a/src/pages/parent/utils/paperDetail.ts b/src/pages/parent/utils/paperDetail.ts index ff40591..ef75922 100644 --- a/src/pages/parent/utils/paperDetail.ts +++ b/src/pages/parent/utils/paperDetail.ts @@ -6,7 +6,7 @@ import { getQuestionStemSource, isHtmlContent, } from '@/utils/questionHtml'; -import type { ReportQuestion, ReportQuestionSummary } from './data'; +import type { QuestionOption, ReportQuestion, ReportQuestionSummary } from './data'; export type QuestionStatus = 'correct' | 'wrong' | 'unanswered'; @@ -93,6 +93,73 @@ const formatQuestionScoreText = (item: Record) => { return `${actualScore}/${fullScore || actualScore || 0}`; }; +const OPTION_LABELS = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O']; + +/** 从原始题目数据中解析选项列表,参考 web 版 Question.vue 的 rawParsedOptions / parsedOptionHtml 逻辑 */ +const parseQuestionOptions = (item: Record): QuestionOption[] => { + // 1. 优先解析 optionHtml(标准格式:JSON 数组,每项含 html 属性) + const optHtml = item.optionHtml; + if (optHtml != null) { + try { + const parsed = typeof optHtml === 'string' ? JSON.parse(optHtml) : optHtml; + if (Array.isArray(parsed)) { + return parsed.map((opt: any, idx: number) => ({ + label: OPTION_LABELS[idx] || String.fromCharCode(65 + idx), + value: typeof opt === 'string' ? opt : (opt.html || opt.value || opt.optionValue || ''), + })); + } + } catch { + /* ignore parse error */ + } + } + + // 2. 尝试 optionsMu / options(多种格式) + let opts: any = item.optionsMu ?? item.options ?? item.optionList; + if (opts == null) return []; + + if (typeof opts === 'string') { + try { + opts = JSON.parse(opts); + } catch { + return []; + } + } + if (!Array.isArray(opts)) return []; + + // 3. 扁平化多余嵌套 [[...]] → [...] + while (opts.length === 1 && Array.isArray(opts[0])) { + opts = opts[0]; + } + + // 4. 数组的数组(完形填空/阅读理解结构)→ 取第一组选项作为当前题目的选项 + if (opts.length && Array.isArray(opts[0])) { + const firstGroup = opts[0]; + if (Array.isArray(firstGroup)) { + return firstGroup.map((opt: any, idx: number) => { + const value = typeof opt === 'string' ? opt : (opt.html || opt.value || opt.optionValue || ''); + return { + label: OPTION_LABELS[idx] || String.fromCharCode(65 + idx), + value: String(value || '').replace(/http:\/\//gi, 'https://'), + }; + }); + } + } + + // 5. 纯字符串数组 → 简单选项 + if (opts.every((i: any) => typeof i === 'string')) { + return opts.map((opt: string, idx: number) => ({ + label: OPTION_LABELS[idx] || String.fromCharCode(65 + idx), + value: String(opt || '').replace(/http:\/\//gi, 'https://'), + })); + } + + // 6. 对象数组(带 html/value 属性的对象) + return opts.map((opt: any, idx: number) => ({ + label: opt.label || opt.optionKey || OPTION_LABELS[idx] || String.fromCharCode(65 + idx), + value: String(opt.html || opt.value || opt.optionValue || opt.content || '').replace(/http:\/\//gi, 'https://'), + })); +}; + export const mapPaperQuestionSummary = (item: Record, index: number): ReportQuestionSummary => { return { no: Number(item.titleSort ?? item.sort ?? index + 1), @@ -120,6 +187,7 @@ export const mapPaperQuestion = (item: Record, index: number): const answerSource = getQuestionAnswerSource(item); const { studentAnswer, studentAnswerHtml, studentAnswerImages } = buildStudentAnswer(item); const stemImages = extractTitleImages(stemSource); + const options = parseQuestionOptions(item); return { no: Number(item.titleSort ?? item.sort ?? index + 1), @@ -128,6 +196,7 @@ export const mapPaperQuestion = (item: Record, index: number): status, stemHtml: formatQuestionHtml(stemSource, { stripImages: true }), stemImages, + options, correctAnswerHtml: formatQuestionHtml(answerSource), studentAnswer, studentAnswerHtml, @@ -151,6 +220,7 @@ export const mapPaperQuestions = (list: unknown[]) => export const normalizeLegacyQuestion = (item: Record): ReportQuestion => { const legacy = item as ReportQuestion & { stem?: string; correctAnswer?: string }; if (legacy.stemHtml) return legacy as ReportQuestion; + const legacyOptions = Array.isArray((item as any).options) ? (item as any).options : []; return { no: Number(legacy.no), type: String(legacy.type), @@ -158,6 +228,7 @@ export const normalizeLegacyQuestion = (item: Record): ReportQu status: legacy.status, stemHtml: String(legacy.stem || ''), stemImages: [], + options: legacyOptions, correctAnswerHtml: String(legacy.correctAnswer || ''), studentAnswer: String(legacy.studentAnswer || ''), studentAnswerHtml: '', diff --git a/src/utils/questionHtml.ts b/src/utils/questionHtml.ts index 77383cf..fda8eb4 100644 --- a/src/utils/questionHtml.ts +++ b/src/utils/questionHtml.ts @@ -252,7 +252,9 @@ const simpleFormulaToHtml = (formula: string): string => { text = text.replace(/\\pm\b/g, '±'); text = text.replace(/\\times\b/g, '×'); text = text.replace(/\\div\b/g, '÷'); + text = text.replace(/\\le\b/g, '≤'); text = text.replace(/\\leq\b/g, '≤'); + text = text.replace(/\\ge\b/g, '≥'); text = text.replace(/\\geq\b/g, '≥'); text = text.replace(/\\neq\b/g, '≠'); text = text.replace(/\\in\b/g, '∈');