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
@@ -17,6 +17,7 @@ import { openStatistics, useEditorMenu } from "../composables/editorMenu"
|
||||
import { useTeacherCollab } from "../composables/teacherCollab"
|
||||
|
||||
const SubmitFlowchart = defineAsyncComponent(() => import("./SubmitFlowchart.vue"))
|
||||
const FlowchartControls = defineAsyncComponent(() => import("./FlowchartControls.vue"))
|
||||
|
||||
const collabStore = useCollabStore()
|
||||
|
||||
@@ -211,6 +212,8 @@ const changeLanguage = (v: LANGUAGE) => {
|
||||
|
||||
<div class="spacer" />
|
||||
|
||||
<FlowchartControls v-if="drawing && isDesktop" />
|
||||
|
||||
<n-button
|
||||
v-if="showHelpButton"
|
||||
:size="buttonSize"
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<script setup lang="ts">
|
||||
import { useThemeVars } from "naive-ui"
|
||||
import { storeToRefs } from "pinia"
|
||||
import { useFlowchartStore } from "oj/store/flowchart"
|
||||
import { parseTime } from "utils/functions"
|
||||
|
||||
/**
|
||||
* 画流程图时工具栏右边那一组:已保存几点、撤销、重做(设计稿 FlowEdB)。
|
||||
* 原来这几个和节点库挤在画布右上角的面板里,盖住画好的框;保存状态只是一个 20px 的圆点。
|
||||
* 「清空画布」收进了「⋯」。
|
||||
*/
|
||||
const { editor } = storeToRefs(useFlowchartStore())
|
||||
const theme = useThemeVars()
|
||||
|
||||
const saved = computed(() => {
|
||||
const at = editor.value?.savedAt
|
||||
return at ? `已保存 ${parseTime(at, "HH:mm")}` : ""
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="editor" class="flowchart-controls">
|
||||
<span v-if="saved" class="saved">{{ saved }}</span>
|
||||
<n-button
|
||||
:disabled="!editor.canUndo"
|
||||
aria-label="撤销"
|
||||
title="撤销(Ctrl+Z)"
|
||||
@click="editor.undo()"
|
||||
>
|
||||
<template #icon>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M9 14L4 9l5-5" />
|
||||
<path d="M4 9h10a6 6 0 010 12h-3" />
|
||||
</svg>
|
||||
</template>
|
||||
</n-button>
|
||||
<n-button
|
||||
:disabled="!editor.canRedo"
|
||||
aria-label="重做"
|
||||
title="重做(Ctrl+Y)"
|
||||
@click="editor.redo()"
|
||||
>
|
||||
<template #icon>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M15 14l5-5-5-5" />
|
||||
<path d="M20 9H10a6 6 0 000 12h3" />
|
||||
</svg>
|
||||
</template>
|
||||
</n-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.flowchart-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.saved {
|
||||
font-size: 13px;
|
||||
color: v-bind("theme.textColor3");
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -7,6 +7,7 @@ import { useProblemStore } from "oj/store/problem"
|
||||
import { useUserStore } from "shared/store/user"
|
||||
import { sortFlowchartCriteria } from "utils/constants"
|
||||
import { decompressFromBase64, parseTime } from "utils/functions"
|
||||
import { parseDoc, signature } from "shared/components/FlowchartEditor/model"
|
||||
|
||||
/**
|
||||
* 「结果」页签里流程图作业的那一块:AI 在评 / 评完了几分 / 历次分数 / 评语和改进建议。
|
||||
@@ -104,63 +105,15 @@ function percentType(ratio: number) {
|
||||
}
|
||||
|
||||
// ==================== 载回画布 ====================
|
||||
/** 这一版的画布数据(压缩前的 JSON 字符串);老提交的压缩数据可能是坏的 */
|
||||
const versionJson = computed(() => {
|
||||
/**
|
||||
* 这一版的图(结构树)。2026-10 换成插入式编辑器之前交的是 vue-flow 的格式,读不出来就是
|
||||
* null:那些只能看上面的图,载不回画布。老提交的压缩数据也可能是坏的
|
||||
*/
|
||||
const versionDoc = computed(() => {
|
||||
const data = detail.value?.flowchartData?.data
|
||||
if (typeof data !== "string" || !data) return null
|
||||
try {
|
||||
return decompressFromBase64(data)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 一张图「长什么样」:节点的 id / 类型 / data(字写在里面)/ 位置,连线的两端和标签。不直接比 JSON 字符串 ——
|
||||
* 载回画布时编辑器会补上默认字段(sourceHandle: null 之类),老提交还带着 vue-flow 的运行时
|
||||
* 字段,字符串永远对不上,按钮就一直在。
|
||||
*/
|
||||
type Shape = { nodes?: unknown[]; edges?: unknown[] }
|
||||
function signature(data: Shape) {
|
||||
const nodes = (data.nodes ?? []).map((raw) => {
|
||||
const node = raw as {
|
||||
id?: string
|
||||
type?: string
|
||||
data?: unknown
|
||||
position?: { x?: number; y?: number }
|
||||
}
|
||||
return [
|
||||
node.id,
|
||||
node.type,
|
||||
// 节点上的字(customLabel)和原始类型都在 data 里,整个带上
|
||||
JSON.stringify(node.data ?? {}),
|
||||
Math.round(node.position?.x ?? 0),
|
||||
Math.round(node.position?.y ?? 0),
|
||||
].join("|")
|
||||
})
|
||||
const edges = (data.edges ?? []).map((raw) => {
|
||||
const edge = raw as {
|
||||
source?: string
|
||||
target?: string
|
||||
sourceHandle?: string | null
|
||||
targetHandle?: string | null
|
||||
label?: unknown
|
||||
}
|
||||
return [
|
||||
edge.source,
|
||||
edge.target,
|
||||
edge.sourceHandle ?? "",
|
||||
edge.targetHandle ?? "",
|
||||
edge.label ?? "",
|
||||
].join("|")
|
||||
})
|
||||
return JSON.stringify([nodes.sort(), edges.sort()])
|
||||
}
|
||||
|
||||
const versionSignature = computed(() => {
|
||||
if (!versionJson.value) return null
|
||||
try {
|
||||
return signature(JSON.parse(versionJson.value) as Shape)
|
||||
return parseDoc(JSON.parse(decompressFromBase64(data)))
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
@@ -168,22 +121,18 @@ const versionSignature = computed(() => {
|
||||
|
||||
/**
|
||||
* 只有画布和这一版不一样时才给「载回画布」:刚交完、还没动过的时候,这个按钮点了什么
|
||||
* 都不会变,只会让人以为它坏了。读 getFlowchartData() 会顺带跟踪画布的节点和边。
|
||||
* 都不会变,只会让人以为它坏了。比的是结构和字,不比 id。
|
||||
* 读 getDoc() 会顺带跟踪画布的数据
|
||||
*/
|
||||
const canvasDiffers = computed(() => {
|
||||
if (!editor.value || !versionSignature.value) return false
|
||||
return signature(editor.value.getFlowchartData()) !== versionSignature.value
|
||||
if (!editor.value || !versionDoc.value) return false
|
||||
return signature(editor.value.getDoc()) !== signature(versionDoc.value)
|
||||
})
|
||||
|
||||
function loadToCanvas() {
|
||||
if (!editor.value || !versionJson.value) return
|
||||
try {
|
||||
const json = JSON.parse(versionJson.value)
|
||||
editor.value.setFlowchartData({ nodes: json.nodes || [], edges: json.edges || [] })
|
||||
message.success("已载回画布,按 Ctrl+Z 可以撤回")
|
||||
} catch {
|
||||
message.error("这一版的图数据坏了,载不回画布")
|
||||
}
|
||||
if (!editor.value || !versionDoc.value) return
|
||||
editor.value.setDoc(versionDoc.value)
|
||||
message.success("已载回画布,按 Ctrl+Z 可以撤回")
|
||||
}
|
||||
|
||||
/** A / S 之后:照着图写代码。切回学生习惯的那门语言 */
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { storeToRefs } from "pinia"
|
||||
import { compressToBase64 } from "utils/functions"
|
||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||
import { useMermaidConverter } from "../composables/useMermaidConverter"
|
||||
import { countBlank, toMermaid } from "shared/components/FlowchartEditor/model"
|
||||
import { useProblemStore } from "oj/store/problem"
|
||||
import { useFlowchartStore, type FlowchartEditorInstance } from "oj/store/flowchart"
|
||||
|
||||
@@ -21,20 +21,22 @@ const { problem } = storeToRefs(useProblemStore())
|
||||
const flowchartStore = useFlowchartStore()
|
||||
const { loading } = storeToRefs(flowchartStore)
|
||||
const { isDesktop } = useBreakpoints()
|
||||
const { convertToMermaid } = useMermaidConverter()
|
||||
|
||||
function submit() {
|
||||
const editor = flowchartEditorRef?.value
|
||||
if (!editor) return
|
||||
const flowchartData = editor.getFlowchartData()
|
||||
if (!flowchartData?.nodes?.length || !flowchartData?.edges?.length) {
|
||||
message.warning("画布上还没有图:从左边拖几个框进来,再用线连起来")
|
||||
const doc = editor.getDoc()
|
||||
if (!doc.steps.length) {
|
||||
message.warning("画布上还没有图:点「开始」下面的 +,加第一步")
|
||||
return
|
||||
}
|
||||
flowchartStore.submit(
|
||||
convertToMermaid(flowchartData),
|
||||
compressToBase64(JSON.stringify(flowchartData)),
|
||||
)
|
||||
// 空框交上去 AI 只能瞎猜,学生也不知道扣分是因为忘了写字
|
||||
const blank = countBlank(doc.steps)
|
||||
if (blank) {
|
||||
message.warning(`有 ${blank} 个框还没写字,点一下框就能写`)
|
||||
return
|
||||
}
|
||||
flowchartStore.submit(toMermaid(doc), compressToBase64(JSON.stringify(doc)))
|
||||
}
|
||||
|
||||
// 进题、换题都读一次历次分数(store 里同一道题只读一次)
|
||||
|
||||
Reference in new issue
Block a user