diff --git a/apps/web/src/shared/components/FlowchartEditor/index.vue b/apps/web/src/shared/components/FlowchartEditor/index.vue index 91125fc..d4556bb 100644 --- a/apps/web/src/shared/components/FlowchartEditor/index.vue +++ b/apps/web/src/shared/components/FlowchartEditor/index.vue @@ -19,6 +19,7 @@ import { getNodeTypeConfig } from "./useNodeStyles" import { useHistory } from "./useHistory" import { useFlowOperations } from "./useFlowOperations" import { useCache } from "./useCache" +import { toPortableEdges, toPortableNodes } from "./serialize" import CustomNode from "./CustomNode.vue" import { useProblemStore } from "oj/store/problem" @@ -120,10 +121,21 @@ const handleRedo = () => { } } -// 清空画布 +// 清空画布。工具栏那个按钮点一下就全没了,且 clearCache() 会连本题存着的草稿 +// 一起删掉,刷新也找不回来 —— 学生误点的代价太大,加一道确认 +const dialog = useDialog() const handleClear = () => { - clearCanvas() - clearCache() + if (nodes.value.length === 0 && edges.value.length === 0) return + dialog.warning({ + title: "清空画布", + content: "画布上的内容会被清掉,这道题存着的草稿也会一起删除。确定吗?", + positiveText: "清空", + negativeText: "再想想", + onPositiveClick: () => { + clearCanvas() + clearCache() + }, + }) } // 键盘事件 @@ -139,10 +151,12 @@ const handleKeyDown = (event: KeyboardEvent) => { } if (event.ctrlKey || event.metaKey) { - if (event.key === "z" && !event.shiftKey) { + const key = event.key.toLowerCase() + if (key === "z" && !event.shiftKey) { event.preventDefault() handleUndo() - } else if (event.key === "z" && event.shiftKey) { + } else if ((key === "z" && event.shiftKey) || key === "y") { + // 工具栏上的提示写的就是 Ctrl+Y,但原来只实现了 Ctrl+Shift+Z,按 Y 没反应 event.preventDefault() handleRedo() } @@ -188,9 +202,10 @@ const setFlowchartData = (data: { nodes: Node[]; edges: Edge[] }) => { defineExpose({ nodes, edges, + // 提交出去的这份会压缩后长期存进数据库,同样裁掉运行时内部字段 getFlowchartData: () => ({ - nodes: nodes.value, - edges: edges.value, + nodes: toPortableNodes(nodes.value), + edges: toPortableEdges(edges.value), }), setFlowchartData, }) diff --git a/apps/web/src/shared/components/FlowchartEditor/serialize.ts b/apps/web/src/shared/components/FlowchartEditor/serialize.ts new file mode 100644 index 0000000..e9bb63b --- /dev/null +++ b/apps/web/src/shared/components/FlowchartEditor/serialize.ts @@ -0,0 +1,45 @@ +import type { Edge, Node } from "@vue-flow/core" + +/** + * 把画布上的节点/连线裁成「可持久化」的形状。 + * + * 画布里的 node 是 vue-flow 的 GraphNode:除了我们自己塞进去的字段,它还挂着 + * `dimensions` / `computedPosition` / `handleBounds` / `selected` / `dragging` / + * `resizing` / `initialized` / `isParent` / `events` 一堆运行时内部状态(见 + * vue-flow 的 parseNode)。这些东西会跟着一起: + * + * - 写进 localStorage(每次改动都写一次) + * - 进 20 份历史快照,每份都要 JSON 深拷贝一遍 + * - 压缩后提交进数据库,长期存着 + * + * 实测一个两节点的图就能撑到 600+ 字节,其中大半是 handleBounds。而重新挂载时 + * 这些字段全都会被重新计算,存下来没有任何意义 —— 还会把存档格式和 vue-flow + * 的内部实现绑死,将来升级或迁移数据都要跟着动。 + * + * `style` 保留:它是建节点时按类型算好的(见 useNodeStyles),丢了会让恢复出来 + * 的图变样。 + */ +export function toPortableNodes(nodes: Node[]) { + return nodes.map((node) => ({ + id: node.id, + type: node.type, + position: { + x: node.position?.x ?? 0, + y: node.position?.y ?? 0, + }, + data: node.data, + style: node.style, + })) as Node[] +} + +export function toPortableEdges(edges: Edge[]) { + return edges.map((edge) => ({ + id: edge.id, + source: edge.source, + target: edge.target, + sourceHandle: edge.sourceHandle ?? null, + targetHandle: edge.targetHandle ?? null, + type: edge.type, + label: edge.label, + })) as Edge[] +} diff --git a/apps/web/src/shared/components/FlowchartEditor/useCache.ts b/apps/web/src/shared/components/FlowchartEditor/useCache.ts index 2f61556..26daf31 100644 --- a/apps/web/src/shared/components/FlowchartEditor/useCache.ts +++ b/apps/web/src/shared/components/FlowchartEditor/useCache.ts @@ -1,6 +1,7 @@ import { ref, toValue, watch, type Ref, type MaybeRefOrGetter } from "vue" import { useStorage, useDebounceFn } from "@vueuse/core" import type { Node, Edge } from "@vue-flow/core" +import { toPortableEdges, toPortableNodes } from "./serialize" /** * 缓存管理 - 使用 @vueuse 的 useStorage @@ -28,8 +29,8 @@ export function useCache( // 防抖保存:isSaving 在 watch 中置 true,保存完成后置 false,使 UI 能感知保存中状态 const debouncedSave = useDebounceFn(() => { - storedData.value.nodes = nodes.value - storedData.value.edges = edges.value + storedData.value.nodes = toPortableNodes(nodes.value) + storedData.value.edges = toPortableEdges(edges.value) storedData.value.timestamp = new Date().toISOString() lastSaved.value = new Date() hasUnsavedChanges.value = false @@ -39,8 +40,8 @@ export function useCache( // 立即保存 const saveToCache = () => { isSaving.value = true - storedData.value.nodes = nodes.value - storedData.value.edges = edges.value + storedData.value.nodes = toPortableNodes(nodes.value) + storedData.value.edges = toPortableEdges(edges.value) storedData.value.timestamp = new Date().toISOString() lastSaved.value = new Date() hasUnsavedChanges.value = false diff --git a/apps/web/src/shared/components/FlowchartEditor/useHistory.ts b/apps/web/src/shared/components/FlowchartEditor/useHistory.ts index 94ced95..e4bf0cf 100644 --- a/apps/web/src/shared/components/FlowchartEditor/useHistory.ts +++ b/apps/web/src/shared/components/FlowchartEditor/useHistory.ts @@ -1,5 +1,6 @@ import { shallowRef, computed } from "vue" import type { Node, Edge } from "@vue-flow/core" +import { toPortableEdges, toPortableNodes } from "./serialize" /** * 简化的历史记录管理 @@ -18,7 +19,14 @@ export function useHistory() { nodes: Node[], edges: Edge[], ): { nodes: Node[]; edges: Edge[] } => - JSON.parse(JSON.stringify({ nodes, edges })) as { + // 先裁掉 vue-flow 的运行时内部字段再深拷贝:20 份快照 × 每个节点几百字节的 + // handleBounds/dimensions,纯属白拷 + JSON.parse( + JSON.stringify({ + nodes: toPortableNodes(nodes), + edges: toPortableEdges(edges), + }), + ) as { nodes: Node[] edges: Edge[] }