# 时差培养箱业务流程图制作规范(可复用模板) > **用途**:统一流程图制作标准,确保任何功能的流程图都能: > 1. **100% 还原真实业务逻辑**(包括分支、回流、异常路径) > 2. **清晰展示三端联动**(operate / control / front 的交互与影响) > 3. **提供完整节点详情**(每个节点的触发条件、执行逻辑、涉及数据、代码位置) > 4. **自动布局不重叠**(用 dagre 自动分层,不手写坐标,杜绝线条/元素重叠交叉) > 5. **可拖拽 + 记忆位置**(节点可拖动,位置自动存 localStorage,刷新后恢复) > 6. **支持无限扩展**(画布宽高不限,业务有多复杂流程图就撑多大) > **⚠ 本规范 v2(2026-06-24 重写)**:吸取第一版"手写坐标导致重叠乱"的教训, > 改为 **dagre 自动布局**;分支从"藏在面板文字里"改为"真实画进图";新增拖拽+localStorage。 > 标杆参考实现见第九章「参考示例」。 --- ## 〇、最重要的三条铁律(先记这个) 1. **分支必须真实画进图**——决策点(平衡vs开始、移植/冷冻/删除/作废)要在画布上真分叉成多个节点多条线,**绝不能**写成一个笼统节点 + 面板里一段"后续分支"文字。 2. **布局必须自动算(dagre),不手写坐标**——手写 x/y 节点一多必然重叠交叉。用 dagre 分层布局打底,保证 0 重叠;用户再拖拽微调。 3. **右侧面板只放"本节点自己的细节"**——不放分支(分支已经画在图上了)。面板=这步是什么/前置/触发/步骤/后端/数据/三端联动/代码位置。 --- ## 一、流程图核心原则 ### 1.1 真实性原则:不折叠、不简化 - ❌ **禁止**把分支写成文字描述:"点击后可选 A 或 B" - ❌ **禁止**把分支塞进详情面板的"后续分支"板块(v1 的错误做法,已废弃该板块) - ✅ **必须**在画布上画出真实的分支节点和分叉连线:决策节点下真分出多个子节点 ### 1.2 完整性原则:覆盖所有路径 - **正常路径**:用户正常操作的主流程(如:入箱→培养→拍照→看图→标记→结束) - **分支路径**:业务决策点的不同选择(如:平衡 vs 直接开始、移植 vs 冷冻 vs 删除 vs 作废) - **异常路径**:错误、失败、超时、权限不足等场景(如:硬件异常、校验失败、MQTT 超时) - **回流路径**:回到前面某步重新走(如:对焦失败→重新对焦、拍照失败→重试) ### 1.3 三端联动原则:明确跨端影响 每个操作都要说清楚: - **本端做了什么**(operate 点了按钮 / control 收到命令 / front 刷新界面) - **触发了什么**(调接口 / 发 MQTT / 改数据库 / 发 Kafka) - **影响了哪些端**(其他端的界面变化 / 状态变化 / 行为变化) ### 1.4 无限画布原则:不限宽高 - 流程往哪延伸就往哪画(上下左右+斜向都可以) - 画布尺寸根据内容自动撑开(CSS 不设 max-width / max-height) - 浏览器出现滚动条是正常的(不强制塞进一屏) --- ## 二、流程图结构规范 ### 2.0 文件结构(4 个文件,必须同目录) 为了好维护、避免单文件过大被截断,拆成 4 个文件放同一目录: | 文件 | 作用 | 大小参考 | |---|---|---| | `xxx流程图.html` | 骨架:顶栏+图例+画布+详情面板 DOM + 全部 CSS | ~10KB | | `flow-data.js` | 数据:`NODES`(节点数组)+ `EDGES`(连线数组) | 随业务 | | `flow-render.js` | 引擎:dagre 布局 + 渲染 + 拖拽 + localStorage + 折线 + 面板 | ~14KB | | `dagre.min.js` | 自动布局库(离线内置,约 278KB,自包含 graphlib) | 278KB | HTML 末尾按顺序引用: ```html ``` > dagre.min.js 来源:`https://cdnjs.cloudflare.com/ajax/libs/dagre/0.8.5/dagre.min.js`, > 下载后放同目录即可离线用(已验证可用)。`flow-render.js` 里调 `dagre.graphlib.Graph` / `dagre.layout`。 ### 2.1 主画布布局 ``` ┌────────────────────────────────────────────────┐ │ 顶部固定栏:标题 + 图例 + [重置布局][适应屏幕][已保存] │ ├────────────────────────────────────────────────┤ │ 画布视口 viewport(可平移/滚轮缩放): │ │ world(transform 平移缩放): │ │ ├─ svg(直角折线连线,在节点下层) │ │ └─ 节点(绝对定位,dagre 算坐标,可拖拽) │ │ 右下角:缩放控制 [+][百分比][-] │ └────────────────────────────────────────────────┘ 右侧滑出:详情面板(点节点弹出) ``` ### 2.2 节点样式规范(颜色 = 标杆实现实际用色) | 类型 | type | 边框色 | 背景色 | 图标 | 说明 | |---|---|---|---|---|---| | 起止 | `start` | `#16A085` | `#E8F8F5` | 🥚/🔚 | 流程起点终点 | | operate | `operate` | `#4A90E2` | `#EAF3FC` | 🖥️ | 操作端动作 | | control | `control` | `#F39C12` | `#FEF5E7` | ⚙️ | 后台动作 | | front | `front` | `#9B59B6` | `#F5EEF8` | 💻 | 管理端动作 | | 判断分支 | `branch` | `#E67E22` | `#FDF2E9` | ❓ | 决策点(虚线边框) | | 异常 | `error` | `#E74C3C` | `#FDEDEC` | ⚠️ | 报警/失败 | 节点结构(由 JS 生成,宽度固定 200px,高度由内容自动量): ```html
🖥️新建患者入箱
operate 操作端
录入患者信息 + 选 16 孔位 + 受精方式
``` > **node-brief 很重要**:画布上每个节点直接显示一句话摘要,不用点开就懂大概;点开才看完整详情。 ### 2.3 连线样式规范(直角折线 + 4 种类型) | type | 含义 | 颜色 | 线型 | |---|---|---|---| | `internal` | 本端内部流程 | `#7F8C8D` 灰 | 实线 | | `cross` | 跨端调用(API/MQTT/Kafka/DB同步) | `#16A085` 绿 | 虚线 `6,5` | | `error` | 异常/报警路径 | `#E74C3C` 红 | 点线 `2,4` | | `loop` | 回流(回到前面某步/下一轮循环) | `#E67E22` 橙 | 点线 `2,4` | - **一律用直角折线**(电路图风格),不用曲线:`M sx,sy L sx,midY L ex,midY L ex,ey` - 连线在节点**下层**(z-index:svg=1,node=2),不遮挡节点 - 连线**带文字标签**(label),标签加白底矩形避免被线穿过 - 箭头用 SVG ``,每种 type 一个对应颜色的箭头 --- ## 三、节点详情面板规范(右侧滑出) > **v2 变更**:从 10 个板块精简为 **8 个**。删掉了"后续分支"(分支改画进图)。 > 面板宽度 640px,从右滑出,点遮罩/ESC/×关闭。面板只讲**这一个节点自己**的事。 每个节点的 `detail` 对象字段(flow-data.js 里)与面板板块一一对应: | 字段 | 面板板块 | 内容 | |---|---|---| | `desc` | 📋 这步是什么 | 一段大白话,说清这个节点干什么 | | `pre` | 🔑 前置条件 | 数组,能进这步要满足的条件 | | `trigger` | 🎯 触发方式 | 哪个界面哪个按钮 / 什么 MQTT / 什么循环触发 | | `steps` | 🔄 操作步骤 | 数组,用户做什么 或 系统自动执行的步骤 | | `backend` | ⚙️ 后端逻辑 | 代码层面做了什么(调接口/发MQTT/落库) | | `data` | 💾 数据/状态变化 | 数组,DB哪些表、内存哪些变量、UI怎么变 | | `cross` | 🌐 三端联动 | 对象 `{from, via, to[]}`,没有跨端则 `null`(面板不显示该块) | | `code` | 📍 代码位置 | 数组,`文件:行号 方法名` / 表名 / topic | ### 3.1 节点数据完整示例(flow-data.js 里一个节点) ```javascript { id: 'op-start-dish', type: 'operate', icon: '▶️', title: '开始培养(保存)', brief: '落库 dish+embryo → 发 MQTT 通知 control', // 画布上直接显示的一句话 detail: { desc: '入箱的核心动作。前端校验后调 StartDishApi,后端把培养皿+每个胚胎落库,再发 MQTT StartDish 通知 control。', pre: ['表单校验通过(孔位≥1、必填非空)', '舱室=空闲', 'control 在线'], trigger: 'operate 入箱窗点【保存=开始培养】→ StartDish_Click', steps: ['前端校验必填项', '调 StartDishApi', '后端落库 dish 表+embryo 表', '后端发 MQTT StartDish'], backend: 'StartDish_Click → StartDishApi → dish/embryo 落库 → MqttSendRpc 发 MQTT StartDish', data: ['DB:dish 表插1条(培养中),embryo 表插N条(state=0)', '舱状态:空闲→培养中'], cross: { from: 'operate 点【开始培养】', via: 'StartDishApi → dish/embryo 落库 → MQTT StartDish', to: ['control 收 MQTT → HouseBin.StartDish → 启动舱主循环', 'front 设备管理:舱格变"培养中"', 'operate 主界面:舱格变色+显示患者'] }, code: ['operate AddDishWindowView.xaml.cs:451 StartDish_Click', 'control AppData.cs:1102 StartDish', 'DB dish/embryo 表'] } } ``` ### 3.2 三端联动板块(cross)渲染成 ``` 本端:operate 点【开始培养】 ↓ StartDishApi → dish/embryo 落库 → MQTT StartDish 影响: ▸ control 收 MQTT → HouseBin.StartDish → 启动舱主循环 ▸ front 设备管理:舱格变"培养中" ▸ operate 主界面:舱格变色+显示患者 ``` --- ## 四、分支绘制规范(用数据表达,dagre 自动布局) > **核心**:分支不靠手画,而是在 `EDGES` 里写好"从哪个节点连到哪些节点", > dagre 自动算出分叉布局。你只管把分支的**节点**和**连线**定义对,图形自动成型。 ### 4.1 二选一分支(如:先平衡 / 直接开始) 加一个 `branch` 类型判断节点,从它连出两条边到两个不同节点: ```javascript // 节点 { id: 'br-balance-or-start', type: 'branch', title: '判断:先平衡?还是直接开始?', ... } // 连线:判断节点 → 两条路 { from: 'br-balance-or-start', to: 'op-balance', type:'internal', label:'①点【平衡】' }, { from: 'br-balance-or-start', to: 'op-start-dish', type:'internal', label:'②点【开始培养】' }, ``` ### 4.2 多路分支(如:移植/冷冻/删除/作废) 判断节点连出 N 条边到 N 个并排节点,dagre 自动把它们排成一排: ```javascript { from: 'br-destination', to: 'op-transplant', type:'internal', label:'移植' }, { from: 'br-destination', to: 'op-freeze', type:'internal', label:'冷冻' }, { from: 'br-destination', to: 'op-delete', type:'internal', label:'删除' }, { from: 'br-destination', to: 'op-invalid', type:'internal', label:'作废' }, // 四个去向再汇合到"结束培养" { from: 'op-transplant', to: 'op-end', type:'internal' }, { from: 'op-freeze', to: 'op-end', type:'internal' }, { from: 'op-delete', to: 'op-end', type:'internal' }, { from: 'op-invalid', to: 'op-end', type:'internal' }, ``` ### 4.3 回流(如:换气完→回主循环、舱位释放→回主界面) 用 `type:'loop'`(橙色点线),从后面节点连回前面节点: ```javascript { from: 'ctl-airswap', to: 'ctl-loop', type:'loop', label:'换气完→下轮' }, { from: 'op-back-main', to: 'op-main', type:'loop', label:'舱位释放·可放下一个' }, ``` ### 4.4 异常旁路(如:拍照失败→报警) 用 `type:'error'`(红色点线),从主流程节点旁拉出到异常节点: ```javascript { from: 'ctl-photo', to: 'err-alarm', type:'error', label:'拍照失败' }, { from: 'err-alarm', to: 'op-main', type:'error', label:'报警显示' }, ``` ### 4.5 跨端调用(如:operate→control) 用 `type:'cross'`(绿色虚线),表示经 API/MQTT/Kafka 到另一个端: ```javascript { from: 'op-start-dish', to: 'ctl-recv', type:'cross', label:'MQTT StartDish' }, { from: 'ctl-photo', to: 'op-detail', type:'cross', label:'Kafka→DB→轮询' }, ``` > **要点**:分支汇合点(如四去向都→结束培养)也用边表达,dagre 会自动收拢。 > 你不需要算任何坐标,只要边写对,图就对。 --- ## 五、交互规范 ### 5.1 节点交互 - **默认**:显示图标+标题+类型标签+一句话摘要(brief) - **hover**:阴影加深,z-index 提升 - **激活(点击)**:边框高亮光圈,右侧滑出详情面板 - **拖拽**:按住可拖动,连线实时跟随重绘;拖动距离>3px 算拖拽(不触发点击) ### 5.2 拖拽 + localStorage 记忆(v2 新增·关键) - 每个节点可鼠标拖动,松手后**自动存 localStorage**(键名带版本号如 `tl-flow-positions-v2`) - 下次打开**自动恢复**上次拖好的位置(没存过则用 dagre 初始布局) - 顶栏 **「重置布局」** 按钮:清 localStorage + 重跑 dagre + 适应屏幕(拖乱了能救回来) - 顶栏 **「适应屏幕」** 按钮:整图居中缩放到刚好看全 - 顶栏 **「已自动保存」** 状态提示:拖完闪一下"✓ 已保存位置" ### 5.3 画布平移 + 缩放 - 空白处按住拖动 = 平移整个画布 - 鼠标滚轮 = 以鼠标位置为中心缩放(0.3~2 倍) - 右下角 [+][-] 按钮 + 百分比显示 - 缩放/平移用 `world` 元素的 `transform: translate() scale()` ### 5.3 详情面板 - 从右侧滑出,宽 **640px**(max-width 92vw 适配小屏) - 关闭:点遮罩 / 按 ESC / 点右上角 × - 标题带类型徽章(颜色=节点类型色) ### 5.4 连线绘制(直角折线) - SVG 在节点渲染后**动态读取节点真实坐标**再画(保证对齐) - 直角折线:`M sx,sy L sx,midY L ex,midY L ex,ey` - A 在 B 上方 → A 底部出、B 顶部进;B 在 A 上方(回流)→ 反向;同层 → 走侧边 - 拖动节点时**实时重绘所有连线**(`renderEdges()`) --- ## 六、代码结构规范 ### 6.1 三文件职责 - **flow-data.js**:只放数据,导出 `NODES` 和 `EDGES` 两个数组(见 3.1 / 第四章示例) - **flow-render.js**:IIFE 包裹,按 8 步走: 1. `computeLayout()`——dagre 算每个节点 `{x,y}`(先临时渲染量节点高度) 2. `loadSaved()`——读 localStorage 覆盖坐标 3. `renderNodes()`——绝对定位渲染节点 + 绑拖拽 + 绑点击 4. `renderEdges()`——SVG 直角折线 + 箭头 + 标签 5. `makeDraggable()`——拖拽 + 拖完 `savePositions()` 6. 平移/缩放(wheel + 空白拖动 + 缩放按钮) 7. 详情面板(`showDetail` / `closeDetail` / `detailHTML`) 8. `init()`——串起来;`重置布局`/`适应屏幕` 按钮 - **HTML**:骨架 DOM(顶栏/viewport/world/svg/缩放/面板)+ 全部 CSS ### 6.2 节点/连线数据结构(⚠ 不含坐标,dagre 自动算) ```javascript const NODES = [ { id: 'op-start-dish', // 唯一ID,格式:端-功能-动作 type: 'operate', // start/operate/control/front/branch/error icon: '▶️', title: '开始培养(保存)', brief: '落库 dish+embryo → 发 MQTT 通知 control', // 画布上直接显示 detail: { desc, pre[], trigger, steps[], backend, data[], cross|null, code[] } // 见第三章 }, // ... 没有 x/y!坐标由 dagre 算 ]; const EDGES = [ { from: 'op-start-dish', to: 'ctl-recv', type: 'cross', label: 'MQTT StartDish' }, // type: internal(本端) / cross(跨端) / error(异常) / loop(回流) // label 可选 ]; ``` ### 6.3 dagre 调用要点 ```javascript const g = new dagre.graphlib.Graph(); g.setGraph({ rankdir: 'TB', nodesep: 60, ranksep: 90, marginx: 40, marginy: 40 }); g.setDefaultEdgeLabel(() => ({})); NODES.forEach(n => g.setNode(n.id, { width: 200, height: 量出来的高度 })); EDGES.forEach(e => g.setEdge(e.from, e.to)); dagre.layout(g); g.nodes().forEach(id => { const nd = g.node(id); nodePos[id] = { x: nd.x - nd.width/2, y: nd.y - nd.height/2 }; }); ``` --- ## 七、制作清单(每次制作流程图必做) ### 7.1 前期准备 - [ ] 用 codegraph 挖通完整业务链路(从入口到出口,包括所有分支) - [ ] 列出涉及的三端文件(operate / control / front 各有哪些文件参与) - [ ] 列出涉及的数据表(哪些表会增删改查) - [ ] 列出涉及的接口/MQTT(哪些 API / topic 会被调用) ### 7.2 节点设计 - [ ] 每个操作/判断/事件抽象成一个节点(含 `branch` 判断节点、`error` 异常节点) - [ ] 唯一 ID(格式 `端-功能`,如 `op-start-dish`、`ctl-loop`、`br-destination`) - [ ] 填 `brief`(画布一句话)+ 完整 `detail`(8 字段,见第三章) - [ ] **不写坐标**(dagre 自动算) ### 7.3 分支设计(画进图,不进面板) - [ ] 每个决策点建一个 `branch` 节点 - [ ] 从决策节点连出多条 `EDGES` 到不同子节点(见第四章) - [ ] 分支汇合点也用边连(dagre 自动收拢) - [ ] 回流用 `loop`、异常旁路用 `error`、跨端用 `cross` ### 7.4 连线设计 - [ ] 每条边定 `type`(internal/cross/error/loop)+ 可选 `label` - [ ] 检查 EDGES 里 from/to 的 id 都在 NODES 里存在(用脚本验,见 7.6) ### 7.5 三端联动设计 - [ ] 每个跨端节点的 `detail.cross` 写清 `{from, via, to[]}` - [ ] 跨端用 `cross` 类型边在图上连出来 ### 7.6 测试与验收(用脚本 + 浏览器双验) - [ ] **脚本验数据**:node 跑一遍,检查①边引用的节点都存在 ②id唯一 ③每个节点 detail 8字段齐 ④跑 dagre 布局**0 重叠对** - [ ] **浏览器验视觉**:Edge/Chrome headless 截图,确认布局整齐、分支并排、连线分类、颜色正确 - [ ] **JS 语法**:`node --check flow-data.js` / `node --check flow-render.js` - [ ] **交互**:点节点弹面板、拖动节点连线跟随、刷新后位置恢复、重置布局可救回 - [ ] 4 个文件在同一目录 验数据脚本(复用): ```bash node -e ' global.dagre=require("./dagre.min.js"); const src=require("fs").readFileSync("./flow-data.js","utf8").replace(/const NODES/,"NODES").replace(/const EDGES/,"EDGES"); global.NODES=[];global.EDGES=[];eval(src); const ids=new Set(NODES.map(n=>n.id));let bad=0; EDGES.forEach(e=>{if(!ids.has(e.from)){console.log("X起点:",e.from);bad++}if(!ids.has(e.to)){console.log("X终点:",e.to);bad++}}); const g=new dagre.graphlib.Graph();g.setGraph({rankdir:"TB",nodesep:60,ranksep:90});g.setDefaultEdgeLabel(()=>({})); NODES.forEach(n=>g.setNode(n.id,{width:200,height:90}));EDGES.forEach(e=>{if(g.hasNode(e.from)&&g.hasNode(e.to))g.setEdge(e.from,e.to)}); dagre.layout(g); const b=g.nodes().map(id=>{const n=g.node(id);return{id,x:n.x-100,y:n.y-45,w:200,h:90}});let ov=0; for(let i=0;ic.x&&a.yc.y)ov++} console.log("节点",NODES.length,"边",EDGES.length,"坏引用",bad,"重叠",ov,(ov===0&&bad===0)?"[OK]":"[!]"); ' ``` --- ## 八、常见错误与纠正 ### ❌ 错误 1:把分支写成文字 / 塞进面板(v1 最大的坑) **错**:一个"标记胚胎去向"节点,分支写在面板"后续分支"里:移植/冷冻/删除/作废。 **纠正**:建 `br-destination` 判断节点,画布上真分出 4 个并排节点 + 4 条边(见 4.2)。 ### ❌ 错误 2:手写节点坐标导致重叠交叉(v1 第二大坑) **错**:每个节点写死 `x:200, y:100`,节点一多线条到处交叉。 **纠正**:删掉所有坐标,用 dagre 自动分层布局(见 6.3),脚本验证 0 重叠。 ### ❌ 错误 3:详情面板内容不完整 缺前置条件/缺三端联动/缺代码位置。**纠正**:严格按第三章 8 字段填。 ### ❌ 错误 4:连线颜色/线型不区分 所有线一个样 → 看不出本端还是跨端。**纠正**:按 2.3 用 4 种 type 区分颜色+线型。 ### ❌ 错误 5:单文件塞太多导致写入被截断 **错**:节点详情多了,单个 HTML 几千行,一次写容易断。 **纠正**:拆 4 文件(见 2.0),数据(flow-data.js)和引擎(flow-render.js)分开,分批追加。 --- ## 九、参考示例(标杆实现,照着抄) ### 标杆:培养全流程图(本规范 v2 配套,桌面) - **文件**(4 个,同目录): - `时差培养箱-培养全流程详图.html`(骨架+CSS) - `flow-data.js`(23 节点 + 39 连线) - `flow-render.js`(dagre+拖拽+localStorage+折线+面板) - `dagre.min.js`(布局库) - **覆盖**:入箱→平衡分支/直接开始→control 舱主循环(换气/对焦/拍照三分支)→看图→胚胎去向四分支→结束→回主界面回流;异常报警旁路;front 旁路联动 - **特点**:dagre 0 重叠、分支真画进图、可拖拽记忆位置、直角折线、三端配色 - **验证**:脚本(0坏引用/0重叠/detail齐) + Edge headless 截图均通过 > **下次做新功能流程图**:复制这 4 个文件,改 `flow-data.js` 的 NODES/EDGES, > HTML 改标题,flow-render.js 基本不用动。改完跑 7.6 的验证脚本。 --- ## 十、总结 ✅ **记住这几条,流程图就不会出错**: 1. **分支真实画进图**(建 branch 节点 + 多条边,不写进面板文字) 2. **坐标交给 dagre**(不手写 x/y,杜绝重叠交叉) 3. **详情面板写全 8 字段**(desc/pre/trigger/steps/backend/data/cross/code) 4. **三端联动说清楚**(cross 字段:谁触发→经过什么→影响谁) 5. **连线分 4 类**(internal/cross/error/loop,颜色+线型区分) 6. **拖拽+localStorage**(可微调 + 记忆位置 + 重置可救回) 7. **拆 4 文件 + 脚本验证**(避免截断,保证 0 重叠 0 坏引用) 📌 **每次制作前先读这份规范,制作后跑 7.6 验证脚本。直接复制第九章标杆 4 文件改最省事。**