From 09160154054dd57eefa6387dc29179cef6904a74 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Wed, 28 Oct 2026 12:33:00 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=B5=81=E7=A8=8B=E5=9B=BE):=20=E8=BE=93?= =?UTF-8?q?=E5=85=A5/=E8=BE=93=E5=87=BA/=E8=AF=AD=E5=8F=A5/=E6=9D=A1?= =?UTF-8?q?=E4=BB=B6/=E5=BE=AA=E7=8E=AF=E4=BA=94=E7=A7=8D=E6=A1=86?= =?UTF-8?q?=E5=90=84=E4=B8=80=E7=A7=8D=E9=A2=9C=E8=89=B2=EF=BC=8C=E4=B8=8D?= =?UTF-8?q?=E5=86=8D=E8=BE=93=E5=85=A5=E8=BE=93=E5=87=BA=E5=90=8C=E8=89=B2?= =?UTF-8?q?=E3=80=81=E6=9D=A1=E4=BB=B6=E5=BE=AA=E7=8E=AF=E5=90=8C=E8=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 输出改紫、循环改青,绿和红留给起止框、选中和标错。存下来的 Mermaid 里循环也标成 decision,画之前按「有箭头从后面绕回来」认出循环,历史提交的图也按新颜色画。 Co-Authored-By: Claude Opus 5.5 --- .../components/FlowchartEditor/paint.ts | 20 ++++++++----- apps/web/src/shared/composables/useMermaid.ts | 29 ++++++++++++++++--- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/apps/web/src/shared/components/FlowchartEditor/paint.ts b/apps/web/src/shared/components/FlowchartEditor/paint.ts index 50fdc7a..e30a7b5 100644 --- a/apps/web/src/shared/components/FlowchartEditor/paint.ts +++ b/apps/web/src/shared/components/FlowchartEditor/paint.ts @@ -2,22 +2,28 @@ import { PLACEHOLDER, type LaidNode } from './layout' /** * 框的颜色和形状:画流程图的画布(index.vue)和只看的图(FlowchartView.vue)共用一份, - * 两边才长得一样 + * 两边才长得一样。题面里的 Mermaid 图在 useMermaid.ts 另有一份,改颜色两边一起改。 + * + * 输入 / 输出 / 处理 / 判断 / 循环各一种颜色;绿和红不给框用 —— 绿是起止框和选中,红是标错 */ type Paint = { fill: string; stroke: string; text: string } const LIGHT: Record = { terminal: { fill: '#eef8f2', stroke: '#18a058', text: '#0c6b3b' }, - io: { fill: '#eef4fd', stroke: '#2f6bc4', text: '#1d4f9c' }, + input: { fill: '#eef4fd', stroke: '#2f6bc4', text: '#1d4f9c' }, + output: { fill: '#f4effc', stroke: '#8a55c9', text: '#5d2f99' }, process: { fill: '#ffffff', stroke: '#7d828a', text: '#1f2225' }, - decision: { fill: '#fdf5e6', stroke: '#d08a1c', text: '#8a5200' }, + if: { fill: '#fdf5e6', stroke: '#d08a1c', text: '#8a5200' }, + while: { fill: '#e8f6f8', stroke: '#1590a6', text: '#0b5e6e' }, } const DARK: Record = { terminal: { fill: 'rgba(24,160,88,0.16)', stroke: '#3fbf7f', text: '#8fe0b4' }, - io: { fill: 'rgba(70,130,220,0.18)', stroke: '#5b8fdc', text: '#a9c6f2' }, + input: { fill: 'rgba(70,130,220,0.18)', stroke: '#5b8fdc', text: '#a9c6f2' }, + output: { fill: 'rgba(150,100,215,0.2)', stroke: '#a57ee0', text: '#d2bcf4' }, process: { fill: '#26272b', stroke: '#8a8f98', text: '#e6e7ea' }, - decision: { fill: 'rgba(208,138,28,0.18)', stroke: '#e0a040', text: '#f2cd8c' }, + if: { fill: 'rgba(208,138,28,0.18)', stroke: '#e0a040', text: '#f2cd8c' }, + while: { fill: 'rgba(30,160,185,0.18)', stroke: '#3cb4c8', text: '#9edce6' }, } export function useFlowPaint() { @@ -25,9 +31,7 @@ export function useFlowPaint() { function paint(kind: LaidNode['kind']) { const paints = isDark.value ? DARK : LIGHT if (kind === 'start' || kind === 'end') return paints.terminal! - if (kind === 'input' || kind === 'output') return paints.io! - if (kind === 'if' || kind === 'while') return paints.decision! - return paints.process! + return paints[kind] ?? paints.process! } const lineColor = computed(() => (isDark.value ? '#8a8f98' : '#7d828a')) return { isDark, paint, lineColor } diff --git a/apps/web/src/shared/composables/useMermaid.ts b/apps/web/src/shared/composables/useMermaid.ts index f270f87..3d10fc1 100644 --- a/apps/web/src/shared/composables/useMermaid.ts +++ b/apps/web/src/shared/composables/useMermaid.ts @@ -10,16 +10,16 @@ const semanticNodeClasses = ['startNode', 'endNode', 'startEnd', 'input', 'outpu const displayStyleId = 'oj-mermaid-display-style' -/** 框的颜色:[class, 底色, 边框]。pele 把 `class n0 startNode` 原样挂在节点的 上 */ +/** 框的颜色:[class, 底色, 边框],和画布的 FlowchartEditor/paint.ts 一致。pele 把 `class n0 startNode` 原样挂在节点的 上 */ const nodeColors: [string, string, string][] = [ ['startNode', '#eef8f2', '#18a058'], ['startEnd', '#eef8f2', '#18a058'], ['endNode', '#eef8f2', '#18a058'], ['input', '#eef4fd', '#2f6bc4'], - ['output', '#eef4fd', '#2f6bc4'], + ['output', '#f4effc', '#8a55c9'], ['process', '#ffffff', '#7d828a'], ['decision', '#fdf5e6', '#d08a1c'], - ['loop', '#fdf5e6', '#d08a1c'], + ['loop', '#e8f6f8', '#1590a6'], ['oj-node-palette-0', '#dbeafe', '#2563eb'], ['oj-node-palette-1', '#ccfbf1', '#0d9488'], ['oj-node-palette-2', '#ede9fe', '#7c3aed'], @@ -74,6 +74,27 @@ ${nodeColors const svgNamespace = 'http://www.w3.org/2000/svg' +/** + * 契约的 flowToMermaid 把判断和循环都标成 decision(库里存的提交、参考图都是这样), + * 两者要分颜色,只能在画之前认出循环:有箭头从后面的框绕回来的 decision 就是循环 + */ +function markLoops(code: string) { + const order = new Map() + const backTargets = new Set() + for (const line of code.split('\n')) { + const head = /^\s*(\w+)/.exec(line)?.[1] + if (head && head !== 'flowchart' && head !== 'graph' && head !== 'class' && !order.has(head)) order.set(head, order.size) + const edge = /^\s*(\w+)\b.*?-->(?:\|[^|]*\|)?\s*(\w+)/.exec(line) + if (!edge) continue + const [, from, to] = edge + if (!order.has(to!)) order.set(to!, order.size) + if (order.get(from!)! > order.get(to!)!) backTargets.add(to!) + } + return code.replace(/^(\s*class\s+)(\w+)(\s+)decision\s*$/gm, (line, pre: string, id: string, gap: string) => + backTargets.has(id) ? `${pre}${id}${gap}loop` : line, + ) +} + function applyFlowchartDisplayStyle(container: HTMLElement) { container.classList.add('oj-mermaid-surface') @@ -124,7 +145,7 @@ export function useMermaid() { try { // pele/lazy 按图的类型现拉对应的模块,题目里绝大多数是流程图,只拉那一块 const { renderAsync } = await import('pele/lazy') - const { svg } = await renderAsync(mermaidCode, { + const { svg } = await renderAsync(markLoops(mermaidCode), { // 量字宽用页面真实的字体,否则框会比字窄或宽 fontFamily: getComputedStyle(container).fontFamily, // 题目是老师写的、提交是学生写的,图里的链接和外链图片一律不画