1575 lines
48 KiB
Vue
1575 lines
48 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" 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>
|
||
</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>
|
||
<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>
|
||
</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);
|
||
// 处理公式
|
||
t = processFormula(t);
|
||
|
||
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('');
|
||
|
||
// 监听题目变化,初始化答案
|
||
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 渲染服务
|
||
const processFormula = (html: 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);
|
||
return `<img class="formula-img" src="${imageUrl}" style="height:1.4em;vertical-align:middle;" />`;
|
||
});
|
||
};
|
||
|
||
|
||
// 选项列表 - 单选题/多选题:仅用 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);
|
||
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;
|
||
});
|
||
|
||
// 题目类型
|
||
const questionType = computed(() => {
|
||
const data = props.data;
|
||
if (!data) return 'other';
|
||
|
||
// 判断题
|
||
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);
|
||
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 });
|
||
};
|
||
|
||
// 填空题
|
||
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;
|
||
}
|
||
}
|
||
}
|
||
|
||
.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;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.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>
|