- 学生画的图现在按例子和测试数据评(拼成程序跑),参考图用不上了:「让学生自己画」 只剩一句说明,保存前改成检查有没有例子;「给学生的提示」那一栏去掉(学生从来看不到, 16 道题也都没填) - 「给学生看一张参考流程图」改用学生画图的同一个编辑器,存结构树(problem.flowchart_data, 原来是死列),Mermaid 由后端照它生成,题面里的图和学生交的图同一套形状 - 「AI 照 Python 答案画一张」改成出结构树,直接摆在画布上接着改;for 循环画成 计数变量 + 当型循环 - 老题手写的 Mermaid 照旧给代码框,旁边「改用画布画」 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b9ece7d38f
commit
6e946c99fb
8 files changed
+159
-48
No files matched your search
@@ -1,4 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
|
flowToMermaid,
|
||||||
|
parseFlowDoc,
|
||||||
addContestProblemRequestSchema,
|
addContestProblemRequestSchema,
|
||||||
reorderContestProblemsRequestSchema,
|
reorderContestProblemsRequestSchema,
|
||||||
astCheckRequestSchema,
|
astCheckRequestSchema,
|
||||||
@@ -206,6 +208,7 @@ async function serialize(row: ProblemRow) {
|
|||||||
allowFlowchart: row.allowFlowchart,
|
allowFlowchart: row.allowFlowchart,
|
||||||
showFlowchart: row.showFlowchart,
|
showFlowchart: row.showFlowchart,
|
||||||
mermaidCode: row.mermaidCode,
|
mermaidCode: row.mermaidCode,
|
||||||
|
flowchartData: asRecord(row.flowchartData),
|
||||||
flowchartHint: row.flowchartHint,
|
flowchartHint: row.flowchartHint,
|
||||||
astRules: row.astRules,
|
astRules: row.astRules,
|
||||||
answers: Array.isArray(row.answers) ? row.answers : [],
|
answers: Array.isArray(row.answers) ? row.answers : [],
|
||||||
@@ -276,6 +279,17 @@ async function generateSqlDisplay(
|
|||||||
return { display: outcome.value }
|
return { display: outcome.value }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参考流程图:画布画的(结构树)以它为准,Mermaid 照它生成 —— 题面里给学生看的、评分退回
|
||||||
|
* 老办法时拿来比的都是这份,和学生交的图同一套形状。只有手写 Mermaid 的老题原样存
|
||||||
|
*/
|
||||||
|
function referenceFlowchart(raw: Record<string, unknown> | null, mermaidCode: string | null) {
|
||||||
|
const doc = parseFlowDoc(raw)
|
||||||
|
return doc && doc.steps.length
|
||||||
|
? { flowchartData: doc, mermaidCode: flowToMermaid(doc) }
|
||||||
|
: { flowchartData: {}, mermaidCode }
|
||||||
|
}
|
||||||
|
|
||||||
function problemValues(data: ReturnType<typeof createProblemRequestSchema.parse>, isSql: boolean) {
|
function problemValues(data: ReturnType<typeof createProblemRequestSchema.parse>, isSql: boolean) {
|
||||||
return {
|
return {
|
||||||
displayId: data._id,
|
displayId: data._id,
|
||||||
@@ -296,7 +310,7 @@ function problemValues(data: ReturnType<typeof createProblemRequestSchema.parse>
|
|||||||
source: data.source,
|
source: data.source,
|
||||||
allowFlowchart: data.allowFlowchart,
|
allowFlowchart: data.allowFlowchart,
|
||||||
showFlowchart: data.showFlowchart,
|
showFlowchart: data.showFlowchart,
|
||||||
mermaidCode: data.mermaidCode,
|
...referenceFlowchart(data.flowchartData, data.mermaidCode),
|
||||||
flowchartHint: data.flowchartHint,
|
flowchartHint: data.flowchartHint,
|
||||||
astRules: pickAstRules(data.astRules, data.languages),
|
astRules: pickAstRules(data.astRules, data.languages),
|
||||||
answers: data.answers,
|
answers: data.answers,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
parseFlowDoc,
|
||||||
batchProblemTagRequestSchema,
|
batchProblemTagRequestSchema,
|
||||||
generateFlowchartRequestSchema,
|
generateFlowchartRequestSchema,
|
||||||
renameTagRequestSchema,
|
renameTagRequestSchema,
|
||||||
@@ -16,7 +17,7 @@ import type { AuthUser } from "../../auth/session"
|
|||||||
import { db, schema } from "../../db"
|
import { db, schema } from "../../db"
|
||||||
import { failure, parseBody, success } from "../../http"
|
import { failure, parseBody, success } from "../../http"
|
||||||
import { completeChat } from "../../services/ai"
|
import { completeChat } from "../../services/ai"
|
||||||
import { queryInteger } from "../helpers"
|
import { asRecord, queryInteger } from "../helpers"
|
||||||
import { canEdit, findTagsByName, normalizeTagNames } from "./problem"
|
import { canEdit, findTagsByName, normalizeTagNames } from "./problem"
|
||||||
|
|
||||||
export const adminTagRoutes = new Hono<AppEnv>()
|
export const adminTagRoutes = new Hono<AppEnv>()
|
||||||
@@ -254,18 +255,32 @@ adminTagRoutes.put("/problems/:id/visibility", requireProblemPermission, async (
|
|||||||
|
|
||||||
// ---------------------------------------------------------------- Python → 流程图
|
// ---------------------------------------------------------------- Python → 流程图
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 照标准答案画参考流程图。原来让 AI 直接写 Mermaid,老师拿到的是一段代码,要改只能改代码;
|
||||||
|
* 现在出结构树,后台画布直接摆上去,和学生画图是同一个编辑器
|
||||||
|
*/
|
||||||
|
const PYTHON_TO_FLOWCHART = `把下面这段 Python 标准答案画成流程图,给中职学生看。只返回 JSON:{"steps":[…]}
|
||||||
|
每一步是下面五种之一(开始、结束不用写):
|
||||||
|
{"kind":"input","text":"输入 n"}
|
||||||
|
{"kind":"output","text":"输出 a(保留两位小数)"}
|
||||||
|
{"kind":"process","text":"i = i + 1"}
|
||||||
|
{"kind":"if","text":"a > b","yes":[…],"no":[…]}
|
||||||
|
{"kind":"while","text":"i < 365","body":[…]}
|
||||||
|
- 框里写中文和简单的式子,像课本上那样;不要写 input、print、int、range 这些函数名
|
||||||
|
- for 循环改成计数变量 + while:先一个处理框给变量赋初值,while 写条件,循环体最后一个处理框让变量加一
|
||||||
|
- elif 写成 else 里再套一个 if
|
||||||
|
- break、continue、函数定义尽量改写成等价的顺序 / 判断 / 循环;实在改不了就用一个处理框写一句说明`
|
||||||
|
|
||||||
adminTagRoutes.post("/problems/flowchart", requireProblemPermission, async (c) => {
|
adminTagRoutes.post("/problems/flowchart", requireProblemPermission, async (c) => {
|
||||||
const parsed = await parseBody(c, generateFlowchartRequestSchema, "python 代码不能为空")
|
const parsed = await parseBody(c, generateFlowchartRequestSchema, "python 代码不能为空")
|
||||||
if (!parsed.success) return parsed.response
|
if (!parsed.success) return parsed.response
|
||||||
try {
|
try {
|
||||||
const flowchart = await completeChat(
|
const reply = asRecord(
|
||||||
`你是一个可以将Python代码转换为mermaid的助手。
|
JSON.parse(await completeChat(PYTHON_TO_FLOWCHART, parsed.data.python, { json: true })),
|
||||||
请将用户提供的Python代码转换为 Mermaid 纯文本。
|
|
||||||
注意括号内的内容用引号包裹,如果本身就有引号,请注意双引号和单引号的问题。
|
|
||||||
请只返回 mermaid 代码,连 \`\`\` 都不需要。`,
|
|
||||||
parsed.data.python,
|
|
||||||
)
|
)
|
||||||
return success(c, { flowchart } satisfies GenerateFlowchartResponse)
|
const doc = parseFlowDoc({ version: 2, steps: reply.steps })
|
||||||
|
if (!doc || !doc.steps.length) throw new Error("AI returned no flowchart steps")
|
||||||
|
return success(c, { doc: { ...doc } } satisfies GenerateFlowchartResponse)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Flowchart generation failed", error)
|
console.error("Flowchart generation failed", error)
|
||||||
return failure(c, 502, "ai-unavailable", "生成失败,请稍后再试")
|
return failure(c, 502, "ai-unavailable", "生成失败,请稍后再试")
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ export function toggleProblemVisible(problemID: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function generateFlowchartFromPythonCode(python: string) {
|
export function generateFlowchartFromPythonCode(python: string) {
|
||||||
return api.post<{ flowchart: string }>("admin/problems/flowchart", {
|
return api.post<{ doc: Record<string, unknown> }>("admin/problems/flowchart", {
|
||||||
python,
|
python,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -276,7 +276,9 @@ export function generateTestInputs(data: GenerateTestInputsRequest) {
|
|||||||
|
|
||||||
/** 出站补默认值。字段名两边已经一致,不再做键名转换 */
|
/** 出站补默认值。字段名两边已经一致,不再做键名转换 */
|
||||||
function toProblemBody(problem: AdminProblem | BlankProblem) {
|
function toProblemBody(problem: AdminProblem | BlankProblem) {
|
||||||
const p = problem as Partial<AdminProblem>
|
const p = problem as Partial<Omit<AdminProblem, "flowchartData">> & {
|
||||||
|
flowchartData?: Record<string, unknown> | null
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
_id: p._id,
|
_id: p._id,
|
||||||
title: p.title,
|
title: p.title,
|
||||||
@@ -300,6 +302,7 @@ function toProblemBody(problem: AdminProblem | BlankProblem) {
|
|||||||
allowFlowchart: p.allowFlowchart ?? false,
|
allowFlowchart: p.allowFlowchart ?? false,
|
||||||
showFlowchart: p.showFlowchart ?? false,
|
showFlowchart: p.showFlowchart ?? false,
|
||||||
mermaidCode: p.mermaidCode ?? null,
|
mermaidCode: p.mermaidCode ?? null,
|
||||||
|
flowchartData: p.flowchartData ?? null,
|
||||||
flowchartHint: p.flowchartHint ?? null,
|
flowchartHint: p.flowchartHint ?? null,
|
||||||
astRules: p.astRules ?? null,
|
astRules: p.astRules ?? null,
|
||||||
sqlConfig: p.sqlConfig ?? null,
|
sqlConfig: p.sqlConfig ?? null,
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ const props = defineProps<{
|
|||||||
astChecks: AstCheckState
|
astChecks: AstCheckState
|
||||||
flowchartMode: FlowchartMode
|
flowchartMode: FlowchartMode
|
||||||
mermaidCode: string
|
mermaidCode: string
|
||||||
flowchartHint: string
|
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const tab = defineModel<PreviewTab>("tab", { required: true })
|
const tab = defineModel<PreviewTab>("tab", { required: true })
|
||||||
@@ -248,18 +247,15 @@ onMounted(() => nextTick(render))
|
|||||||
<!-- 流程图:一直挂着,改了图的代码随时知道画不画得出来 -->
|
<!-- 流程图:一直挂着,改了图的代码随时知道画不画得出来 -->
|
||||||
<div v-show="tab === 'flowchart'" class="page">
|
<div v-show="tab === 'flowchart'" class="page">
|
||||||
<p v-if="flowchartMode === 'none'" class="placeholder">这道题不用流程图</p>
|
<p v-if="flowchartMode === 'none'" class="placeholder">这道题不用流程图</p>
|
||||||
<p v-else-if="flowchartMode === 'draw'" class="muted small">
|
<p v-else-if="flowchartMode === 'draw'" class="placeholder">
|
||||||
学生自己画,这张图<b>学生看不到</b>,只拿来给学生画的图打分。
|
学生在题目页右边自己画,交上去按例子和测试数据评
|
||||||
<template v-if="flowchartHint.trim()">学生看到的提示:{{ flowchartHint }}</template>
|
|
||||||
</p>
|
</p>
|
||||||
<p v-else class="muted small">学生在题面里点开「老师给的参考流程图」看到的是:</p>
|
<template v-else>
|
||||||
<p v-if="flowchartMode !== 'none' && !mermaidCode.trim()" class="placeholder">
|
<p class="muted small">学生在题面里点开「老师给的参考流程图」看到的是:</p>
|
||||||
还没写图的代码
|
<p v-if="!mermaidCode.trim()" class="placeholder">还没画</p>
|
||||||
</p>
|
<div v-if="renderError" class="chartError">画不出来:{{ renderError }}</div>
|
||||||
<div v-if="renderError && flowchartMode !== 'none'" class="chartError">
|
</template>
|
||||||
画不出来:{{ renderError }}
|
<div v-show="flowchartMode === 'show'" ref="chart" class="chart"></div>
|
||||||
</div>
|
|
||||||
<div v-show="flowchartMode !== 'none'" ref="chart" class="chart"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -42,8 +42,17 @@ import {
|
|||||||
type TemplateParts,
|
type TemplateParts,
|
||||||
} from "./components/codeTemplate"
|
} from "./components/codeTemplate"
|
||||||
import type { AstCheckState, FlowchartMode, PreviewTab } from "./components/editorTypes"
|
import type { AstCheckState, FlowchartMode, PreviewTab } from "./components/editorTypes"
|
||||||
|
import {
|
||||||
|
emptyDoc,
|
||||||
|
parseDoc,
|
||||||
|
toMermaid,
|
||||||
|
type FlowDoc,
|
||||||
|
} from "shared/components/FlowchartEditor/model"
|
||||||
|
|
||||||
const CodeEditor = defineAsyncComponent(() => import("shared/components/CodeEditor.vue"))
|
const CodeEditor = defineAsyncComponent(() => import("shared/components/CodeEditor.vue"))
|
||||||
|
const FlowchartEditor = defineAsyncComponent(
|
||||||
|
() => import("shared/components/FlowchartEditor/index.vue"),
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 编程题(C / C++ / Python)的出题页,设计稿「编程题出题页」方案 B:左边写,右边是学生
|
* 编程题(C / C++ / Python)的出题页,设计稿「编程题出题页」方案 B:左边写,右边是学生
|
||||||
@@ -93,6 +102,8 @@ interface CodeForm {
|
|||||||
astRules: AstRules | null
|
astRules: AstRules | null
|
||||||
flowchartMode: FlowchartMode
|
flowchartMode: FlowchartMode
|
||||||
mermaidCode: string
|
mermaidCode: string
|
||||||
|
/** 画布画的参考流程图;null 是老题(只有手写的 Mermaid)或者还没画 */
|
||||||
|
flowchartDoc: FlowDoc | null
|
||||||
flowchartHint: string
|
flowchartHint: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +129,7 @@ const blankForm = (): CodeForm => ({
|
|||||||
astRules: null,
|
astRules: null,
|
||||||
flowchartMode: "none",
|
flowchartMode: "none",
|
||||||
mermaidCode: "",
|
mermaidCode: "",
|
||||||
|
flowchartDoc: null,
|
||||||
flowchartHint: "",
|
flowchartHint: "",
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -437,13 +449,26 @@ const FLOWCHART_LABEL: Record<FlowchartMode, string> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mermaidOk = ref(false)
|
const mermaidOk = ref(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 老题的参考图是手写的 Mermaid(没有结构树):照旧给代码框;新画的、或者点了「改用画布画」
|
||||||
|
* 的用画布。保存时画布的图以它为准,后端照它重新生成 Mermaid
|
||||||
|
*/
|
||||||
|
const legacyMermaid = computed(() => !form.flowchartDoc && !!form.mermaidCode.trim())
|
||||||
|
/** 右边预览画的那张:画布有图就照画布生成,不然是老的代码 */
|
||||||
|
const previewMermaid = computed(() => {
|
||||||
|
if (!form.flowchartDoc) return form.mermaidCode
|
||||||
|
return form.flowchartDoc.steps.length ? toMermaid(form.flowchartDoc) : ""
|
||||||
|
})
|
||||||
const aiDrawing = ref(false)
|
const aiDrawing = ref(false)
|
||||||
|
|
||||||
async function aiFlowchart() {
|
async function aiFlowchart() {
|
||||||
aiDrawing.value = true
|
aiDrawing.value = true
|
||||||
try {
|
try {
|
||||||
const res = await generateFlowchartFromPythonCode(form.answers.Python)
|
const res = await generateFlowchartFromPythonCode(form.answers.Python)
|
||||||
form.mermaidCode = res.flowchart
|
const doc = parseDoc(res.doc)
|
||||||
|
if (!doc) throw new Error("AI 画出来的图读不了")
|
||||||
|
form.flowchartDoc = doc
|
||||||
preview.value = "flowchart"
|
preview.value = "flowchart"
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
message.error(errorMessage(err, "生成失败,请稍后再试"))
|
message.error(errorMessage(err, "生成失败,请稍后再试"))
|
||||||
@@ -599,6 +624,7 @@ async function loadProblem(data: AdminProblem) {
|
|||||||
astRules: data.astRules ?? null,
|
astRules: data.astRules ?? null,
|
||||||
flowchartMode: data.allowFlowchart ? "draw" : data.showFlowchart ? "show" : "none",
|
flowchartMode: data.allowFlowchart ? "draw" : data.showFlowchart ? "show" : "none",
|
||||||
mermaidCode: data.mermaidCode ?? "",
|
mermaidCode: data.mermaidCode ?? "",
|
||||||
|
flowchartDoc: parseDoc(data.flowchartData),
|
||||||
flowchartHint: data.flowchartHint ?? "",
|
flowchartHint: data.flowchartHint ?? "",
|
||||||
} satisfies CodeForm)
|
} satisfies CodeForm)
|
||||||
answerTab.value = RUNNABLE.find((l) => languages.includes(l) && answers[l].trim()) ?? "Python"
|
answerTab.value = RUNNABLE.find((l) => languages.includes(l) && answers[l].trim()) ?? "Python"
|
||||||
@@ -703,10 +729,17 @@ const blockers = computed<Blocker[]>(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (form.flowchartMode !== "none") {
|
if (form.flowchartMode === "show") {
|
||||||
if (!form.mermaidCode.trim()) list.push({ text: "写流程图的代码", section: "flowchart" })
|
if (legacyMermaid.value) {
|
||||||
else if (!mermaidOk.value)
|
if (!mermaidOk.value)
|
||||||
list.push({ text: "改好流程图的代码(画不出来)", section: "flowchart", severe: true })
|
list.push({ text: "改好流程图的代码(画不出来)", section: "flowchart", severe: true })
|
||||||
|
} else if (!form.flowchartDoc?.steps.length) {
|
||||||
|
list.push({ text: "画参考流程图", section: "flowchart" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 学生画的图拼成程序拿例子和测试数据跑(flowchart/program.ts),一组例子都没有就没得评
|
||||||
|
if (form.flowchartMode === "draw" && !examples.value.length) {
|
||||||
|
list.push({ text: "勾至少一组测试数据当例子(流程图照例子评)", section: "cases" })
|
||||||
}
|
}
|
||||||
return list
|
return list
|
||||||
})
|
})
|
||||||
@@ -775,6 +808,10 @@ async function save() {
|
|||||||
allowFlowchart: form.flowchartMode === "draw",
|
allowFlowchart: form.flowchartMode === "draw",
|
||||||
showFlowchart: form.flowchartMode === "show",
|
showFlowchart: form.flowchartMode === "show",
|
||||||
mermaidCode: form.flowchartMode === "none" ? form.mermaidCode : form.mermaidCode.trim(),
|
mermaidCode: form.flowchartMode === "none" ? form.mermaidCode : form.mermaidCode.trim(),
|
||||||
|
flowchartData:
|
||||||
|
form.flowchartMode === "show" && form.flowchartDoc?.steps.length
|
||||||
|
? { ...form.flowchartDoc }
|
||||||
|
: null,
|
||||||
flowchartHint: form.flowchartHint.trim(),
|
flowchartHint: form.flowchartHint.trim(),
|
||||||
astRules: form.astRules,
|
astRules: form.astRules,
|
||||||
sqlConfig: null,
|
sqlConfig: null,
|
||||||
@@ -1196,15 +1233,27 @@ onMounted(async () => {
|
|||||||
<label class="mode" :class="{ on: form.flowchartMode === 'draw' }">
|
<label class="mode" :class="{ on: form.flowchartMode === 'draw' }">
|
||||||
<n-radio value="draw" />
|
<n-radio value="draw" />
|
||||||
<span>
|
<span>
|
||||||
<b>让学生自己画,按这张图打分</b>
|
<b>让学生自己画</b>
|
||||||
<small>这张图学生看不到,只拿来给学生画的图打分;画到 A / S 级算做完</small>
|
<small>按例子和测试数据评;画到 A / S 级算做完</small>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</n-radio-group>
|
</n-radio-group>
|
||||||
<template v-if="form.flowchartMode !== 'none'">
|
<!-- 学生画:按例子和测试数据评(拼成程序跑),不再需要参考图 -->
|
||||||
|
<n-text v-if="form.flowchartMode === 'draw'" depth="3" class="note">
|
||||||
|
学生画的图会拼成程序,拿这道题的例子和测试数据跑一遍,逐组看输出对不对,不用准备参考图。
|
||||||
|
</n-text>
|
||||||
|
<template v-if="form.flowchartMode === 'show'">
|
||||||
<div class="inlineRow">
|
<div class="inlineRow">
|
||||||
<span class="label">图的代码(Mermaid)</span>
|
<span class="label">{{ legacyMermaid ? "图的代码(Mermaid)" : "参考流程图" }}</span>
|
||||||
<span class="grow"></span>
|
<span class="grow"></span>
|
||||||
|
<n-button
|
||||||
|
v-if="legacyMermaid"
|
||||||
|
size="tiny"
|
||||||
|
quaternary
|
||||||
|
@click="form.flowchartDoc = emptyDoc()"
|
||||||
|
>
|
||||||
|
改用画布画
|
||||||
|
</n-button>
|
||||||
<n-button
|
<n-button
|
||||||
size="tiny"
|
size="tiny"
|
||||||
:loading="aiDrawing"
|
:loading="aiDrawing"
|
||||||
@@ -1215,25 +1264,20 @@ onMounted(async () => {
|
|||||||
AI 照 Python 答案画一张
|
AI 照 Python 答案画一张
|
||||||
</n-button>
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
|
<template v-if="legacyMermaid">
|
||||||
<n-input
|
<n-input
|
||||||
v-model:value="form.mermaidCode"
|
v-model:value="form.mermaidCode"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
class="mono"
|
class="mono"
|
||||||
:autosize="{ minRows: 6, maxRows: 18 }"
|
:autosize="{ minRows: 6, maxRows: 18 }"
|
||||||
placeholder="graph TD A[开始] --> B[输入 n]"
|
|
||||||
/>
|
/>
|
||||||
<n-text
|
<n-text :type="mermaidOk ? 'success' : 'error'" class="note">
|
||||||
v-if="form.mermaidCode.trim()"
|
|
||||||
:type="mermaidOk ? 'success' : 'error'"
|
|
||||||
class="note"
|
|
||||||
>
|
|
||||||
{{ mermaidOk ? "✓ 画得出来(右边就是)" : "✗ 还画不出来,看右边的报错" }}
|
{{ mermaidOk ? "✓ 画得出来(右边就是)" : "✗ 还画不出来,看右边的报错" }}
|
||||||
</n-text>
|
</n-text>
|
||||||
<n-input
|
</template>
|
||||||
v-if="form.flowchartMode === 'draw'"
|
<div v-else class="flowCanvas">
|
||||||
v-model:value="form.flowchartHint"
|
<FlowchartEditor v-model:value="form.flowchartDoc" :draft="false" height="460px" />
|
||||||
placeholder="给学生的提示(选填),比如:先判断门是不是开着"
|
</div>
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</section>
|
</section>
|
||||||
@@ -1326,8 +1370,7 @@ onMounted(async () => {
|
|||||||
:ast-rules="form.astRules"
|
:ast-rules="form.astRules"
|
||||||
:ast-checks="astChecks"
|
:ast-checks="astChecks"
|
||||||
:flowchart-mode="form.flowchartMode"
|
:flowchart-mode="form.flowchartMode"
|
||||||
:mermaid-code="form.mermaidCode"
|
:mermaid-code="previewMermaid"
|
||||||
:flowchart-hint="form.flowchartHint"
|
|
||||||
@render-state="mermaidOk = $event"
|
@render-state="mermaidOk = $event"
|
||||||
@collapse="previewCollapsed = true"
|
@collapse="previewCollapsed = true"
|
||||||
/>
|
/>
|
||||||
@@ -1658,4 +1701,10 @@ onMounted(async () => {
|
|||||||
.blocker:hover {
|
.blocker:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.flowCanvas {
|
||||||
|
border: 1px solid v-bind("theme.borderColor");
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -30,8 +30,14 @@ import { layoutDoc, PLACEHOLDER, type LaidNode, type LaidSlot } from "./layout"
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
height?: string
|
height?: string
|
||||||
|
/**
|
||||||
|
* 题目页(默认):每道题一份本地草稿,自动存。
|
||||||
|
* false:图交给外面管(后台出题页的参考流程图,v-model:value),不存草稿
|
||||||
|
*/
|
||||||
|
draft?: boolean
|
||||||
}
|
}
|
||||||
const { height = "calc(100vh - 134px)" } = defineProps<Props>()
|
const { height = "calc(100vh - 134px)", draft = true } = defineProps<Props>()
|
||||||
|
const model = defineModel<FlowDoc | null>("value", { default: null })
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const theme = useThemeVars()
|
const theme = useThemeVars()
|
||||||
@@ -98,6 +104,13 @@ const cacheKey = computed(() => {
|
|||||||
const savedAt = ref<Date | null>(null)
|
const savedAt = ref<Date | null>(null)
|
||||||
|
|
||||||
function loadDraft() {
|
function loadDraft() {
|
||||||
|
if (!draft) {
|
||||||
|
doc.value = model.value ? cloneDoc(model.value) : emptyDoc()
|
||||||
|
deselect()
|
||||||
|
resetHistory()
|
||||||
|
fitSoon()
|
||||||
|
return
|
||||||
|
}
|
||||||
let saved: unknown = null
|
let saved: unknown = null
|
||||||
try {
|
try {
|
||||||
saved = storage.get(cacheKey.value)
|
saved = storage.get(cacheKey.value)
|
||||||
@@ -135,12 +148,20 @@ watch(
|
|||||||
doc,
|
doc,
|
||||||
() => {
|
() => {
|
||||||
if (loading) return
|
if (loading) return
|
||||||
saveDraft(cacheKey.value)
|
if (draft) saveDraft(cacheKey.value)
|
||||||
|
else model.value = cloneDoc(doc.value)
|
||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/** 外面换了一张图(后台点了「AI 照答案画一张」):摆上来,进历史,Ctrl+Z 能回去 */
|
||||||
|
watch(model, (next) => {
|
||||||
|
if (draft || !next || signature(next) === signature(doc.value)) return
|
||||||
|
setDoc(next)
|
||||||
|
})
|
||||||
|
|
||||||
watch(cacheKey, () => {
|
watch(cacheKey, () => {
|
||||||
|
if (!draft) return
|
||||||
loading = true
|
loading = true
|
||||||
loadDraft()
|
loadDraft()
|
||||||
nextTick(() => (loading = false))
|
nextTick(() => (loading = false))
|
||||||
|
|||||||
@@ -143,7 +143,10 @@ type ExcludeKeys =
|
|||||||
| "isPublic"
|
| "isPublic"
|
||||||
| "contestId"
|
| "contestId"
|
||||||
|
|
||||||
export type BlankProblem = Omit<AdminProblem, ExcludeKeys | "hint" | "mermaidCode"> & {
|
export type BlankProblem = Omit<
|
||||||
|
AdminProblem,
|
||||||
|
ExcludeKeys | "hint" | "mermaidCode" | "flowchartData"
|
||||||
|
> & {
|
||||||
id?: number
|
id?: number
|
||||||
// 新建比赛题时由 detail.vue 在提交前写进来
|
// 新建比赛题时由 detail.vue 在提交前写进来
|
||||||
contestId?: number | null
|
contestId?: number | null
|
||||||
@@ -151,6 +154,8 @@ export type BlankProblem = Omit<AdminProblem, ExcludeKeys | "hint" | "mermaidCod
|
|||||||
// v-model 要的是 lvalue,模板里没法 ?? 兜底,所以在类型上就收掉 null
|
// v-model 要的是 lvalue,模板里没法 ?? 兜底,所以在类型上就收掉 null
|
||||||
hint: string
|
hint: string
|
||||||
mermaidCode: string
|
mermaidCode: string
|
||||||
|
/** 画布画的参考流程图(FlowDoc);不给就照存 mermaidCode(老题手写的 Mermaid) */
|
||||||
|
flowchartData?: Record<string, unknown> | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProblemRow {
|
export interface ProblemRow {
|
||||||
|
|||||||
@@ -675,8 +675,9 @@ export const generateFlowchartRequestSchema = z.object({
|
|||||||
.min(1)
|
.min(1)
|
||||||
.max(64 * 1024),
|
.max(64 * 1024),
|
||||||
})
|
})
|
||||||
|
/** AI 照 Python 答案画的参考流程图:结构树(FlowDoc),后台画布直接摆上去接着改 */
|
||||||
export const generateFlowchartResponseSchema = z.object({
|
export const generateFlowchartResponseSchema = z.object({
|
||||||
flowchart: z.string(),
|
doc: z.record(z.string(), z.unknown()),
|
||||||
})
|
})
|
||||||
|
|
||||||
// ---------------------------------------------------------------- 题目管理
|
// ---------------------------------------------------------------- 题目管理
|
||||||
@@ -771,6 +772,8 @@ export const adminProblemSchema = z.object({
|
|||||||
allowFlowchart: z.boolean(),
|
allowFlowchart: z.boolean(),
|
||||||
showFlowchart: z.boolean(),
|
showFlowchart: z.boolean(),
|
||||||
mermaidCode: z.string().nullable(),
|
mermaidCode: z.string().nullable(),
|
||||||
|
/** 参考流程图的结构树(FlowDoc);2026-10 之前出的题只有 mermaidCode,这里是空对象 */
|
||||||
|
flowchartData: z.record(z.string(), z.unknown()),
|
||||||
flowchartHint: z.string().nullable(),
|
flowchartHint: z.string().nullable(),
|
||||||
astRules: astRulesSchema.nullable(),
|
astRules: astRulesSchema.nullable(),
|
||||||
answers: z.array(problemAnswerSchema),
|
answers: z.array(problemAnswerSchema),
|
||||||
@@ -823,6 +826,11 @@ export const createProblemRequestSchema = z.object({
|
|||||||
allowFlowchart: z.boolean().default(false),
|
allowFlowchart: z.boolean().default(false),
|
||||||
showFlowchart: z.boolean().default(false),
|
showFlowchart: z.boolean().default(false),
|
||||||
mermaidCode: z.string().nullable().default(null),
|
mermaidCode: z.string().nullable().default(null),
|
||||||
|
/**
|
||||||
|
* 用画布画的参考流程图(FlowDoc)。给了就以它为准,mermaidCode 由后端照它重新生成;
|
||||||
|
* 不给(老题只有手写的 Mermaid)就原样存 mermaidCode
|
||||||
|
*/
|
||||||
|
flowchartData: z.record(z.string(), z.unknown()).nullable().default(null),
|
||||||
flowchartHint: z.string().nullable().default(null),
|
flowchartHint: z.string().nullable().default(null),
|
||||||
astRules: astRulesSchema.nullable().default(null),
|
astRules: astRulesSchema.nullable().default(null),
|
||||||
// order_sensitive 缺省补 false —— 与旧后端 SQLConfigSerializer 的
|
// order_sensitive 缺省补 false —— 与旧后端 SQLConfigSerializer 的
|
||||||
|
|||||||
Reference in new issue
Block a user