perf(Markdown): 题面、教程、AI 提示每次打开都去 unpkg.com 拉四个库,机房网络下又慢又不稳

md-editor-v3 的 MdPreview 不管正文里有没有代码、公式,都会从 unpkg.com 加载
highlight.js、katex、mermaid、echarts(mermaid 还是另一份 11.x)。而我们只用到最基础的
Markdown:题面 95% 是富文本存的 HTML,教程是标题、表格、图片,AI 输出是标题、列表、
表格、少量代码块,库里一处公式、mermaid 都没有。

- 显示:新组件 shared/components/Markdown.vue,直接用 markdown-it,代码高亮走本地的
  highlight.js(和练一练共用 shared/highlight.ts,补了 C++ / SQL)。9 处 MdPreview 全换掉。
  正文默认 16px,教程也统一成 16px。
- 编辑:后台教程编辑器换成项目已有的 CodeMirror,加一排格式按钮;图片可以按按钮选、
  粘贴、拖进来,都是先上传再插入。md-editor-v3 从依赖里删掉。
- AI 生成的内容(AI 提示、AI 分析、班级报告)不再放行原生 HTML:学生能在代码里诱导
  AI 吐出 <img onerror>,老师在后台一打开就执行。库里 1196 条 AI 输出没有一个 HTML
  标签,关掉不影响显示。题面、教程照旧放行。
- 长英文改成在单词之间折行,原来 break-all 会把 "I live" 断成 "I l / ive"。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-12 14:02:54 +08:00
1 parent c497ccadc8
commit 6f47ad8358
24 files changed
+504 -308

No files matched your search

@@ -6,9 +6,7 @@ import { useProblemStore } from "oj/store/problem"
import { DIFFICULTY } from "utils/constants"
import { copyToClipboard, getTagColor } from "utils/functions"
import { useSubmissionStore } from "oj/store/submission"
import { useDark } from "@vueuse/core"
import { MdPreview } from "md-editor-v3"
import "md-editor-v3/lib/preview.css"
import Markdown from "shared/components/Markdown.vue"
import SqlProblemData from "./SqlProblemData.vue"
import SimilarProblems from "./SimilarProblems.vue"
import { isFlowchartPass, useFlowchartStore } from "oj/store/flowchart"
@@ -19,7 +17,6 @@ import { useProblemPageContext } from "../composables/problemPageContext"
const ProblemFlowchart = defineAsyncComponent(() => import("./ProblemFlowchart.vue"))
const MyFlowchart = defineAsyncComponent(() => import("./MyFlowchart.vue"))
const isDark = useDark()
const theme = useThemeVars()
const problemStore = useProblemStore()
const { problem } = storeToRefs(problemStore)
@@ -234,30 +231,18 @@ const astRequirements = computed(() => Object.entries(problem.value?.astRequirem
<section class="block">
<h3 class="title">描述</h3>
<MdPreview
preview-theme="vuepress"
:model-value="problem.description"
:theme="isDark ? 'dark' : 'light'"
/>
<Markdown html :content="problem.description" />
</section>
<!-- 输入、输出一般都只有一两句,左右摆开省半屏 -->
<div v-if="!isSQL" class="io">
<section class="block">
<h3 class="title">输入</h3>
<MdPreview
preview-theme="vuepress"
:model-value="problem.inputDescription"
:theme="isDark ? 'dark' : 'light'"
/>
<Markdown html :content="problem.inputDescription" />
</section>
<section class="block">
<h3 class="title">输出</h3>
<MdPreview
preview-theme="vuepress"
:model-value="problem.outputDescription"
:theme="isDark ? 'dark' : 'light'"
/>
<Markdown html :content="problem.outputDescription" />
</section>
</div>
@@ -388,20 +373,12 @@ const astRequirements = computed(() => Object.entries(problem.value?.astRequirem
<section v-if="problem.hint" class="block">
<h3 class="title">提示</h3>
<MdPreview
preview-theme="preview"
:model-value="problem.hint"
:theme="isDark ? 'dark' : 'light'"
/>
<Markdown html :content="problem.hint" />
</section>
<section v-if="problem.source" class="block">
<h3 class="title">来源</h3>
<MdPreview
preview-theme="vuepress"
:model-value="problem.source"
:theme="isDark ? 'dark' : 'light'"
/>
<Markdown html :content="problem.source" />
</section>
<SimilarProblems v-if="showSimilar" title="相似题目推荐" />
@@ -477,23 +454,6 @@ const astRequirements = computed(() => Object.entries(problem.value?.astRequirem
border-left: 3px solid v-bind("theme.primaryColor");
}
/* md-editor 的预览自带一圈 padding、白底和段落外边距,放在小节标题下面显得很散 */
:deep(.md-editor) {
background: transparent;
}
:deep(.md-editor-preview-wrapper) {
padding: 0;
}
:deep(.md-editor-preview > :first-child) {
margin-top: 0;
}
:deep(.md-editor-preview > :last-child) {
margin-bottom: 0;
}
.io {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -23,9 +23,7 @@ import ResultHeader from "./ResultHeader.vue"
import SimilarProblems from "./SimilarProblems.vue"
import { useLessonStore } from "oj/store/lesson"
import { useProblemPageContext } from "../composables/problemPageContext"
import { MdPreview } from "md-editor-v3"
import "md-editor-v3/lib/preview.css"
import { useDark } from "@vueuse/core"
import Markdown from "shared/components/Markdown.vue"
const props = defineProps<{
submission?: Submission
@@ -36,7 +34,6 @@ const props = defineProps<{
peer?: boolean
}>()
const isDark = useDark()
const problemStore = useProblemStore()
const theme = useThemeVars()
@@ -390,12 +387,7 @@ const columns: DataTableColumn<JudgeCaseResult>[] = [
<template v-if="showAIHint && hintTarget">
<n-card size="small" class="hint-card">
<n-spin v-if="hintLoading && !hintContent" size="small" />
<MdPreview
v-if="hintContent"
:model-value="hintContent"
preview-theme="vuepress"
:theme="isDark ? 'dark' : 'light'"
/>
<Markdown v-if="hintContent" :content="hintContent" />
<!-- 等级和「再多一点提示」。按钮出不出由后端 done 里的 canEscalate 定,
前端不自己推阶梯(要再交一次才升得动,规则在 services/hint-level.ts) -->
<n-flex
@@ -30,7 +30,7 @@ export function useSubmissionHint() {
const hintFeedbackSending = ref(false)
// 打字机:每 24ms 吐 3 个字,约 125 字/秒。步子不敢迈太小 ——
// 每一帧都要让 MdPreview 重渲染一次 Markdown,机房那批机器扛不住逐字
// 每一帧都要让 Markdown 重渲染一次 Markdown,机房那批机器扛不住逐字
const TYPE_STEP = 3
const TYPE_INTERVAL = 24
let typingTimer: ReturnType<typeof setInterval> | null = null