fix(流程图): 点进去空白的 tab、以及「渲染成功」这个只进不出的开关

## 两个开关同时打开会做出一个空 tab

后端在 `allowFlowchart` 为真时把 `mermaidCode` 置成 null(不能把标准答案下发给
正要自己画图的学生,见 routes/problem.ts)。而前端 `tabOptions` 只看
`showFlowchart` 就把 "flowchart" 加进选项,面板那边却要求
`showFlowchart && mermaidCode` —— 两个开关都打开时,选项存在、面板不存在,
URL 里带 `?tab=flowchart` 就会选中一个渲染不出任何东西的页签。

三个断点条件还各不相同(两处要求两者都有,第三处只看 showFlowchart,那处会拿
null 去渲染 ProblemFlowchart)。统一成一个 `canShowFlowchart`。

后台那边把「显示标准流程图」在允许提交流程图时置灰并说明原因,再补一个 watch
把存量数据里两个都开着的情况纠正掉 —— 它们本来就是互斥的。

## 「渲染成功」只进不出

保存前的校验靠 `mermaidRenderSuccess`,而 MermaidEditor 只在成功时 emit、
这个 ref 也就只会从 false 变 true,永不复位。**先写对、再改坏,照样能存进库。**

改成上报渲染结果本身(`render-state`),并在 modelValue 一变就立刻打回
「未验证」,等防抖后的渲染真跑完再报结论 —— 只挂防抖那一支的话,改完 300ms 内
点保存读到的还是上一次的结论,刚改坏的代码会被当成校验通过。宁可让出题人多等
一下,也不能放脏数据进库。

实测:改动后 50ms 读到 false(此时保存会被拦),渲染完成后回到 true;
贴一段坏语法则一直是 false。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-27 06:27:02 -06:00
parent d10172f017
commit 9e41855610
3 changed files with 45 additions and 11 deletions

View File

@@ -173,6 +173,16 @@ const languageOptions = [
const isSQLProblem = computed(() => !!problem.value?.languages.includes("SQL"))
// SQL 题联动SQL 必须是唯一语言(后端强校验),不需要预制代码,自动初始化 sql_config
// 两个流程图开关是互斥的allowFlowchart 为真时后端不会把 mermaidCode 下发给
// 学生showFlowchart 就成了一个点进去什么都没有的 tab。UI 上已经把开关置灰,
// 这里再把存量数据里两个都开着的情况纠正掉。
watch(
() => problem.value?.allowFlowchart,
(allow) => {
if (allow) problem.value.showFlowchart = false
},
)
watch(
() => problem.value?.languages,
(langs) => {
@@ -310,9 +320,10 @@ function downloadTestcases() {
download(`problems/${problem.value.id}/test-cases`)
}
// Mermaid 渲染事件处理
function onMermaidRenderSuccess() {
mermaidRenderSuccess.value = true
// Mermaid 渲染事件处理。这里必须原样接受 false ——
// 原来只在成功时置 true、永不复位先写对再改坏就能把语法错误的代码存进库
function onMermaidRenderState(ok: boolean) {
mermaidRenderSuccess.value = ok
}
// 题目是否有漏写的
@@ -864,7 +875,15 @@ watch(
<n-switch v-model:value="problem.allowFlowchart" />
</n-form-item>
<n-form-item label="显示标准流程图">
<n-switch v-model:value="problem.showFlowchart" />
<n-flex align="center">
<n-switch
v-model:value="problem.showFlowchart"
:disabled="problem.allowFlowchart"
/>
<n-text v-if="problem.allowFlowchart" depth="3" style="font-size: 12px">
让学生自己画图时标准流程图不会下发给学生这个开关没有意义
</n-text>
</n-flex>
</n-form-item>
</n-form>
@@ -872,7 +891,7 @@ watch(
<n-form-item>
<MermaidEditor
v-model="problem.mermaidCode"
@render-success="onMermaidRenderSuccess"
@render-state="onMermaidRenderState"
/>
</n-form-item>
<n-form-item label="流程图提示信息(选填)">

View File

@@ -53,9 +53,17 @@ const { shouldShowProblem } = storeToRefs(screenModeStore)
const { isMobile, isDesktop } = useBreakpoints()
// tab 选项和面板必须用同一个条件。后端在 allowFlowchart 为真时会把 mermaidCode
// 置成 null不能把标准答案下发给正要自己画图的学生只看 showFlowchart 的话,
// 两个开关同时打开就会做出一个「选项存在、面板不存在」的 tab —— URL 里带
// ?tab=flowchart 会选中一个渲染不出任何东西的页签。
const canShowFlowchart = computed(
() => !!problem.value?.showFlowchart && !!problem.value?.mermaidCode,
)
const tabOptions = computed(() => {
const options: string[] = ["content"]
if (problem.value?.showFlowchart) {
if (canShowFlowchart.value) {
options.push("flowchart")
}
@@ -159,7 +167,7 @@ watch(
<ProblemContent />
</n-tab-pane>
<n-tab-pane
v-if="problem.showFlowchart && problem.mermaidCode"
v-if="canShowFlowchart"
name="flowchart"
tab="流程图表"
>
@@ -211,7 +219,7 @@ watch(
<ProblemContent />
</n-tab-pane>
<n-tab-pane
v-if="problem.showFlowchart && problem.mermaidCode"
v-if="canShowFlowchart"
name="flowchart"
tab="流程图表"
>
@@ -251,7 +259,7 @@ watch(
<n-tab-pane name="content" tab="描述">
<ProblemContent />
</n-tab-pane>
<n-tab-pane v-if="problem.showFlowchart" name="flowchart" tab="流程">
<n-tab-pane v-if="canShowFlowchart" name="flowchart" tab="流程">
<ProblemFlowchart />
</n-tab-pane>
<n-tab-pane name="editor" tab="代码">

View File

@@ -7,19 +7,26 @@ const mermaidContainer = useTemplateRef<HTMLElement>("mermaidContainer")
const { renderFlowchart, renderError, renderSuccess } = useMermaid()
// 上报渲染结果而不是只上报成功:调用方拿它做保存前校验,只进不出的话,
// 「先写对再改坏」照样能存进库
const emit = defineEmits<{
renderSuccess: []
renderState: [ok: boolean]
}>()
const renderMermaid = async () => {
await renderFlowchart(mermaidContainer.value, modelValue.value)
if (renderSuccess.value) emit("renderSuccess")
emit("renderState", renderSuccess.value)
}
onMounted(() => {
nextTick(renderMermaid)
})
// 一改动就立刻把状态打回「未验证」,等防抖后的渲染真跑完再报结果。
// 只挂防抖那一支的话,改完 300ms 内点保存,读到的还是上一次渲染的结论 ——
// 刚改坏的代码会被当成校验通过。宁可让用户多等一下,也不能放脏数据进库。
watch(modelValue, () => emit("renderState", false))
// 出题页是边敲边预览,不防抖的话每个字符都会触发一次完整的 mermaid 渲染,
// 而中间态几乎全是语法错误
watchDebounced(modelValue, renderMermaid, { debounce: 300, maxWait: 1000 })