feat(流程图): 画流程图不用再拖框连线——点箭头上的 + 加一步,判断自动分出是 / 否,排版全自动
原来要从节点库拖框、对准连接点拉线、自己摆位置,还能画出断线和孤立的框; 画布上全是彩色圆角块,交上去的却是平行四边形和菱形。现在照画布稿 FlowEdB: - 图存成一棵结构树(顺序 / 判断 / 循环),开始、结束固定在两头,排版是纯函数 - 每根箭头上一个 +,点了选输入 / 输出 / 处理 / 判断 / 循环;点框直接改字,右上角「删除」 - 形状照国标流程图,画布、缩略图、交给 AI 的 Mermaid 是同一套;缩略图配色跟着画布改 - 已保存时间、撤销、重做挪到工具栏,「清空画布」收进「⋯」;空框交不上去 - 去掉 vue-flow 三个包(编辑器打包后 16 KB),删掉没人进的 /flowchart 页 2026-10 之前交的图是 vue-flow 格式,只能看缩略图、载不回画布;旧草稿键名不同,不去碰。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ce26925e79
commit
09f8a56913
25 files changed
+1670
-2102
No files matched your search
@@ -9,6 +9,7 @@ import { compressToBase64, copyToClipboard } from "utils/functions"
|
||||
import storage from "utils/storage"
|
||||
import { useDraftKey } from "./draftKey"
|
||||
import { useTeacherCollab } from "./teacherCollab"
|
||||
import { useFlowchartStore } from "oj/store/flowchart"
|
||||
import { useProblemPageContext } from "./problemPageContext"
|
||||
|
||||
/**
|
||||
@@ -48,7 +49,9 @@ export function useEditorMenu(statisticsInline: Ref<boolean>) {
|
||||
if (ctx.value.classStats && !statisticsInline.value && userStore.isTeacherOrAbove) {
|
||||
items.push({ label: "课堂统计", key: "statistics" })
|
||||
}
|
||||
if (codeStore.code.language !== "Flowchart") {
|
||||
if (codeStore.code.language === "Flowchart") {
|
||||
items.push({ label: "清空画布", key: "clearFlowchart" })
|
||||
} else {
|
||||
if (codeStore.code.language !== "SQL") items.push({ label: "去自测猫", key: "testcat" })
|
||||
items.push({ label: "复制代码", key: "copy" })
|
||||
// 协作中的教师不给「重置代码」:那会儿编辑器里是**学生的**代码,而 v-model
|
||||
@@ -80,6 +83,23 @@ export function useEditorMenu(statisticsInline: Ref<boolean>) {
|
||||
})
|
||||
}
|
||||
|
||||
// 清空会连这道题存着的草稿一起删掉(空图不留草稿),点错了只剩 Ctrl+Z
|
||||
const flowchartStore = useFlowchartStore()
|
||||
function clearFlowchart() {
|
||||
const editor = flowchartStore.editor
|
||||
if (!editor) return
|
||||
dialog.warning({
|
||||
title: "清空画布",
|
||||
content: "画布上的图会全部清掉。确定吗?",
|
||||
positiveText: "清空",
|
||||
negativeText: "再想想",
|
||||
onPositiveClick: () => {
|
||||
editor.clear()
|
||||
message.success("已清空,按 Ctrl+Z 可以撤回")
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function goTestCat() {
|
||||
const data = {
|
||||
lang: LANGUAGE_FORMAT_VALUE[codeStore.code.language],
|
||||
@@ -124,6 +144,9 @@ export function useEditorMenu(statisticsInline: Ref<boolean>) {
|
||||
case "reset":
|
||||
reset()
|
||||
return true
|
||||
case "clearFlowchart":
|
||||
clearFlowchart()
|
||||
return true
|
||||
case "edit":
|
||||
goEdit()
|
||||
return true
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
/**
|
||||
* 节点/连线标签是学生自己敲的任意文字,而下面是直接把它塞进 "..." 里。
|
||||
* 出现一个双引号就会把 mermaid 语法撑破:图渲染不出来,坏掉的代码还会原样送去
|
||||
* 给 AI 打分,学生完全不知道自己被扣分是因为一个引号。
|
||||
*
|
||||
* mermaid 用 `#NN;` 形式的实体转义。`#` 必须**先**转,否则标签里本来就有的
|
||||
* `#quot;` 之类会被当成实体解释。换行转成 <br/>,不然会截断整条语句。
|
||||
*/
|
||||
function escapeLabel(raw: unknown) {
|
||||
return String(raw ?? "")
|
||||
.replace(/#/g, "#35;")
|
||||
.replace(/"/g, "#quot;")
|
||||
.replace(/\r?\n/g, "<br/>")
|
||||
}
|
||||
|
||||
/**
|
||||
* 将流程图 JSON 数据转换为 Mermaid 格式
|
||||
*/
|
||||
export function useMermaidConverter() {
|
||||
const convertToMermaid = (flowchartData: any) => {
|
||||
const nodes = flowchartData?.nodes
|
||||
// edges 原来没做空值保护(nodes 有),拿到 undefined 会直接抛
|
||||
const edges = flowchartData?.edges ?? []
|
||||
|
||||
if (!nodes || nodes.length === 0) {
|
||||
return "graph TD\n A[空流程图]"
|
||||
}
|
||||
|
||||
let mermaid = "graph TD\n"
|
||||
|
||||
// Build safe ID mapping to prevent Mermaid syntax errors from special characters
|
||||
const idMap = new Map<string, string>()
|
||||
nodes.forEach((node: any, index: number) => {
|
||||
idMap.set(node.id, `node_${index}`)
|
||||
})
|
||||
const safeId = (id: string) => idMap.get(id) || id.replace(/[^a-zA-Z0-9_]/g, "_")
|
||||
|
||||
// 处理节点 - 根据原始类型和自定义标签
|
||||
nodes.forEach((node: any) => {
|
||||
const nodeId = safeId(node.id)
|
||||
const label = escapeLabel(node.data?.customLabel || node.data?.label || "节点")
|
||||
const originalType = node.data?.originalType || node.type
|
||||
|
||||
// 根据节点原始类型确定Mermaid语法
|
||||
switch (originalType) {
|
||||
case "start":
|
||||
mermaid += ` ${nodeId}(("${label}"))\n`
|
||||
break
|
||||
case "end":
|
||||
mermaid += ` ${nodeId}(("${label}"))\n`
|
||||
break
|
||||
case "input":
|
||||
// 输入框使用平行四边形
|
||||
mermaid += ` ${nodeId}[/"${label}"/]\n`
|
||||
break
|
||||
case "output":
|
||||
// 输出框使用平行四边形
|
||||
mermaid += ` ${nodeId}[/"${label}"/]\n`
|
||||
break
|
||||
case "default":
|
||||
mermaid += ` ${nodeId}["${label}"]\n`
|
||||
break
|
||||
case "decision":
|
||||
mermaid += ` ${nodeId}{"${label}"}\n`
|
||||
break
|
||||
case "loop":
|
||||
// 循环使用菱形
|
||||
mermaid += ` ${nodeId}{"${label}"}\n`
|
||||
break
|
||||
default:
|
||||
mermaid += ` ${nodeId}["${label}"]\n`
|
||||
}
|
||||
})
|
||||
|
||||
// 处理边
|
||||
edges.forEach((edge: any) => {
|
||||
const source = safeId(edge.source)
|
||||
const target = safeId(edge.target)
|
||||
const rawLabel = String(edge.label ?? "")
|
||||
|
||||
if (rawLabel.trim() !== "") {
|
||||
mermaid += ` ${source} -->|"${escapeLabel(rawLabel)}"| ${target}\n`
|
||||
} else {
|
||||
mermaid += ` ${source} --> ${target}\n`
|
||||
}
|
||||
})
|
||||
|
||||
// 添加样式定义来区分不同类型的节点
|
||||
mermaid += "\n"
|
||||
mermaid += " classDef startNode fill:#dcfce7,stroke:#16a34a,stroke-width:2px,color:#0f172a\n"
|
||||
mermaid += " classDef endNode fill:#fee2e2,stroke:#dc2626,stroke-width:2px,color:#0f172a\n"
|
||||
mermaid += " classDef input fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#0f172a\n"
|
||||
mermaid += " classDef output fill:#ede9fe,stroke:#7c3aed,stroke-width:2px,color:#0f172a\n"
|
||||
mermaid += " classDef process fill:#f0f9ff,stroke:#0284c7,stroke-width:2px,color:#0f172a\n"
|
||||
mermaid += " classDef decision fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#0f172a\n"
|
||||
mermaid += " classDef loop fill:#fae8ff,stroke:#c026d3,stroke-width:2px,color:#0f172a\n"
|
||||
mermaid += "\n"
|
||||
|
||||
// 为节点应用样式
|
||||
nodes.forEach((node: any) => {
|
||||
const nodeId = safeId(node.id)
|
||||
const originalType = node.data?.originalType || node.type
|
||||
|
||||
switch (originalType) {
|
||||
case "start":
|
||||
mermaid += ` class ${nodeId} startNode\n`
|
||||
break
|
||||
case "end":
|
||||
mermaid += ` class ${nodeId} endNode\n`
|
||||
break
|
||||
case "input":
|
||||
mermaid += ` class ${nodeId} input\n`
|
||||
break
|
||||
case "output":
|
||||
mermaid += ` class ${nodeId} output\n`
|
||||
break
|
||||
case "decision":
|
||||
mermaid += ` class ${nodeId} decision\n`
|
||||
break
|
||||
case "loop":
|
||||
mermaid += ` class ${nodeId} loop\n`
|
||||
break
|
||||
default:
|
||||
mermaid += ` class ${nodeId} process\n`
|
||||
}
|
||||
})
|
||||
|
||||
return mermaid
|
||||
}
|
||||
|
||||
return {
|
||||
convertToMermaid,
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user