2185 lines
70 KiB
Vue
2185 lines
70 KiB
Vue
<template>
|
||
<view class="question-item">
|
||
<!-- 左侧题目区域 -->
|
||
<scroll-view :class="['left', { expanded: isLeftExpanded }]" scroll-y :show-scrollbar="false">
|
||
<!-- 父题目 -->
|
||
<view v-if="data?.pidTitle" class="left-title">
|
||
<rich-text :nodes="filterTitleWithoutImages(data.pidTitle)" />
|
||
</view>
|
||
<!-- 主题目(优先用 titleMu,为 null 则用 title) -->
|
||
<view class="left-title">
|
||
<rich-text :nodes="filterTitleWithoutImages(data?.titleMu || data?.title)" />
|
||
</view>
|
||
<!-- 题目中的图片单独显示(可点击预览) -->
|
||
<view v-if="titleImages.length" class="title-images">
|
||
<image
|
||
v-for="(img, imgIdx) in titleImages"
|
||
:key="imgIdx"
|
||
:src="img"
|
||
mode="widthFix"
|
||
@click="previewTitleImage(imgIdx)"
|
||
/>
|
||
</view>
|
||
<!-- 音频按钮 -->
|
||
<view v-if="data?.voiceUrl" class="left-voice" @click="playAudio">
|
||
<image
|
||
:src="isAudioPlay
|
||
? `${OSS_URL}/icon/horn_active.svg`
|
||
: `${OSS_URL}/icon/horn.svg`"
|
||
mode="aspectFit"
|
||
/>
|
||
</view>
|
||
<!-- 子题目(非阅读理解 / 非完形填空) -->
|
||
<view v-if="data?.children?.length && questionType !== 'yuedu' && questionType !== 'wanxing'" class="left-children">
|
||
<view v-for="(child, cidx) in data.children" :key="child.id" class="child-title">
|
||
<rich-text :nodes="filterTitleWithoutImages(child.title, cidx)" />
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 阅读理解:子题题干 + 选项(纵向布局) -->
|
||
<view v-if="isYueDu && yueDuList.length" class="left-yuedu">
|
||
<view v-for="(subQ, sIdx) in yueDuList" :key="sIdx" class="left-yuedu-item">
|
||
<view class="left-yuedu-stem">
|
||
<text class="left-yuedu-idx">{{ sIdx + 1 }}.</text>
|
||
<view v-if="subQ.stem" class="left-yuedu-stem-text">
|
||
<rich-text :nodes="subQ.stem" />
|
||
</view>
|
||
</view>
|
||
<view :class="['left-yuedu-opts', { vertical: !subQ.horizontal }]">
|
||
<view v-for="(opt, oIdx) in subQ.options" :key="oIdx" class="left-yuedu-opt">
|
||
<text class="left-yuedu-opt-label">{{ opt.label }}.</text>
|
||
<view class="left-yuedu-opt-value">
|
||
<rich-text :nodes="opt.value" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 完形填空:序号 + 选项(横向紧凑布局,无题干) -->
|
||
<view v-if="isWanXing && yueDuList.length" class="left-wanxing">
|
||
<view v-for="(subQ, sIdx) in yueDuList" :key="sIdx" class="left-wanxing-row">
|
||
<text class="left-wanxing-idx">{{ sIdx + 1 }}</text>
|
||
<view class="left-wanxing-opts">
|
||
<view v-for="(opt, oIdx) in subQ.options" :key="oIdx" class="left-wanxing-opt">
|
||
<text class="left-wanxing-opt-label">{{ opt.label }}.</text>
|
||
<view class="left-wanxing-opt-value">
|
||
<rich-text :nodes="opt.value" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 分隔线 + 折叠按钮 -->
|
||
<view :class="['divider-wrapper', { 'at-right': isLeftExpanded }]">
|
||
<view class="divider"></view>
|
||
<view class="fold-btn" @click="toggleLeftExpand">
|
||
<text>{{ isLeftExpanded ? '收起' : '展开' }}</text>
|
||
<view :class="['fold-arrow', { rotated: !isLeftExpanded }]"></view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 右侧答题区域 -->
|
||
<scroll-view :class="['right', { collapsed: isLeftExpanded }]" scroll-y :show-scrollbar="false">
|
||
<!-- 批改结果 -->
|
||
<view v-if="reportFlag" class="result-box">
|
||
<view :class="['result', resultClass.class]">
|
||
<text>我的答案</text>
|
||
<view class="emoji-box">
|
||
<image :src="`${OSS_URL}/icon/${resultClass.icon}.svg`" mode="aspectFit" />
|
||
<text>{{ resultClass.text }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 答题区域 -->
|
||
<view class="answer-area">
|
||
<!-- 单选题 -->
|
||
<template v-if="questionType === 'single'">
|
||
<view class="choice-list">
|
||
<view
|
||
v-for="(option, oIdx) in optionList"
|
||
:key="oIdx"
|
||
:class="['choice-item', { active: isSelected(option.key), correct: showCorrect(option.key), wrong: showWrong(option.key) }]"
|
||
@click="selectOption(option.key)"
|
||
>
|
||
<view class="choice-key">{{ option.key }}</view>
|
||
<view class="choice-content">
|
||
<rich-text :nodes="option.value" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<!-- 多选题 -->
|
||
<template v-else-if="questionType === 'multiple'">
|
||
<view class="choice-list">
|
||
<view
|
||
v-for="(option, oIdx) in optionList"
|
||
:key="oIdx"
|
||
:class="['choice-item multiple', { active: isMultiSelected(option.key) }]"
|
||
@click="selectMultiOption(option.key)"
|
||
>
|
||
<view class="choice-key">{{ option.key }}</view>
|
||
<view class="choice-content">
|
||
<rich-text :nodes="option.value" />
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<!-- 判断题 -->
|
||
<template v-else-if="questionType === 'judgment'">
|
||
<view class="judgment-list">
|
||
<view
|
||
:class="['judgment-item', { active: submitAnswer === '对' || submitAnswer === 'A' }]"
|
||
@click="selectJudgment('对')"
|
||
>
|
||
<image :src="`${OSS_URL}/icon/icon_correct.svg`" mode="aspectFit" />
|
||
<text>正确</text>
|
||
</view>
|
||
<view
|
||
:class="['judgment-item', { active: submitAnswer === '错' || submitAnswer === 'B' }]"
|
||
@click="selectJudgment('错')"
|
||
>
|
||
<image :src="`${OSS_URL}/icon/icon_wrong.svg`" mode="aspectFit" />
|
||
<text>错误</text>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<!-- 填空题 -->
|
||
<template v-else-if="questionType === 'input'">
|
||
<view class="input-area">
|
||
<view v-for="idx in fillBlankNum" :key="idx" class="input-item">
|
||
<text class="input-label">第{{ idx }}空:</text>
|
||
<input
|
||
class="input-field"
|
||
:value="getInputValue(idx - 1)"
|
||
:disabled="!!reportFlag"
|
||
placeholder="请输入答案"
|
||
@input="onInputChange($event, idx - 1)"
|
||
/>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<!-- 阅读理解 / 完形填空:答题卡 -->
|
||
<template v-else-if="questionType === 'yuedu' || questionType === 'wanxing'">
|
||
<view class="yuedu-answer-card">
|
||
<view class="yuedu-card-title">
|
||
<text>答题卡</text>
|
||
</view>
|
||
<view v-for="(subQ, sIdx) in yueDuList" :key="sIdx" class="yuedu-card-row">
|
||
<text class="yuedu-card-num">{{ sIdx + 1 }}</text>
|
||
<view class="yuedu-card-btns">
|
||
<view
|
||
v-for="(opt, oIdx) in subQ.options"
|
||
:key="oIdx"
|
||
:class="['yuedu-card-btn', {
|
||
active: getYueDuAnswer(sIdx) === opt.label,
|
||
correct: showYueDuCorrect(sIdx, opt.label),
|
||
wrong: showYueDuWrong(sIdx, opt.label)
|
||
}]"
|
||
@click="selectYueDuOption(sIdx, opt.label)"
|
||
>
|
||
<text>{{ opt.label }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<!-- 其他题型(上传图片 - 支持多图) -->
|
||
<template v-else>
|
||
<view class="upload-area">
|
||
<!-- 提示文字 -->
|
||
<view v-if="!reportFlag" class="upload-tip">
|
||
<text class="tip-icon">✏️</text>
|
||
<text>请将答案写在纸上,点击下方“拍照上传答案”按钮拍照上传(最多上传3张图片)</text>
|
||
</view>
|
||
<!-- 已上传的图片列表 -->
|
||
<view v-if="picList.length" class="pic-list">
|
||
<view
|
||
v-for="(pic, pIdx) in picList"
|
||
:key="pIdx"
|
||
class="pic-item"
|
||
>
|
||
<image :src="pic" mode="aspectFill" @click="previewPicList(pIdx)" />
|
||
<view v-if="!reportFlag" class="delete-btn" @click="deletePic(pic)">
|
||
<text>×</text>
|
||
</view>
|
||
</view>
|
||
<!-- 添加更多按钮(最多10张) -->
|
||
<view v-if="!reportFlag && picList.length < 3" class="add-btn" @click="chooseImage">
|
||
<text>+</text>
|
||
</view>
|
||
</view>
|
||
<!-- 未上传时显示上传按钮 -->
|
||
<view v-else-if="!reportFlag" class="upload-btn" @click="chooseImage">
|
||
<!-- 有些自定义 button 组件不会冒泡 click,这里把事件直接绑在按钮上,避免“点了没反应” -->
|
||
<mj-button
|
||
class="upload-btn-button"
|
||
type="primary"
|
||
@click.stop="chooseImage"
|
||
@tap.stop="chooseImage"
|
||
>
|
||
拍照上传答案
|
||
</mj-button>
|
||
</view>
|
||
<!-- 报告模式无图片 -->
|
||
<view v-else class="empty-tip">
|
||
<text>未作答</text>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
</view>
|
||
|
||
<!-- 答案与解析(查看报告时显示) -->
|
||
<template v-if="reportFlag">
|
||
<!-- 参考答案 -->
|
||
<view v-if="data?.answerMu || data?.answer" class="answer-box">
|
||
<view class="answer-box-title">参考答案</view>
|
||
<view class="answer-box-main">
|
||
<rich-text :nodes="filterTitle(data?.answerMu || data?.answer)" />
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 解析 -->
|
||
<view v-if="(data?.titleAnalyzeMu || data?.titleAnalyze) || data?.titleAnalyzePicture" class="answer-box">
|
||
<view class="answer-box-title">解析</view>
|
||
<view v-if="data?.titleAnalyzeMu || data?.titleAnalyze" class="answer-box-main analyze">
|
||
<rich-text :nodes="filterTitle(data?.titleAnalyzeMu || data?.titleAnalyze)" />
|
||
</view>
|
||
<view v-if="data.titleAnalyzePicture" class="answer-box-main analyze-pic">
|
||
<image
|
||
v-for="(pic, pIdx) in data.titleAnalyzePicture.split(',')"
|
||
:key="pIdx"
|
||
:src="pic"
|
||
mode="widthFix"
|
||
@click="previewAnalyzeImage(pic)"
|
||
/>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- AI答案(接口返回 data.aiAnswer 不为空时显示) -->
|
||
<view v-if="data?.aiAnswer != null " class="answer-box">
|
||
<view class="answer-box-title">AI答案</view>
|
||
<view class="answer-box-main analyze">
|
||
<rich-text :nodes="filterTitle(typeof data.aiAnswer === 'string' ? data.aiAnswer : String(data.aiAnswer))" />
|
||
</view>
|
||
</view>
|
||
<!-- AI点评 -->
|
||
<view v-if="data?.aiAnalyze != null " class="answer-box">
|
||
<view class="answer-box-title">AI点评</view>
|
||
<view class="answer-box-main analyze">
|
||
<rich-text :nodes="filterTitle(typeof data.aiAnalyze === 'string' ? data.aiAnalyze : String(data.aiAnalyze))" />
|
||
</view>
|
||
</view>
|
||
</template>
|
||
</scroll-view>
|
||
|
||
<!-- 图片裁剪组件 -->
|
||
<qy-image-cropper
|
||
:visible="showCropper"
|
||
:imageSrc="cropperImageSrc"
|
||
:outputWidth="750"
|
||
:outputHeight="750"
|
||
@update:visible="showCropper = $event"
|
||
@confirm="handleCropConfirm"
|
||
@cancel="handleCropCancel"
|
||
/>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed, watch, onUnmounted } from 'vue';
|
||
import { uploadFile } from '../../../api/upload';
|
||
|
||
const props = defineProps<{
|
||
data?: any;
|
||
idx: number;
|
||
total: number;
|
||
resourceType?: string;
|
||
reportFlag: 0 | 1;
|
||
subjectId?: string;
|
||
}>();
|
||
|
||
const emit = defineEmits(['submit']);
|
||
|
||
const OSS_URL = 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com';
|
||
|
||
// 音频状态
|
||
const isAudioPlay = ref(false);
|
||
let audioContext: any = null;
|
||
|
||
// 左侧展开状态(展开时隐藏右侧答题区域)
|
||
const isLeftExpanded = ref(false);
|
||
|
||
// 切换左侧展开
|
||
const toggleLeftExpand = () => {
|
||
isLeftExpanded.value = !isLeftExpanded.value;
|
||
};
|
||
|
||
|
||
// 从标题中提取图片 URL(仅内容图,排除 formula-img;支持 src 在任意属性位置)
|
||
const titleImages = computed(() => {
|
||
// 优先使用 titleMu,为 null 则用 title
|
||
const title = props.data?.titleMu || props.data?.title || '';
|
||
const pidTitle = props.data?.pidTitle || '';
|
||
const allTitle = pidTitle + title;
|
||
const imgRegex = /<img[^>]*\s*src=["']([^"']+)["'][^>]*\/?>/gi;
|
||
const images: string[] = [];
|
||
let match;
|
||
while ((match = imgRegex.exec(allTitle)) !== null) {
|
||
if (/formula-img/i.test(match[0])) continue;
|
||
const url = (match[1] || '').trim().replace(/http:\/\//gi, 'https://');
|
||
if (url) images.push(url);
|
||
}
|
||
return images;
|
||
});
|
||
|
||
// 过滤:只移除题目内容图,保留 formula-img(后端预渲染的公式图),并处理 u 标签
|
||
const filterTitleWithoutImages = (title?: string, cidx?: number | string) => {
|
||
if (!title) return '';
|
||
// 只移除非公式的 img,保留 class="formula-img" 的公式图(后端已返回带公式图的 HTML)
|
||
let t = title.replace(/<img(?![^>]*formula-img)[^>]*\/?>/gi, '');
|
||
// 处理空的 u 标签,添加 class 以便显示下划线
|
||
t = t.replace(/<u><\/u>/gi, '<u class="fill-blank"></u>');
|
||
t = t.replace(/http:\/\//gi, 'https://');
|
||
// 先修复数学符号(< > 转义、AUB→∪),避免被 HTML 解析截断
|
||
t = fixMathSymbolsInHtml(t);
|
||
// 处理公式(传入失败 URL 集合,触发响应式依赖以便失败后自动重渲染)
|
||
t = processFormula(t, failedFormulaUrls.value);
|
||
|
||
if (cidx !== undefined) {
|
||
t = `(${Number(cidx) + 1})` + t;
|
||
}
|
||
return t;
|
||
};
|
||
|
||
// 预览标题图片
|
||
const previewTitleImage = (index: number) => {
|
||
uni.previewImage({
|
||
urls: titleImages.value,
|
||
current: titleImages.value[index],
|
||
});
|
||
};
|
||
|
||
// 处理 rich-text 点击(小程序中 rich-text 内的图片无法直接点击)
|
||
const handleTitleClick = (e: any, title: string) => {
|
||
// 此处无法直接获取点击的图片,图片预览通过单独的 image 组件实现
|
||
};
|
||
|
||
const handleRichTextClick = (e: any) => {
|
||
// rich-text 点击事件处理
|
||
};
|
||
|
||
// 本地提交答案
|
||
const submitAnswer = ref('');
|
||
const submitAnswerPic = ref('');
|
||
|
||
// ========== 公式图片预加载 + 静默重试 ==========
|
||
// 加载永久失败的公式图片 URL 集合(重试耗尽后加入,触发模板重新渲染用文本兜底)
|
||
const failedFormulaUrls = ref<Set<string>>(new Set());
|
||
|
||
// 监听题目变化,初始化答案
|
||
watch(
|
||
() => props.data?.id,
|
||
() => {
|
||
submitAnswer.value = props.data?.submitAnswer || '';
|
||
submitAnswerPic.value = props.data?.submitAnswerPic || '';
|
||
// 停止音频
|
||
if (audioContext) {
|
||
audioContext.stop();
|
||
isAudioPlay.value = false;
|
||
}
|
||
},
|
||
{ immediate: true },
|
||
);
|
||
|
||
// 修复题目中的数学符号:避免 < > 被当作 HTML 解析导致内容丢失,并将 AUB 等显示为正确符号
|
||
const fixMathSymbolsInHtml = (html: string): string => {
|
||
if (html == null || typeof html !== 'string') return '';
|
||
let t = html;
|
||
// 小号连字符/减号 ﹣ (U+FE63) 显示过小,统一替换为正常大小的减号 − (U+2212)
|
||
t = t.replace(/\uFE63/g, '\u2212');
|
||
// 将非标签的 < 转义,避免 " -2 < x < 2" 等被当作标签截断
|
||
t = t.replace(/<(?![a-zA-Z/!])/g, '<');
|
||
// 将数学比较中的 > 转义(仅空格包围的情况,避免破坏标签)
|
||
t = t.replace(/ > /g, ' > ');
|
||
// 集合并集:AUB -> A∪B(题目中常写作 AUB,需显示为并集符号 ∪)
|
||
t = t.replace(/AUB/g, 'A∪B');
|
||
// 集合写法 [x| 有时被截断,确保 ≤ ≥ 不被误解析(已是单字符,一般无问题)
|
||
return t;
|
||
};
|
||
|
||
// LaTeX 渲染服务 URL(使用 CodeCogs 在线渲染)
|
||
const LATEX_RENDER_URL = 'https://latex.codecogs.com/svg.image?';
|
||
|
||
// 将 LaTeX 公式转为图片 URL
|
||
const latexToImageUrl = (latex: string): string => {
|
||
if (!latex) return '';
|
||
|
||
// 预处理:将非标准 LaTeX 写法转换为标准格式
|
||
let formula = latex.trim();
|
||
|
||
// 中文标点符号映射表(完整覆盖所有可能的中文标点)
|
||
const punctuationMap: Record<string, string> = {
|
||
',': ',', // 逗号
|
||
'。': '.', // 句号
|
||
':': ':', // 冒号
|
||
';': ';', // 分号
|
||
'!': '!', // 感叹号
|
||
'?': '?', // 问号
|
||
'(': '(', // 左括号
|
||
')': ')', // 右括号
|
||
'【': '[', // 左方括号
|
||
'】': ']', // 右方括号
|
||
'{': '{', // 左花括号
|
||
'}': '}', // 右花括号
|
||
'《': '<', // 左书名号
|
||
'》': '>', // 右书名号
|
||
"\u201c": '"', // 左双引号 "
|
||
"\u201d": '"', // 右双引号 "
|
||
"\u2018": "'", // 左单引号 '
|
||
"\u2019": "'", // 右单引号 '
|
||
'+': '+', // 加号
|
||
'-': '-', // 全角减号
|
||
'\u2212': '-', // Unicode 数学减号 −(MINUS SIGN)
|
||
'×': '*', // 乘号(转为星号,LaTeX 中用 \times)
|
||
'÷': '/', // 除号(转为斜杠)
|
||
'=': '=', // 等号
|
||
'<': '<', // 小于号
|
||
'>': '>', // 大于号
|
||
'%': '%', // 百分号
|
||
'&': '&', // 和号
|
||
'*': '*', // 星号
|
||
'@': '@', // at符号
|
||
'#': '#', // 井号
|
||
'$': '$', // 美元符号
|
||
'^': '^', // 脱字符
|
||
'~': '~', // 波浪号
|
||
'|': '|', // 竖线
|
||
'\': '\\', // 反斜杠
|
||
'/': '/', // 斜杠
|
||
' ': ' ', // 全角空格
|
||
};
|
||
|
||
// 批量替换中文标点
|
||
for (const [cn, en] of Object.entries(punctuationMap)) {
|
||
formula = formula.split(cn).join(en);
|
||
}
|
||
|
||
// Unicode 数学符号到 LaTeX 的转换映射
|
||
const mathSymbolMap: Record<string, string> = {
|
||
// 集合符号
|
||
'∈': ' \\in ', // 属于
|
||
'∉': ' \\notin ', // 不属于
|
||
'⊂': ' \\subset ', // 真子集
|
||
'⊃': ' \\supset ', // 真超集
|
||
'⊆': ' \\subseteq ', // 子集
|
||
'⊇': ' \\supseteq ', // 超集
|
||
'∪': ' \\cup ', // 并集
|
||
'∩': ' \\cap ', // 交集
|
||
'∅': ' \\emptyset ', // 空集
|
||
// 比较符号
|
||
'≤': ' \\leq ', // 小于等于
|
||
'≥': ' \\geq ', // 大于等于
|
||
'≠': ' \\neq ', // 不等于
|
||
'≈': ' \\approx ', // 约等于
|
||
'≡': ' \\equiv ', // 恒等于
|
||
'∝': ' \\propto ', // 正比于
|
||
// 运算符号
|
||
'±': ' \\pm ', // 正负
|
||
'∓': ' \\mp ', // 负正
|
||
'×': ' \\times ', // 乘号
|
||
'÷': ' \\div ', // 除号
|
||
'·': ' \\cdot ', // 点乘
|
||
'∘': ' \\circ ', // 复合
|
||
// 希腊字母
|
||
'α': '\\alpha ',
|
||
'β': '\\beta ',
|
||
'γ': '\\gamma ',
|
||
'δ': '\\delta ',
|
||
'ε': '\\varepsilon ',
|
||
'θ': '\\theta ',
|
||
'λ': '\\lambda ',
|
||
'μ': '\\mu ',
|
||
'σ': '\\sigma ',
|
||
'φ': '\\varphi ',
|
||
'ω': '\\omega ',
|
||
'Δ': '\\Delta ',
|
||
'Σ': '\\Sigma ',
|
||
'Ω': '\\Omega ',
|
||
// 其他常用符号
|
||
'∞': '\\infty ', // 无穷
|
||
'∂': '\\partial ', // 偏导
|
||
'∇': '\\nabla ', // 梯度
|
||
'∫': '\\int ', // 积分
|
||
'∑': '\\sum ', // 求和
|
||
'∏': '\\prod ', // 连乘
|
||
'√': '\\sqrt ', // 根号
|
||
'∠': '\\angle ', // 角
|
||
'⊥': '\\perp ', // 垂直
|
||
'∥': '\\parallel ', // 平行
|
||
'→': '\\to ', // 箭头
|
||
'⇒': '\\Rightarrow ', // 双线箭头
|
||
'⇔': '\\Leftrightarrow ', // 双向箭头
|
||
'∀': '\\forall ', // 任意
|
||
'∃': '\\exists ', // 存在
|
||
'¬': '\\neg ', // 非
|
||
'∧': '\\land ', // 与
|
||
'∨': '\\lor ', // 或
|
||
};
|
||
|
||
// 批量替换数学符号
|
||
for (const [symbol, latex] of Object.entries(mathSymbolMap)) {
|
||
formula = formula.split(symbol).join(latex);
|
||
}
|
||
|
||
// 处理 R+ 等集合表示(R+ → R^+)
|
||
formula = formula.replace(/\b([RNZQC])\+/g, '$1^+');
|
||
formula = formula.replace(/\b([RNZQC])\*/g, '$1^*');
|
||
|
||
formula = formula
|
||
// \overset{\rightarrow}{x} -> \vec{x}(简化向量表示)
|
||
.replace(/\\overset\s*\{\s*\\rightarrow\s*\}\s*\{([^}]+)\}/g, '\\vec{$1}')
|
||
// \text{π} -> \pi
|
||
.replace(/\\text\s*\{\s*π\s*\}/g, '\\pi')
|
||
// \text{sin/cos/tan/log} -> \sin/\cos/\tan/\log(标准 LaTeX 函数)
|
||
.replace(/\\text\s*\{\s*sin\s*\}/gi, '\\sin')
|
||
.replace(/\\text\s*\{\s*cos\s*\}/gi, '\\cos')
|
||
.replace(/\\text\s*\{\s*tan\s*\}/gi, '\\tan')
|
||
.replace(/\\text\s*\{\s*log\s*\}/gi, '\\log')
|
||
// 直接的 Unicode π 转为 \pi
|
||
.replace(/π/g, '\\pi');
|
||
|
||
// 对公式进行 URL 编码
|
||
const encoded = encodeURIComponent(formula);
|
||
return `${LATEX_RENDER_URL}\\inline&space;\\dpi{200}&space;${encoded}`;
|
||
};
|
||
|
||
// 将一段公式内的简单 LaTeX 转为可读文本(三角函数、希腊字母、括号等),供整段或 \frac 分子分母使用
|
||
const applySimpleLatex = (s: string): string => {
|
||
if (!s) return s;
|
||
let t = s.trim();
|
||
// \text{sin/cos/tan/log} -> sin/cos/tan/log
|
||
t = t.replace(/\\text\s*\{\s*sin\s*\}/gi, 'sin');
|
||
t = t.replace(/\\text\s*\{\s*cos\s*\}/gi, 'cos');
|
||
t = t.replace(/\\text\s*\{\s*tan\s*\}/gi, 'tan');
|
||
t = t.replace(/\\text\s*\{\s*log\s*\}/gi, 'log');
|
||
t = t.replace(/\\text\s*\{\s*π\s*\}/g, 'π');
|
||
t = t.replace(/\\pi\b/g, 'π');
|
||
// 希腊字母
|
||
t = t.replace(/\\alpha\b/g, 'α');
|
||
t = t.replace(/\\beta\b/g, 'β');
|
||
t = t.replace(/\\gamma\b/g, 'γ');
|
||
t = t.replace(/\\theta\b/g, 'θ');
|
||
t = t.replace(/\\Delta\b/g, 'Δ');
|
||
// \left( \right) 等
|
||
t = t.replace(/\\left\s*\(/g, '(');
|
||
t = t.replace(/\\right\s*\)/g, ')');
|
||
t = t.replace(/\\left\s*\[/g, '[');
|
||
t = t.replace(/\\right\s*\]/g, ']');
|
||
t = t.replace(/\\left\s*\|/g, '|');
|
||
t = t.replace(/\\right\s*\|/g, '|');
|
||
// 去掉剩余反斜杠(如 \text 中未匹配到的)
|
||
t = t.replace(/\\/g, '');
|
||
return t;
|
||
};
|
||
|
||
// 判断公式是否为简单公式(可以直接用文本显示,不需要图片渲染)
|
||
const isSimpleFormula = (formula: string): boolean => {
|
||
// 需要图片渲染的复杂 LaTeX 命令
|
||
const complexPatterns = [
|
||
/\\frac\b/, // 分数
|
||
/\\sqrt\b/, // 根号
|
||
/\\sum\b/, // 求和
|
||
/\\int\b/, // 积分
|
||
/\\prod\b/, // 连乘
|
||
/\\lim\b/, // 极限
|
||
/\\vec\b/, // 向量
|
||
/\\overline\b/, // 上划线
|
||
/\\underline\b/, // 下划线
|
||
/\\overset\b/, // 上标符号
|
||
/\\underset\b/, // 下标符号
|
||
/\\binom\b/, // 二项式
|
||
/\\matrix\b/, // 矩阵
|
||
/\\begin\b/, // 环境
|
||
/\^{[^}]+}/, // 复杂上标(如 x^{2n})
|
||
/_{[^}]+}/, // 复杂下标(如 a_{n+1})
|
||
];
|
||
|
||
return !complexPatterns.some(pattern => pattern.test(formula));
|
||
};
|
||
|
||
// 将简单公式转换为可显示的 HTML 文本
|
||
const simpleFormulaToHtml = (formula: string): string => {
|
||
let text = formula;
|
||
|
||
// 处理 \text{} 命令
|
||
text = text.replace(/\\text\s*\{([^}]*)\}/g, '$1');
|
||
|
||
// 希腊字母转换
|
||
const greekMap: Record<string, string> = {
|
||
'\\alpha': 'α', '\\beta': 'β', '\\gamma': 'γ', '\\delta': 'δ',
|
||
'\\epsilon': 'ε', '\\varepsilon': 'ε', '\\theta': 'θ', '\\lambda': 'λ',
|
||
'\\mu': 'μ', '\\pi': 'π', '\\sigma': 'σ', '\\phi': 'φ', '\\varphi': 'φ',
|
||
'\\omega': 'ω', '\\Delta': 'Δ', '\\Sigma': 'Σ', '\\Omega': 'Ω',
|
||
};
|
||
for (const [latex, symbol] of Object.entries(greekMap)) {
|
||
text = text.split(latex).join(symbol);
|
||
}
|
||
|
||
// 数学函数
|
||
text = text.replace(/\\sin\b/g, 'sin');
|
||
text = text.replace(/\\cos\b/g, 'cos');
|
||
text = text.replace(/\\tan\b/g, 'tan');
|
||
text = text.replace(/\\log\b/g, 'log');
|
||
text = text.replace(/\\ln\b/g, 'ln');
|
||
|
||
// 符号转换
|
||
text = text.replace(/\\infty\b/g, '∞');
|
||
text = text.replace(/\\pm\b/g, '±');
|
||
text = text.replace(/\\times\b/g, '×');
|
||
text = text.replace(/\\div\b/g, '÷');
|
||
text = text.replace(/\\leq\b/g, '≤');
|
||
text = text.replace(/\\geq\b/g, '≥');
|
||
text = text.replace(/\\neq\b/g, '≠');
|
||
text = text.replace(/\\in\b/g, '∈');
|
||
text = text.replace(/\\subset\b/g, '⊂');
|
||
text = text.replace(/\\cup\b/g, '∪');
|
||
text = text.replace(/\\cap\b/g, '∩');
|
||
text = text.replace(/\\emptyset\b/g, '∅');
|
||
text = text.replace(/\\cdot\b/g, '·');
|
||
text = text.replace(/\\to\b/g, '→');
|
||
|
||
// 处理简单上下标:x^2 -> x², a_n -> aₙ
|
||
const superscriptMap: Record<string, string> = {
|
||
'0': '⁰', '1': '¹', '2': '²', '3': '³', '4': '⁴',
|
||
'5': '⁵', '6': '⁶', '7': '⁷', '8': '⁸', '9': '⁹',
|
||
'+': '⁺', '-': '⁻', 'n': 'ⁿ',
|
||
};
|
||
const subscriptMap: Record<string, string> = {
|
||
'0': '₀', '1': '₁', '2': '₂', '3': '₃', '4': '₄',
|
||
'5': '₅', '6': '₆', '7': '₇', '8': '₈', '9': '₉',
|
||
'+': '₊', '-': '₋', 'n': 'ₙ', 'i': 'ᵢ', 'k': 'ₖ',
|
||
};
|
||
|
||
// 简单上标 x^2
|
||
text = text.replace(/\^([0-9n+\-])/g, (_, char) => superscriptMap[char] || `^${char}`);
|
||
// 简单下标 a_n
|
||
text = text.replace(/_([0-9nik+\-])/g, (_, char) => subscriptMap[char] || `_${char}`);
|
||
|
||
// 移除 \left \right
|
||
text = text.replace(/\\left\s*/g, '');
|
||
text = text.replace(/\\right\s*/g, '');
|
||
|
||
// 移除剩余的反斜杠
|
||
text = text.replace(/\\/g, '');
|
||
|
||
return `<span class="formula-text" style="font-style:italic;">${text}</span>`;
|
||
};
|
||
|
||
// 处理数学公式:使用在线 LaTeX 渲染服务
|
||
// failedUrls: 可选,加载永久失败的 URL 集合;命中时用文本兜底而非 <img>
|
||
const processFormula = (html: string, failedUrls?: Set<string>): string => {
|
||
if (html == null || typeof html !== 'string') return '';
|
||
if (!html) return '';
|
||
|
||
// 匹配 <span ... data-value="..." ... data-w-e-type="formula" ...></span> 格式
|
||
// 使用能正确处理属性值中包含 > 符号的正则(如 "a> 0")
|
||
const formulaRegex = /<span\s+((?:[^<>"']+|"[^"]*"|'[^']*')*)data-w-e-type=["']formula["']((?:[^<>"']+|"[^"]*"|'[^']*')*)>[\s\S]*?<\/span>/gi;
|
||
|
||
return html.replace(formulaRegex, (match) => {
|
||
const valueMatch = match.match(/data-value=["']([^"']+)["']/);
|
||
if (!valueMatch) return match;
|
||
|
||
let formula = valueMatch[1]
|
||
.replace(/\\\\/g, '\\')
|
||
.replace(/\\"/g, '"')
|
||
.replace(/\\'/g, "'");
|
||
|
||
// 如果公式为空或只有空格,返回空
|
||
if (!formula.trim()) return '';
|
||
|
||
// 判断是否为简单公式
|
||
if (isSimpleFormula(formula)) {
|
||
// 简单公式直接转为文本显示
|
||
return simpleFormulaToHtml(formula);
|
||
}
|
||
|
||
// 复杂公式使用 LaTeX 图片渲染
|
||
const imageUrl = latexToImageUrl(formula);
|
||
|
||
// 如果该图片经过多次重试仍失败,使用 LaTeX 文本兜底
|
||
if (failedUrls?.has(imageUrl)) {
|
||
const display = formula.length > 60 ? formula.substring(0, 60) + '…' : formula;
|
||
return `<span class="formula-text">[${display}]</span>`;
|
||
}
|
||
|
||
return `<img class="formula-img" src="${imageUrl}" style="height:1.4em;vertical-align:middle;" />`;
|
||
});
|
||
};
|
||
|
||
// ---------- 公式图片预加载工具函数 ----------
|
||
|
||
// 从原始 HTML 中提取所有复杂公式对应的图片 URL(逻辑与 processFormula 一致)
|
||
const extractFormulaImageUrls = (html: string): string[] => {
|
||
if (html == null || typeof html !== 'string') return [];
|
||
const formulaRegex = /<span\s+((?:[^<>"']+|"[^"]*"|'[^']*')*)data-w-e-type=["']formula["']((?:[^<>"']+|"[^"]*"|'[^']*')*)>[\s\S]*?<\/span>/gi;
|
||
const urls: string[] = [];
|
||
let match;
|
||
while ((match = formulaRegex.exec(html)) !== null) {
|
||
const valueMatch = match[0].match(/data-value=["']([^"']+)["']/);
|
||
if (!valueMatch) continue;
|
||
let formula = valueMatch[1]
|
||
.replace(/\\\\/g, '\\')
|
||
.replace(/\\"/g, '"')
|
||
.replace(/\\'/g, "'");
|
||
if (!formula.trim()) continue;
|
||
if (isSimpleFormula(formula)) continue;
|
||
urls.push(latexToImageUrl(formula));
|
||
}
|
||
return urls;
|
||
};
|
||
|
||
// 单张图片预加载,失败时自动重试(递增延迟 1s → 2s → 3s)
|
||
const preloadImageWithRetry = (url: string, maxRetries = 3): Promise<boolean> => {
|
||
return new Promise((resolve) => {
|
||
let attempt = 0;
|
||
const tryLoad = () => {
|
||
uni.getImageInfo({
|
||
src: url,
|
||
success: () => resolve(true),
|
||
fail: () => {
|
||
attempt++;
|
||
if (attempt < maxRetries) {
|
||
setTimeout(tryLoad, attempt * 1000);
|
||
} else {
|
||
resolve(false);
|
||
}
|
||
},
|
||
});
|
||
};
|
||
tryLoad();
|
||
});
|
||
};
|
||
|
||
// 收集题目中所有可能含公式的原始 HTML(题干、选项、答案、解析等)
|
||
const collectAllRawHtml = (data: any): string[] => {
|
||
if (!data) return [];
|
||
const htmls: string[] = [];
|
||
if (data.pidTitle) htmls.push(data.pidTitle);
|
||
if (data.titleMu || data.title) htmls.push(data.titleMu || data.title);
|
||
// 子题目
|
||
if (data.children?.length) {
|
||
data.children.forEach((c: any) => { if (c.title) htmls.push(c.title); });
|
||
}
|
||
// 选项(支持平铺和嵌套两种结构)
|
||
try {
|
||
let opts = data.optionsMu ?? data.options;
|
||
if (typeof opts === 'string') opts = JSON.parse(opts);
|
||
if (Array.isArray(opts)) {
|
||
opts.forEach((item: any) => {
|
||
if (typeof item === 'string') {
|
||
if (item) htmls.push(item);
|
||
} else if (item?.stem || item?.options) {
|
||
// 阅读理解/完形填空:嵌套结构 { stem, options: [...] }
|
||
if (item.stem) htmls.push(String(item.stem));
|
||
if (Array.isArray(item.options)) {
|
||
item.options.forEach((sub: any) => {
|
||
const sv = typeof sub === 'string' ? sub : (sub?.value ?? sub?.optionValue ?? '');
|
||
if (sv) htmls.push(String(sv));
|
||
});
|
||
}
|
||
} else {
|
||
const v = item?.value ?? item?.optionValue ?? '';
|
||
if (v) htmls.push(String(v));
|
||
}
|
||
});
|
||
}
|
||
} catch { /* ignore parse error */ }
|
||
// 答案、解析、AI 等
|
||
if (data.answerMu || data.answer) htmls.push(data.answerMu || data.answer);
|
||
if (data.titleAnalyzeMu || data.titleAnalyze) htmls.push(data.titleAnalyzeMu || data.titleAnalyze);
|
||
if (data.aiAnswer != null) htmls.push(String(data.aiAnswer));
|
||
if (data.aiAnalyze != null) htmls.push(String(data.aiAnalyze));
|
||
return htmls;
|
||
};
|
||
|
||
// 监听题目数据变化,后台预加载所有公式图片,失败自动重试,彻底失败后切换文本兜底
|
||
let preloadGeneration = 0;
|
||
watch(
|
||
() => props.data,
|
||
async (newData) => {
|
||
const generation = ++preloadGeneration;
|
||
// 新题目先清空失败集合(乐观渲染,先展示图片)
|
||
failedFormulaUrls.value = new Set();
|
||
|
||
if (!newData) return;
|
||
|
||
// 收集题目所有原始 HTML,提取其中的公式图片 URL
|
||
const allUrls = new Set<string>();
|
||
for (const html of collectAllRawHtml(newData)) {
|
||
const processed = fixMathSymbolsInHtml(html);
|
||
extractFormulaImageUrls(processed).forEach((url) => allUrls.add(url));
|
||
}
|
||
|
||
if (allUrls.size === 0) return;
|
||
|
||
// 并发预加载每张公式图片(每张最多重试 3 次,间隔递增)
|
||
const results = await Promise.all(
|
||
Array.from(allUrls).map(async (url) => ({
|
||
url,
|
||
ok: await preloadImageWithRetry(url, 3),
|
||
})),
|
||
);
|
||
|
||
// 如果用户已切到下一题(generation 过期),丢弃本次结果
|
||
if (generation !== preloadGeneration) return;
|
||
|
||
const failed = results.filter((r) => !r.ok).map((r) => r.url);
|
||
if (failed.length > 0) {
|
||
console.warn(`[Question] ${failed.length} 张公式图片加载失败,已切换为文本兜底`, failed);
|
||
// 设置新 Set 触发响应式更新 → filterTitleWithoutImages / optionList / filterTitle 重新求值 → 失败图片用文本替代
|
||
failedFormulaUrls.value = new Set(failed);
|
||
}
|
||
},
|
||
{ immediate: true },
|
||
);
|
||
|
||
// 选项列表 - 单选题/多选题:仅用 optionsMu 或 options,不用 optionList
|
||
// options/optionsMu 可能是 JSON 字符串,需先解析为数组
|
||
const optionList = computed(() => {
|
||
const data = props.data;
|
||
let raw =
|
||
data?.optionsMu != null
|
||
? data.optionsMu
|
||
: data?.options != null
|
||
? data.options
|
||
: null;
|
||
|
||
// options/optionsMu 为 JSON 字符串时解析为数组
|
||
if (typeof raw === 'string') {
|
||
try {
|
||
raw = JSON.parse(raw);
|
||
} catch (e) {
|
||
console.warn('optionList: options/optionsMu JSON 解析失败', e);
|
||
raw = null;
|
||
}
|
||
}
|
||
|
||
const list = Array.isArray(raw) ? raw : raw ? [raw] : [];
|
||
if (list.length === 0) return [];
|
||
|
||
return list.map((item: any, idx: number) => {
|
||
let value = '';
|
||
|
||
if (typeof item === 'string') {
|
||
value = String(item || '');
|
||
} else {
|
||
const v = item?.value ?? item?.optionValue ?? item;
|
||
value = v != null && v !== '' ? String(v) : '';
|
||
}
|
||
|
||
value = fixMathSymbolsInHtml(value);
|
||
value = processFormula(value, failedFormulaUrls.value);
|
||
value = typeof value === 'string' ? value.replace(/http:\/\//gi, 'https://') : '';
|
||
// 选项内容为空时显示占位,避免只看到选项字母没有内容
|
||
// if (!value || (typeof value === 'string' && !value.trim())) {
|
||
// value = '';
|
||
// }
|
||
|
||
return {
|
||
key:
|
||
typeof item === 'string'
|
||
? String.fromCharCode(65 + idx)
|
||
: (item.key || item.optionKey || item.label || String.fromCharCode(65 + idx)),
|
||
value: value,
|
||
};
|
||
});
|
||
});
|
||
|
||
// 填空数量
|
||
const fillBlankNum = computed(() => {
|
||
return props.data?.fillBlankNum || 0;
|
||
});
|
||
|
||
// ========== 阅读理解 / 完形填空检测 ==========
|
||
|
||
// 解析原始 options 数据(保留嵌套结构,不做展平),与 admin 端 normalizedOptions 对齐
|
||
const rawParsedOptions = computed(() => {
|
||
const data = props.data;
|
||
let raw = data?.optionsMu != null ? data.optionsMu
|
||
: data?.options != null ? data.options : null;
|
||
if (typeof raw === 'string') {
|
||
try { raw = JSON.parse(raw); } catch { raw = null; }
|
||
}
|
||
if (!Array.isArray(raw)) return [];
|
||
// 解包多余嵌套 [[...]] → [...]
|
||
let list = raw;
|
||
while (Array.isArray(list) && list.length === 1 && Array.isArray(list[0])) {
|
||
list = list[0];
|
||
}
|
||
return list;
|
||
});
|
||
|
||
// 是否为阅读理解(选项含 stem + options 子结构)
|
||
const isYueDu = computed(() => {
|
||
const list = rawParsedOptions.value;
|
||
if (!list.length) return false;
|
||
return list.some((item: any) => item?.options && item?.stem);
|
||
});
|
||
|
||
// 是否为完形填空(选项含 options 子结构但无 stem)
|
||
const isWanXing = computed(() => {
|
||
if (isYueDu.value) return false;
|
||
const list = rawParsedOptions.value;
|
||
if (!list.length) return false;
|
||
return list.some((item: any) => item?.options && !item?.stem);
|
||
});
|
||
|
||
// 阅读理解 / 完形填空:子题列表(统一格式:{ index, stem, options: [{label, value}] })
|
||
const yueDuList = computed(() => {
|
||
if (!isYueDu.value && !isWanXing.value) return [];
|
||
return rawParsedOptions.value.map((item: any, idx: number) => {
|
||
// 子选项
|
||
const rawSubOpts = Array.isArray(item.options) ? item.options : [];
|
||
const subOpts = rawSubOpts.map((opt: any, oIdx: number) => {
|
||
let v = typeof opt === 'string' ? opt : (opt?.value ?? opt?.optionValue ?? '');
|
||
v = v != null ? String(v) : '';
|
||
v = fixMathSymbolsInHtml(v);
|
||
v = processFormula(v, failedFormulaUrls.value);
|
||
v = v.replace(/http:\/\//gi, 'https://');
|
||
return {
|
||
label: (typeof opt === 'string') ? String.fromCharCode(65 + oIdx) : (opt?.label || opt?.optionKey || String.fromCharCode(65 + oIdx)),
|
||
value: v,
|
||
};
|
||
});
|
||
// 子题题干
|
||
let stem = '';
|
||
if (item.stem) {
|
||
stem = String(item.stem);
|
||
stem = fixMathSymbolsInHtml(stem);
|
||
stem = processFormula(stem, failedFormulaUrls.value);
|
||
stem = stem.replace(/http:\/\//gi, 'https://');
|
||
}
|
||
// 判断选项是否适合水平排列(所有选项文本长度 < 10 个字符)
|
||
let horizontal = false;
|
||
if (subOpts.length > 0) {
|
||
const maxLen = Math.max(...subOpts.map((o: any) => String(o.value || '').replace(/<[^>]*>/g, '').length));
|
||
horizontal = maxLen < 10;
|
||
}
|
||
return { index: idx, stem, options: subOpts, horizontal };
|
||
});
|
||
});
|
||
|
||
// 题目类型
|
||
const questionType = computed(() => {
|
||
const data = props.data;
|
||
if (!data) return 'other';
|
||
|
||
// 阅读理解(优先判断,避免被单选题误匹配)
|
||
if (isYueDu.value) return 'yuedu';
|
||
|
||
// 完形填空
|
||
if (isWanXing.value) return 'wanxing';
|
||
|
||
// 判断题
|
||
if (['判断', '判断题'].includes(data.titleChannelTypeName)) {
|
||
return 'judgment';
|
||
}
|
||
|
||
// 多选题
|
||
if (data.titleChannelTypeName === '多选题') {
|
||
return 'multiple';
|
||
}
|
||
|
||
// 填空题
|
||
if (data.fillBlankNum) {
|
||
return 'input';
|
||
}
|
||
|
||
// 单选题
|
||
if (optionList.value.length > 0) {
|
||
return 'single';
|
||
}
|
||
|
||
return 'other';
|
||
});
|
||
|
||
// 批改结果
|
||
const resultClass = computed(() => {
|
||
const data = props.data;
|
||
if (data?.correctResult || data?.markFlag) {
|
||
return {
|
||
class: 'success',
|
||
text: '答对啦',
|
||
icon: 'emoji_success',
|
||
};
|
||
}
|
||
// 同时检查文字答案和图片答案
|
||
const hasAnswer = data?.submitAnswer || data?.submitAnswerPic;
|
||
return {
|
||
class: 'error',
|
||
text: hasAnswer ? '答错了' : '未作答',
|
||
icon: 'emoji_fail',
|
||
};
|
||
});
|
||
|
||
// 过滤标题内容(解析等场景)
|
||
const filterTitle = (title?: string, cidx?: number | string) => {
|
||
if (!title) return '';
|
||
let t = title.replace(/http:\/\//gi, 'https://');
|
||
t = fixMathSymbolsInHtml(t);
|
||
t = processFormula(t, failedFormulaUrls.value);
|
||
if (cidx !== undefined) {
|
||
t = `(${Number(cidx) + 1})` + t;
|
||
}
|
||
return t;
|
||
};
|
||
|
||
// 单选题相关
|
||
const isSelected = (key: string) => {
|
||
return submitAnswer.value === key;
|
||
};
|
||
|
||
const showCorrect = (key: string) => {
|
||
if (!props.reportFlag) return false;
|
||
return props.data?.answer?.includes(key);
|
||
};
|
||
|
||
const showWrong = (key: string) => {
|
||
if (!props.reportFlag) return false;
|
||
return submitAnswer.value === key && !props.data?.answer?.includes(key);
|
||
};
|
||
|
||
const selectOption = (key: string) => {
|
||
if (props.reportFlag) return;
|
||
submitAnswer.value = key;
|
||
emit('submit', { submitAnswer: key, toNext: true });
|
||
};
|
||
|
||
// 多选题相关
|
||
const isMultiSelected = (key: string) => {
|
||
return submitAnswer.value.includes(key);
|
||
};
|
||
|
||
const selectMultiOption = (key: string) => {
|
||
if (props.reportFlag) return;
|
||
let answer = submitAnswer.value;
|
||
if (answer.includes(key)) {
|
||
answer = answer.replace(key, '');
|
||
} else {
|
||
answer += key;
|
||
}
|
||
// 排序
|
||
answer = answer.split('').sort().join('');
|
||
submitAnswer.value = answer;
|
||
emit('submit', { submitAnswer: answer });
|
||
};
|
||
|
||
// 判断题
|
||
const selectJudgment = (value: string) => {
|
||
if (props.reportFlag) return;
|
||
submitAnswer.value = value;
|
||
emit('submit', { submitAnswer: value, toNext: true });
|
||
};
|
||
|
||
// ========== 阅读理解 / 完形填空答题 ==========
|
||
|
||
// 将 submitAnswer(逗号分隔字符串)拆为每个子题的答案数组
|
||
const yueDuAnswerList = computed(() => {
|
||
if (!submitAnswer.value) return [];
|
||
return submitAnswer.value.split(',');
|
||
});
|
||
|
||
// 获取某个子题的已选答案
|
||
const getYueDuAnswer = (sIdx: number): string => {
|
||
return yueDuAnswerList.value[sIdx] || '';
|
||
};
|
||
|
||
// 选择阅读理解某个子题的答案
|
||
const selectYueDuOption = (sIdx: number, key: string) => {
|
||
if (props.reportFlag) return;
|
||
const answers = [...yueDuAnswerList.value];
|
||
// 确保数组长度覆盖当前子题索引
|
||
while (answers.length <= sIdx) answers.push('');
|
||
answers[sIdx] = key;
|
||
submitAnswer.value = answers.join(',');
|
||
emit('submit', { submitAnswer: submitAnswer.value });
|
||
};
|
||
|
||
// 报告模式:解析正确答案为数组
|
||
const correctAnswerList = computed(() => {
|
||
const raw = props.data?.answer;
|
||
if (!raw) return [];
|
||
if (Array.isArray(raw)) return raw.map(String);
|
||
if (typeof raw === 'string') {
|
||
// 尝试 JSON 解析 ["A","B"]
|
||
try {
|
||
const parsed = JSON.parse(raw);
|
||
if (Array.isArray(parsed)) return parsed.map(String);
|
||
} catch { /* not JSON */ }
|
||
// 逗号分隔 "A,B,C"
|
||
return raw.split(',').map((s: string) => s.trim());
|
||
}
|
||
return [];
|
||
});
|
||
|
||
// 报告模式:某子题的某选项是否为正确答案
|
||
const showYueDuCorrect = (sIdx: number, key: string): boolean => {
|
||
if (!props.reportFlag) return false;
|
||
return correctAnswerList.value[sIdx] === key;
|
||
};
|
||
|
||
// 报告模式:某子题的某选项是否为错选
|
||
const showYueDuWrong = (sIdx: number, key: string): boolean => {
|
||
if (!props.reportFlag) return false;
|
||
const myAnswer = yueDuAnswerList.value[sIdx];
|
||
const correctAnswer = correctAnswerList.value[sIdx];
|
||
return myAnswer === key && myAnswer !== correctAnswer;
|
||
};
|
||
|
||
// 填空题
|
||
const getInputValue = (idx: number) => {
|
||
const answers = submitAnswer.value.split(',');
|
||
return answers[idx] || '';
|
||
};
|
||
|
||
const onInputChange = (e: any, idx: number) => {
|
||
const value = e.detail.value;
|
||
const answers = submitAnswer.value.split(',');
|
||
while (answers.length <= idx) {
|
||
answers.push('');
|
||
}
|
||
answers[idx] = value;
|
||
submitAnswer.value = answers.join(',');
|
||
emit('submit', { submitAnswer: submitAnswer.value });
|
||
};
|
||
|
||
// 图片列表(支持多图,逗号分隔)
|
||
const picList = computed(() => {
|
||
if (!submitAnswerPic.value) return [];
|
||
return submitAnswerPic.value
|
||
.split(',')
|
||
.map((url: string) => url.replace(/http:\/\//gi, 'https://').trim())
|
||
.filter(Boolean);
|
||
});
|
||
|
||
// 裁剪组件相关
|
||
const showCropper = ref(false);
|
||
const cropperImageSrc = ref('');
|
||
|
||
// 图片上传
|
||
const chooseImage = () => {
|
||
const remainCount = 3 - picList.value.length;
|
||
if (remainCount <= 0) {
|
||
uni.showToast({ title: '最多上传3张图片', icon: 'none' });
|
||
return;
|
||
}
|
||
uni.chooseImage({
|
||
count: 1, // 每次选择一张,选择后立即裁剪
|
||
sizeType: ['original'], // 使用原图,裁剪后再压缩
|
||
sourceType: ['camera', 'album'],
|
||
success: async (res) => {
|
||
const filePath = res.tempFilePaths[0];
|
||
// 显示裁剪界面
|
||
cropperImageSrc.value = filePath;
|
||
showCropper.value = true;
|
||
},
|
||
fail: (err) => {
|
||
console.error('选择图片失败', err);
|
||
uni.showToast({ title: '选择图片失败', icon: 'none' });
|
||
},
|
||
});
|
||
};
|
||
|
||
// 裁剪确认
|
||
const handleCropConfirm = (filePath: string) => {
|
||
uploadImage(filePath);
|
||
};
|
||
|
||
// 裁剪取消
|
||
const handleCropCancel = () => {
|
||
showCropper.value = false;
|
||
cropperImageSrc.value = '';
|
||
};
|
||
|
||
const uploadImage = async (filePath: string) => {
|
||
uni.showLoading({ title: '上传中...' });
|
||
try {
|
||
const uploadRes: any = await uploadFile(filePath);
|
||
|
||
// 追加新图片到列表
|
||
const newUrl = uploadRes.url || uploadRes.filePath;
|
||
const currentList = picList.value;
|
||
const newList = [...currentList, newUrl];
|
||
submitAnswerPic.value = newList.join(',');
|
||
emit('submit', { submitAnswerPic: submitAnswerPic.value });
|
||
uni.hideLoading();
|
||
} catch (error: any) {
|
||
uni.hideLoading();
|
||
uni.showToast({ title: error.message || '上传失败', icon: 'none' });
|
||
}
|
||
};
|
||
|
||
// 删除单张图片
|
||
const deletePic = (url: string) => {
|
||
const newList = picList.value.filter((pic: string) => pic !== url);
|
||
submitAnswerPic.value = newList.join(',');
|
||
emit('submit', { submitAnswerPic: submitAnswerPic.value });
|
||
};
|
||
|
||
// 预览图片列表
|
||
const previewPicList = (index: number) => {
|
||
uni.previewImage({
|
||
urls: picList.value,
|
||
current: index,
|
||
});
|
||
};
|
||
|
||
const previewAnalyzeImage = (url: string) => {
|
||
uni.previewImage({
|
||
urls: props.data.titleAnalyzePicture.split(','),
|
||
current: url,
|
||
});
|
||
};
|
||
|
||
// 音频播放
|
||
const playAudio = () => {
|
||
if (!props.data?.voiceUrl) return;
|
||
|
||
if (!audioContext) {
|
||
audioContext = uni.createInnerAudioContext();
|
||
audioContext.onPlay(() => {
|
||
isAudioPlay.value = true;
|
||
});
|
||
audioContext.onPause(() => {
|
||
isAudioPlay.value = false;
|
||
});
|
||
audioContext.onEnded(() => {
|
||
isAudioPlay.value = false;
|
||
});
|
||
audioContext.onError(() => {
|
||
isAudioPlay.value = false;
|
||
});
|
||
}
|
||
|
||
if (isAudioPlay.value) {
|
||
audioContext.pause();
|
||
} else {
|
||
audioContext.src = props.data.voiceUrl;
|
||
audioContext.play();
|
||
}
|
||
};
|
||
|
||
// 组件卸载时清理
|
||
onUnmounted(() => {
|
||
if (audioContext) {
|
||
audioContext.destroy();
|
||
audioContext = null;
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.question-item {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
position: relative;
|
||
background: rgba(255, 255, 255, 0.95);
|
||
border-radius: 24rpx;
|
||
overflow: hidden;
|
||
}
|
||
|
||
// 分隔线容器
|
||
.divider-wrapper {
|
||
position: absolute;
|
||
left: 50%;
|
||
top: 0;
|
||
bottom: 0;
|
||
width: 50rpx;
|
||
transform: translateX(-50%);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
z-index: 10;
|
||
transition: left 0.3s ease;
|
||
|
||
&.at-right {
|
||
left: calc(100% - 30rpx);
|
||
}
|
||
}
|
||
|
||
.divider {
|
||
flex: 1;
|
||
width: 4rpx;
|
||
margin-top: 32rpx;
|
||
background: repeating-linear-gradient(
|
||
to bottom,
|
||
#cdd5ff,
|
||
#cdd5ff 16rpx,
|
||
transparent 16rpx,
|
||
transparent 32rpx
|
||
);
|
||
border-radius: 4rpx;
|
||
}
|
||
|
||
// 折叠按钮
|
||
.fold-btn {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 8rpx 4rpx;
|
||
background: linear-gradient(135deg, #e8f4fd 0%, #d6ebfa 100%);
|
||
border-radius: 8rpx;
|
||
margin-bottom: 16rpx;
|
||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||
|
||
text {
|
||
font-size: 10rpx;
|
||
color: #4F8EF7;
|
||
writing-mode: vertical-lr;
|
||
letter-spacing: 2rpx;
|
||
}
|
||
|
||
.fold-arrow {
|
||
width: 12rpx;
|
||
height: 12rpx;
|
||
margin-top: 4rpx;
|
||
position: relative;
|
||
transition: transform 0.3s ease;
|
||
|
||
&::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 50%;
|
||
top: 50%;
|
||
width: 8rpx;
|
||
height: 8rpx;
|
||
border-left: 2rpx solid #4F8EF7;
|
||
border-bottom: 2rpx solid #4F8EF7;
|
||
transform: translate(-50%, -70%) rotate(50deg);
|
||
}
|
||
|
||
&.rotated {
|
||
transform: rotate(180deg);
|
||
}
|
||
}
|
||
}
|
||
|
||
.left {
|
||
width: 50%;
|
||
height: 100%;
|
||
padding: 10rpx;
|
||
padding-right: 40rpx;
|
||
box-sizing: border-box;
|
||
transition: width 0.3s ease;
|
||
|
||
&.expanded {
|
||
width: calc(100% - 60rpx);
|
||
padding-right: 10rpx;
|
||
}
|
||
|
||
.left-title {
|
||
padding-top: 5rpx;
|
||
padding-left: 10rpx;
|
||
font-family: Arial;
|
||
font-size: 12rpx;
|
||
color: $font-color;
|
||
line-height: 1.6;
|
||
|
||
:deep(image) {
|
||
max-width: 100%;
|
||
height: auto;
|
||
}
|
||
|
||
// 填空题的下划线样式
|
||
:deep(.fill-blank) {
|
||
display: inline-block;
|
||
min-width: 40rpx;
|
||
height: 1em;
|
||
border-bottom: 2rpx solid $font-color;
|
||
margin: 0 4rpx;
|
||
vertical-align: baseline;
|
||
}
|
||
|
||
// 公式文本样式:使用数学字体风格
|
||
:deep(.formula-text) {
|
||
font-size: 15rpx;
|
||
font-family: 'Times New Roman', 'STIXGeneral', Georgia, serif;
|
||
font-style: italic;
|
||
color: #1a1a1a;
|
||
letter-spacing: 0.5rpx;
|
||
vertical-align: baseline;
|
||
}
|
||
|
||
// 上下标样式:用 span 控制,避免 Unicode 字符过小
|
||
:deep(.sup) {
|
||
font-size: 12rpx;
|
||
line-height: 1;
|
||
vertical-align: super;
|
||
}
|
||
|
||
:deep(.sub) {
|
||
font-size: 12rpx;
|
||
line-height: 1;
|
||
vertical-align: sub;
|
||
}
|
||
|
||
// 分数样式:模拟图片那种上下结构,字体大小与公式文本一致
|
||
:deep(.fraction) {
|
||
display: inline-flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 14rpx;
|
||
line-height: 1.1;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
:deep(.fraction .top) {
|
||
padding: 0 4rpx;
|
||
border-bottom: 2rpx solid currentColor;
|
||
}
|
||
|
||
:deep(.fraction .bottom) {
|
||
padding: 0 4rpx;
|
||
}
|
||
}
|
||
|
||
// 题目图片(可点击预览)
|
||
.title-images {
|
||
padding: 10rpx;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 16rpx;
|
||
|
||
image {
|
||
max-width: 100%;
|
||
border-radius: 8rpx;
|
||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||
}
|
||
}
|
||
|
||
.left-voice {
|
||
padding-left: 32rpx;
|
||
margin-top: 24rpx;
|
||
|
||
image {
|
||
width: 80rpx;
|
||
height: 80rpx;
|
||
}
|
||
}
|
||
|
||
.left-children {
|
||
padding-left: 32rpx;
|
||
margin-top: 24rpx;
|
||
|
||
.child-title {
|
||
font-size: 28rpx;
|
||
color: $font-color;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
}
|
||
|
||
// 阅读理解:子题题干 + 选项(纵向布局)
|
||
.left-yuedu {
|
||
margin-top: 16rpx;
|
||
padding: 0 10rpx;
|
||
|
||
.left-yuedu-item {
|
||
margin-bottom: 16rpx;
|
||
padding: 10rpx 12rpx;
|
||
background: rgba(79, 142, 247, 0.04);
|
||
border-radius: 10rpx;
|
||
border-left: 4rpx solid #8fb5ff;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
|
||
.left-yuedu-stem {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
margin-bottom: 6rpx;
|
||
|
||
.left-yuedu-idx {
|
||
font-family: $font-special;
|
||
font-size: 12rpx;
|
||
font-weight: 700;
|
||
color: #4F8EF7;
|
||
margin-right: 4rpx;
|
||
flex-shrink: 0;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.left-yuedu-stem-text {
|
||
flex: 1;
|
||
font-size: 12rpx;
|
||
color: $font-color;
|
||
line-height: 1.6;
|
||
|
||
:deep(image) {
|
||
max-width: 100%;
|
||
height: auto;
|
||
}
|
||
|
||
:deep(.formula-text) {
|
||
font-size: 13rpx;
|
||
font-family: 'Times New Roman', 'STIXGeneral', Georgia, serif;
|
||
font-style: italic;
|
||
color: #1a1a1a;
|
||
}
|
||
}
|
||
}
|
||
|
||
.left-yuedu-opts {
|
||
padding-left: 16rpx;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 2rpx 16rpx;
|
||
|
||
// 选项文本较长时纵向排列
|
||
&.vertical {
|
||
flex-direction: column;
|
||
gap: 2rpx;
|
||
|
||
.left-yuedu-opt {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
.left-yuedu-opt {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
line-height: 1.6;
|
||
|
||
.left-yuedu-opt-label {
|
||
font-size: 12rpx;
|
||
color: #8f9df7;
|
||
font-weight: 600;
|
||
margin-right: 3rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.left-yuedu-opt-value {
|
||
font-size: 12rpx;
|
||
color: $font-color;
|
||
|
||
:deep(image) {
|
||
max-width: 100%;
|
||
height: auto;
|
||
}
|
||
|
||
:deep(.formula-text) {
|
||
font-size: 13rpx;
|
||
font-family: 'Times New Roman', 'STIXGeneral', Georgia, serif;
|
||
font-style: italic;
|
||
color: #1a1a1a;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 完形填空:序号 + 选项(横向紧凑,无题干,与 admin 对齐)
|
||
.left-wanxing {
|
||
margin-top: 16rpx;
|
||
padding: 0 10rpx;
|
||
|
||
.left-wanxing-row {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 8rpx;
|
||
margin-bottom: 8rpx;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
|
||
.left-wanxing-idx {
|
||
min-width: 20rpx;
|
||
height: 20rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 0 4rpx;
|
||
border-radius: 4rpx;
|
||
background: #f0f4ff;
|
||
font-family: $font-special;
|
||
font-size: 11rpx;
|
||
font-weight: 700;
|
||
color: #4F8EF7;
|
||
flex-shrink: 0;
|
||
line-height: 20rpx;
|
||
}
|
||
|
||
.left-wanxing-opts {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 2rpx 14rpx;
|
||
flex: 1;
|
||
}
|
||
|
||
.left-wanxing-opt {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
line-height: 1.6;
|
||
|
||
.left-wanxing-opt-label {
|
||
font-size: 12rpx;
|
||
color: #8f9df7;
|
||
font-weight: 600;
|
||
margin-right: 2rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.left-wanxing-opt-value {
|
||
font-size: 12rpx;
|
||
color: $font-color;
|
||
|
||
:deep(image) {
|
||
max-width: 100%;
|
||
height: auto;
|
||
}
|
||
|
||
:deep(.formula-text) {
|
||
font-size: 13rpx;
|
||
font-family: 'Times New Roman', 'STIXGeneral', Georgia, serif;
|
||
font-style: italic;
|
||
color: #1a1a1a;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.right {
|
||
width: 50%;
|
||
height: 100%;
|
||
padding: 10rpx;
|
||
padding-left: 40rpx;
|
||
box-sizing: border-box;
|
||
transition: width 0.3s ease, opacity 0.2s ease;
|
||
|
||
&.collapsed {
|
||
width: 0;
|
||
padding: 0;
|
||
opacity: 0;
|
||
overflow: hidden;
|
||
pointer-events: none;
|
||
}
|
||
}
|
||
|
||
.result-box {
|
||
margin-bottom: 10rpx;
|
||
|
||
.result {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0 12rpx;
|
||
height: 40rpx;
|
||
border-radius: 8rpx;
|
||
position: relative;
|
||
font-family: $font-special;
|
||
font-size: 12rpx;
|
||
color: $font-color;
|
||
|
||
&::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
width: 6rpx;
|
||
height: 100%;
|
||
background: #669cfe;
|
||
border-radius: 0 16rpx 16rpx 0;
|
||
}
|
||
|
||
&.error {
|
||
background: linear-gradient(90deg, rgba(255, 191, 191, 0) 0%, #ffbfbf 100%);
|
||
}
|
||
|
||
&.success {
|
||
background: linear-gradient(90deg, rgba(221, 255, 140, 0) 0%, #ddff8c 100%);
|
||
}
|
||
|
||
.emoji-box {
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
image {
|
||
width: 20rpx;
|
||
height: 20rpx;
|
||
margin-right: 8rpx;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.choice-list {
|
||
.choice-item {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
padding: 10rpx;
|
||
margin-bottom: 15rpx;
|
||
background: #f5f7fa;
|
||
border-radius: 16rpx;
|
||
border: 4rpx solid transparent;
|
||
transition: all 0.2s ease;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||
|
||
&.active {
|
||
background: #e6f0ff;
|
||
border-color: #7eacfe;
|
||
}
|
||
|
||
&.correct {
|
||
background: #e6ffe6;
|
||
border-color: #4cd964;
|
||
}
|
||
|
||
&.wrong {
|
||
background: #ffe6e6;
|
||
border-color: #ff4d4f;
|
||
}
|
||
|
||
.choice-key {
|
||
width: 25rpx;
|
||
height: 25rpx;
|
||
background: #fff;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-family: $font-special;
|
||
font-size: 15rpx;
|
||
color: $font-color;
|
||
margin-right: 20rpx;
|
||
flex-shrink: 0;
|
||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.choice-content {
|
||
flex: 1;
|
||
font-size: 12rpx;
|
||
color: $font-color;
|
||
line-height: 1.6;
|
||
|
||
:deep(image) {
|
||
max-width: 100%;
|
||
height: auto;
|
||
}
|
||
|
||
// 公式文本样式:使用数学字体风格
|
||
:deep(.formula-text) {
|
||
font-family: 'Times New Roman', 'STIXGeneral', Georgia, serif;
|
||
font-style: italic;
|
||
font-size: 15rpx;
|
||
color: #1a1a1a;
|
||
letter-spacing: 0.5rpx;
|
||
}
|
||
|
||
// 上下标样式
|
||
:deep(.sup) {
|
||
font-size: 12rpx;
|
||
line-height: 1;
|
||
vertical-align: super;
|
||
}
|
||
|
||
:deep(.sub) {
|
||
font-size: 12rpx;
|
||
line-height: 1;
|
||
vertical-align: sub;
|
||
}
|
||
|
||
// 选项里的分数样式:字体大小与公式文本一致
|
||
:deep(.fraction) {
|
||
display: inline-flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 14rpx;
|
||
line-height: 1.1;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
:deep(.fraction .top) {
|
||
padding: 0 4rpx;
|
||
border-bottom: 2rpx solid currentColor;
|
||
}
|
||
|
||
:deep(.fraction .bottom) {
|
||
padding: 0 4rpx;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 阅读理解 / 完形填空:右侧答题卡
|
||
.yuedu-answer-card {
|
||
.yuedu-card-title {
|
||
text-align: center;
|
||
margin-bottom: 12rpx;
|
||
padding-bottom: 8rpx;
|
||
border-bottom: 2rpx solid #e8ecf4;
|
||
|
||
text {
|
||
font-family: $font-special;
|
||
font-size: 13rpx;
|
||
font-weight: 700;
|
||
color: #8f9df7;
|
||
letter-spacing: 2rpx;
|
||
}
|
||
}
|
||
|
||
.yuedu-card-row {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 10rpx;
|
||
gap: 8rpx;
|
||
|
||
&:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
}
|
||
|
||
.yuedu-card-num {
|
||
width: 22rpx;
|
||
height: 22rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 6rpx;
|
||
background: #f0f4ff;
|
||
font-family: $font-special;
|
||
font-size: 11rpx;
|
||
font-weight: 700;
|
||
color: #4F8EF7;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.yuedu-card-btns {
|
||
display: flex;
|
||
gap: 8rpx;
|
||
flex: 1;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.yuedu-card-btn {
|
||
width: 32rpx;
|
||
height: 32rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 50%;
|
||
background: #f5f7fa;
|
||
border: 2rpx solid #e0e4ea;
|
||
transition: all 0.2s ease;
|
||
box-shadow: 0 1rpx 4rpx rgba(0, 0, 0, 0.05);
|
||
|
||
text {
|
||
font-family: $font-special;
|
||
font-size: 13rpx;
|
||
font-weight: 600;
|
||
color: #666;
|
||
}
|
||
|
||
&.active {
|
||
background: linear-gradient(135deg, #7eacfe 0%, #5b8def 100%);
|
||
border-color: #5b8def;
|
||
box-shadow: 0 2rpx 8rpx rgba(91, 141, 239, 0.35);
|
||
|
||
text {
|
||
color: #fff;
|
||
}
|
||
}
|
||
|
||
&.correct {
|
||
background: linear-gradient(135deg, #6dd5a0 0%, #4cd964 100%);
|
||
border-color: #4cd964;
|
||
box-shadow: 0 2rpx 8rpx rgba(76, 217, 100, 0.35);
|
||
|
||
text {
|
||
color: #fff;
|
||
}
|
||
}
|
||
|
||
&.wrong {
|
||
background: linear-gradient(135deg, #ff7875 0%, #ff4d4f 100%);
|
||
border-color: #ff4d4f;
|
||
box-shadow: 0 2rpx 8rpx rgba(255, 77, 79, 0.35);
|
||
|
||
text {
|
||
color: #fff;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.judgment-list {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 30rpx;
|
||
|
||
.judgment-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
padding: 20rpx;
|
||
background: #f5f7fa;
|
||
border-radius: 24rpx;
|
||
border: 4rpx solid transparent;
|
||
transition: all 0.2s ease;
|
||
|
||
&.active {
|
||
background: #e6f0ff;
|
||
border-color: #7eacfe;
|
||
}
|
||
|
||
image {
|
||
width: 20rpx;
|
||
height: 20rpx;
|
||
margin-bottom: 10rpx;
|
||
}
|
||
|
||
text {
|
||
font-family: $font-special;
|
||
font-size: 28rpx;
|
||
color: $font-color;
|
||
}
|
||
}
|
||
}
|
||
|
||
.input-area {
|
||
.input-item {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 24rpx;
|
||
|
||
.input-label {
|
||
font-size: 28rpx;
|
||
color: $font-color;
|
||
margin-right: 16rpx;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.input-field {
|
||
flex: 1;
|
||
height: 72rpx;
|
||
padding: 0 24rpx;
|
||
background: #f5f7fa;
|
||
border-radius: 12rpx;
|
||
font-size: 28rpx;
|
||
color: $font-color;
|
||
}
|
||
}
|
||
}
|
||
|
||
.upload-area {
|
||
// 提示文字
|
||
.upload-tip {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 16rpx 20rpx;
|
||
margin-bottom: 20rpx;
|
||
background: linear-gradient(135deg, #fff9e6 0%, #fff3cc 100%);
|
||
border-radius: 12rpx;
|
||
border: 2rpx solid #ffe58f;
|
||
|
||
.tip-icon {
|
||
font-size: 20rpx;
|
||
margin-right: 10rpx;
|
||
}
|
||
|
||
text {
|
||
font-size: 13rpx;
|
||
color: #d48806;
|
||
font-family: $font-special;
|
||
}
|
||
}
|
||
|
||
// 多图列表
|
||
.pic-list {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 16rpx;
|
||
|
||
.pic-item {
|
||
position: relative;
|
||
width: 120rpx;
|
||
height: 120rpx;
|
||
border-radius: 12rpx;
|
||
overflow: hidden;
|
||
background: #f5f7fa;
|
||
|
||
image {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.delete-btn {
|
||
position: absolute;
|
||
top: 0;
|
||
right: 0;
|
||
width: 32rpx;
|
||
height: 32rpx;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
border-radius: 0 0 0 12rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
text {
|
||
font-size: 24rpx;
|
||
color: #fff;
|
||
line-height: 1;
|
||
}
|
||
}
|
||
}
|
||
|
||
.add-btn {
|
||
width: 120rpx;
|
||
height: 120rpx;
|
||
border-radius: 12rpx;
|
||
background: #f5f7fa;
|
||
border: 2rpx dashed #ccc;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
text {
|
||
font-size: 48rpx;
|
||
color: #999;
|
||
}
|
||
}
|
||
}
|
||
|
||
.empty-tip {
|
||
padding: 32rpx;
|
||
text-align: center;
|
||
|
||
text {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
}
|
||
}
|
||
|
||
.upload-btn {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 20rpx;
|
||
background: #f5f7fa;
|
||
border-radius: 16rpx;
|
||
border: 2rpx dashed #ccc;
|
||
|
||
.upload-btn-button {
|
||
padding: 10rpx 20rpx;
|
||
font-size: 14rpx;
|
||
background: #7eacfe;
|
||
color: #fff;
|
||
border-radius: 20rpx;
|
||
box-shadow: 0 3rpx 8rpx rgba(0, 0, 0, 0.15);
|
||
font-family: AlimamaShuHeiTi;
|
||
}
|
||
}
|
||
}
|
||
|
||
.answer-box {
|
||
margin-top: 20rpx;
|
||
|
||
&-title {
|
||
padding: 0 32rpx;
|
||
position: relative;
|
||
font-family: $font-special;
|
||
font-size: 12rpx;
|
||
color: $font-color;
|
||
margin-bottom: 14rpx;
|
||
|
||
&::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
width: 12rpx;
|
||
height: 100%;
|
||
background: #669cfe;
|
||
border-radius: 0 16rpx 16rpx 0;
|
||
}
|
||
}
|
||
|
||
&-main {
|
||
font-family: 'OPPO Sans';
|
||
font-size: 12rpx;
|
||
color: $font-color;
|
||
padding-left: 16rpx;
|
||
line-height: 1.6;
|
||
|
||
// 解析中的公式文本样式:使用数学字体风格
|
||
:deep(.formula-text) {
|
||
font-size: 15rpx;
|
||
font-family: 'Times New Roman', 'STIXGeneral', Georgia, serif;
|
||
font-style: italic;
|
||
color: #1a1a1a;
|
||
letter-spacing: 0.5rpx;
|
||
vertical-align: baseline;
|
||
}
|
||
|
||
// 上下标样式
|
||
:deep(.sup) {
|
||
font-size: 12rpx;
|
||
line-height: 1;
|
||
vertical-align: super;
|
||
}
|
||
|
||
:deep(.sub) {
|
||
font-size: 12rpx;
|
||
line-height: 1;
|
||
vertical-align: sub;
|
||
}
|
||
|
||
// 解析中的分数样式
|
||
:deep(.fraction) {
|
||
display: inline-flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 14rpx;
|
||
line-height: 1.1;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
&.analyze {
|
||
:deep(image) {
|
||
max-width: 100%;
|
||
height: auto;
|
||
}
|
||
}
|
||
|
||
&.analyze-pic {
|
||
image {
|
||
max-width: 100%;
|
||
margin-bottom: 16rpx;
|
||
border-radius: 8rpx;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|