diff --git a/src/components/Loading/index.vue b/src/components/Loading/index.vue index a363812..997f32e 100644 --- a/src/components/Loading/index.vue +++ b/src/components/Loading/index.vue @@ -1,18 +1,45 @@ @@ -20,19 +47,25 @@ diff --git a/src/pages/doWork/components/Question.vue b/src/pages/doWork/components/Question.vue index 7767a89..a188a62 100644 --- a/src/pages/doWork/components/Question.vue +++ b/src/pages/doWork/components/Question.vue @@ -290,8 +290,8 @@ const filterTitleWithoutImages = (title?: string, cidx?: number | string) => { t = t.replace(/http:\/\//gi, 'https://'); // 先修复数学符号(< > 转义、AUB→∪),避免被 HTML 解析截断 t = fixMathSymbolsInHtml(t); - // 处理公式 - t = processFormula(t); + // 处理公式(传入失败 URL 集合,触发响应式依赖以便失败后自动重渲染) + t = processFormula(t, failedFormulaUrls.value); if (cidx !== undefined) { t = `(${Number(cidx) + 1})` + t; @@ -320,6 +320,10 @@ const handleRichTextClick = (e: any) => { const submitAnswer = ref(''); const submitAnswerPic = ref(''); +// ========== 公式图片预加载 + 静默重试 ========== +// 加载永久失败的公式图片 URL 集合(重试耗尽后加入,触发模板重新渲染用文本兜底) +const failedFormulaUrls = ref>(new Set()); + // 监听题目变化,初始化答案 watch( () => props.data?.id, @@ -620,7 +624,8 @@ const simpleFormulaToHtml = (formula: string): string => { }; // 处理数学公式:使用在线 LaTeX 渲染服务 -const processFormula = (html: string): string => { +// failedUrls: 可选,加载永久失败的 URL 集合;命中时用文本兜底而非 +const processFormula = (html: string, failedUrls?: Set): string => { if (html == null || typeof html !== 'string') return ''; if (!html) return ''; @@ -648,10 +653,130 @@ const processFormula = (html: string): string => { // 复杂公式使用 LaTeX 图片渲染 const imageUrl = latexToImageUrl(formula); + + // 如果该图片经过多次重试仍失败,使用 LaTeX 文本兜底 + if (failedUrls?.has(imageUrl)) { + const display = formula.length > 60 ? formula.substring(0, 60) + '…' : formula; + return `[${display}]`; + } + return ``; }); }; +// ---------- 公式图片预加载工具函数 ---------- + +// 从原始 HTML 中提取所有复杂公式对应的图片 URL(逻辑与 processFormula 一致) +const extractFormulaImageUrls = (html: string): string[] => { + if (html == null || typeof html !== 'string') return []; + const formulaRegex = /"']+|"[^"]*"|'[^']*')*)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 => { + 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) => { + const v = typeof item === 'string' ? item : (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(); + 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 字符串,需先解析为数组 @@ -688,7 +813,7 @@ const optionList = computed(() => { } value = fixMathSymbolsInHtml(value); - value = processFormula(value); + value = processFormula(value, failedFormulaUrls.value); value = typeof value === 'string' ? value.replace(/http:\/\//gi, 'https://') : ''; // 选项内容为空时显示占位,避免只看到选项字母没有内容 // if (!value || (typeof value === 'string' && !value.trim())) { @@ -762,7 +887,7 @@ const filterTitle = (title?: string, cidx?: number | string) => { if (!title) return ''; let t = title.replace(/http:\/\//gi, 'https://'); t = fixMathSymbolsInHtml(t); - t = processFormula(t); + t = processFormula(t, failedFormulaUrls.value); if (cidx !== undefined) { t = `(${Number(cidx) + 1})` + t; } diff --git a/src/pages/teacher/index/index.vue b/src/pages/teacher/index/index.vue index e82a00d..c466c2d 100644 --- a/src/pages/teacher/index/index.vue +++ b/src/pages/teacher/index/index.vue @@ -584,6 +584,7 @@ $teacher-bg: 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com/urm/main_bg.s flex-wrap: wrap; gap: 6rpx; justify-content: center; + margin-top: 8rpx; } .action-item { @@ -596,7 +597,7 @@ $teacher-bg: 'https://xxl-1313840333.cos.ap-guangzhou.myqcloud.com/urm/main_bg.s background: rgba(255, 255, 255, 0.85); border-radius: 8rpx; border: 1rpx solid rgba(196, 181, 255, 0.2); - margin-top: 8rpx; + // margin-top: 8rpx; width: 100%; .action-icon { font-size: 18rpx; diff --git a/src/pages/teacher/weakKnowledge/index.vue b/src/pages/teacher/weakKnowledge/index.vue index bc8a261..1b819b7 100644 --- a/src/pages/teacher/weakKnowledge/index.vue +++ b/src/pages/teacher/weakKnowledge/index.vue @@ -8,7 +8,7 @@ - + {{ errorMsg }} @@ -23,7 +23,7 @@ 薄弱知识点 Top10 - 暂无薄弱知识点数据 +