fix: 修复家长报告详情页面答题卡数据公式问题,新增选项模块

This commit is contained in:
阿梦 2026-06-16 10:05:54 +08:00
parent 9a25d8976f
commit df717c6705
4 changed files with 325 additions and 11 deletions

View File

@ -238,6 +238,30 @@
暂无题干 暂无题干
</text> </text>
</view> </view>
<!-- 选项模块参考 web Subject/doWork SingleChoice/MultipleChoice 组件 -->
<view
v-if="selectedQuestionDetail.options?.length"
class="field-card options-card"
>
<text class="field-label">选项</text>
<view class="option-list">
<view
v-for="(opt, optIdx) in selectedQuestionDetail.options"
:key="optIdx"
class="option-item"
:class="optionItemClass(opt, optIdx)"
>
<text class="option-label">{{ opt.label }}</text>
<view class="option-value">
<rich-text
v-if="isHtmlContent(opt.value)"
:nodes="opt.value"
/>
<text v-else>{{ opt.value }}</text>
</view>
</view>
</view>
</view>
<view class="field-card success"> <view class="field-card success">
<text class="field-label">正确答案</text> <text class="field-label">正确答案</text>
<view v-if="selectedQuestionDetail.correctAnswerHtml" class="field-content rich-content"> <view v-if="selectedQuestionDetail.correctAnswerHtml" class="field-content rich-content">
@ -417,6 +441,7 @@ import {
mapPaperQuestionSummaries, mapPaperQuestionSummaries,
normalizeLegacyQuestion, normalizeLegacyQuestion,
} from '../utils/paperDetail'; } from '../utils/paperDetail';
import { isHtmlContent } from '@/utils/questionHtml';
type DetailTab = 'analysis' | 'answers' | 'knowledge'; type DetailTab = 'analysis' | 'answers' | 'knowledge';
type KnowledgeLevel = 'all' | 'weak' | 'pass' | 'good'; type KnowledgeLevel = 'all' | 'weak' | 'pass' | 'good';
@ -709,6 +734,46 @@ const statusClass = (status?: ReportQuestion['status']) => {
return 'status-wrong'; 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(/&nbsp;/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) => { const switchTab = (key: DetailTab) => {
activeTab.value = key; activeTab.value = key;
if (key === 'answers') { if (key === 'answers') {
@ -1322,6 +1387,83 @@ const goHome = () => {
border-color: #ddd6fe; 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 { .field-label {
display: inline-flex; display: inline-flex;
margin-bottom: 8rpx; margin-bottom: 8rpx;

View File

@ -62,6 +62,13 @@ export type ReportQuestionSummary = Pick<
'no' | 'type' | 'score' | 'status' | 'classRate' | 'schoolRate' | 'knowledge' 'no' | 'type' | 'score' | 'status' | 'classRate' | 'schoolRate' | 'knowledge'
>; >;
export interface QuestionOption {
/** 选项标签A、B、C、D... */
label: string;
/** 选项内容HTML 格式) */
value: string;
}
export interface ReportQuestion { export interface ReportQuestion {
no: number; no: number;
type: string; type: string;
@ -71,6 +78,8 @@ export interface ReportQuestion {
stemHtml: string; stemHtml: string;
/** 题干中的图片(与学生端一致,从 HTML 中拆出单独展示) */ /** 题干中的图片(与学生端一致,从 HTML 中拆出单独展示) */
stemImages: string[]; stemImages: string[];
/** 选择题的选项列表 */
options: QuestionOption[];
/** 供 rich-text 渲染的正确答案 HTML */ /** 供 rich-text 渲染的正确答案 HTML */
correctAnswerHtml: string; correctAnswerHtml: string;
/** 纯文本学员答案(选择题等) */ /** 纯文本学员答案(选择题等) */
@ -250,16 +259,106 @@ export const reportDetails: Record<string, ReportDetail> = {
{ difficulty: '难题', range: '19-23', score: '18/24', rate: '75.0%' }, { difficulty: '难题', range: '19-23', score: '18/24', rate: '75.0%' },
], ],
questions: [ 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: 1, type: '选择题', score: '4/4', status: 'correct', stem: '请结合材料完成本题要求。',
{ no: 3, type: '选择题', score: '4/4', status: 'correct', stem: '阅读材料并判断关键条件。', correctAnswer: 'B', studentAnswer: 'B', classRate: '80%', schoolRate: '78%', knowledge: '病句辨析' }, correctAnswer: 'A', studentAnswer: 'A', classRate: '88%', schoolRate: '84%', knowledge: '字音字形',
{ no: 4, type: '选择题', score: '4/4', status: 'correct', stem: '从选项中选出表达准确的一项。', correctAnswer: 'D', studentAnswer: 'D', classRate: '76%', schoolRate: '75%', knowledge: '文学常识' }, options: [
{ no: 5, type: '选择题', score: '4/4', status: 'correct', stem: '根据语境判断标点使用。', correctAnswer: 'A', studentAnswer: 'A', classRate: '72%', schoolRate: '72%', knowledge: '标点符号' }, { label: 'A', value: '脍炙人口' },
{ no: 6, type: '选择题', score: '4/4', status: 'correct', stem: '判断句子修辞手法。', correctAnswer: 'B', studentAnswer: 'B', classRate: '88%', schoolRate: '69%', knowledge: '修辞手法' }, { label: 'B', value: '哄堂大笑' },
{ no: 7, type: '选择题', score: '4/4', status: 'correct', stem: '结合古诗文选择正确理解。', correctAnswer: 'C', studentAnswer: 'C', classRate: '84%', schoolRate: '84%', knowledge: '古诗文默写' }, { label: 'C', value: '锋芒毕露' },
{ no: 8, type: '选择题', score: '4/4', status: 'correct', stem: '阅读片段后概括中心信息。', correctAnswer: 'D', studentAnswer: 'D', classRate: '80%', schoolRate: '81%', knowledge: '名著阅读' }, { label: 'D', value: '忍俊不禁' },
{ 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: 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: 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: 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: '文言断句' }, { no: 13, type: '填空题', score: '5/5', status: 'correct', stem: '判断文言断句位置。', correctAnswer: 'B', studentAnswer: 'B', classRate: '80%', schoolRate: '84%', knowledge: '文言断句' },

View File

@ -6,7 +6,7 @@ import {
getQuestionStemSource, getQuestionStemSource,
isHtmlContent, isHtmlContent,
} from '@/utils/questionHtml'; } from '@/utils/questionHtml';
import type { ReportQuestion, ReportQuestionSummary } from './data'; import type { QuestionOption, ReportQuestion, ReportQuestionSummary } from './data';
export type QuestionStatus = 'correct' | 'wrong' | 'unanswered'; export type QuestionStatus = 'correct' | 'wrong' | 'unanswered';
@ -93,6 +93,73 @@ const formatQuestionScoreText = (item: Record<string, unknown>) => {
return `${actualScore}/${fullScore || actualScore || 0}`; 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<string, unknown>): 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<string, unknown>, index: number): ReportQuestionSummary => { export const mapPaperQuestionSummary = (item: Record<string, unknown>, index: number): ReportQuestionSummary => {
return { return {
no: Number(item.titleSort ?? item.sort ?? index + 1), no: Number(item.titleSort ?? item.sort ?? index + 1),
@ -120,6 +187,7 @@ export const mapPaperQuestion = (item: Record<string, unknown>, index: number):
const answerSource = getQuestionAnswerSource(item); const answerSource = getQuestionAnswerSource(item);
const { studentAnswer, studentAnswerHtml, studentAnswerImages } = buildStudentAnswer(item); const { studentAnswer, studentAnswerHtml, studentAnswerImages } = buildStudentAnswer(item);
const stemImages = extractTitleImages(stemSource); const stemImages = extractTitleImages(stemSource);
const options = parseQuestionOptions(item);
return { return {
no: Number(item.titleSort ?? item.sort ?? index + 1), no: Number(item.titleSort ?? item.sort ?? index + 1),
@ -128,6 +196,7 @@ export const mapPaperQuestion = (item: Record<string, unknown>, index: number):
status, status,
stemHtml: formatQuestionHtml(stemSource, { stripImages: true }), stemHtml: formatQuestionHtml(stemSource, { stripImages: true }),
stemImages, stemImages,
options,
correctAnswerHtml: formatQuestionHtml(answerSource), correctAnswerHtml: formatQuestionHtml(answerSource),
studentAnswer, studentAnswer,
studentAnswerHtml, studentAnswerHtml,
@ -151,6 +220,7 @@ export const mapPaperQuestions = (list: unknown[]) =>
export const normalizeLegacyQuestion = (item: Record<string, unknown>): ReportQuestion => { export const normalizeLegacyQuestion = (item: Record<string, unknown>): ReportQuestion => {
const legacy = item as ReportQuestion & { stem?: string; correctAnswer?: string }; const legacy = item as ReportQuestion & { stem?: string; correctAnswer?: string };
if (legacy.stemHtml) return legacy as ReportQuestion; if (legacy.stemHtml) return legacy as ReportQuestion;
const legacyOptions = Array.isArray((item as any).options) ? (item as any).options : [];
return { return {
no: Number(legacy.no), no: Number(legacy.no),
type: String(legacy.type), type: String(legacy.type),
@ -158,6 +228,7 @@ export const normalizeLegacyQuestion = (item: Record<string, unknown>): ReportQu
status: legacy.status, status: legacy.status,
stemHtml: String(legacy.stem || ''), stemHtml: String(legacy.stem || ''),
stemImages: [], stemImages: [],
options: legacyOptions,
correctAnswerHtml: String(legacy.correctAnswer || ''), correctAnswerHtml: String(legacy.correctAnswer || ''),
studentAnswer: String(legacy.studentAnswer || ''), studentAnswer: String(legacy.studentAnswer || ''),
studentAnswerHtml: '', studentAnswerHtml: '',

View File

@ -252,7 +252,9 @@ const simpleFormulaToHtml = (formula: string): string => {
text = text.replace(/\\pm\b/g, '±'); text = text.replace(/\\pm\b/g, '±');
text = text.replace(/\\times\b/g, '×'); text = text.replace(/\\times\b/g, '×');
text = text.replace(/\\div\b/g, '÷'); text = text.replace(/\\div\b/g, '÷');
text = text.replace(/\\le\b/g, '≤');
text = text.replace(/\\leq\b/g, '≤'); text = text.replace(/\\leq\b/g, '≤');
text = text.replace(/\\ge\b/g, '≥');
text = text.replace(/\\geq\b/g, '≥'); text = text.replace(/\\geq\b/g, '≥');
text = text.replace(/\\neq\b/g, '≠'); text = text.replace(/\\neq\b/g, '≠');
text = text.replace(/\\in\b/g, '∈'); text = text.replace(/\\in\b/g, '∈');