Просмотр исходного кода

docs(flow): 流程图制作规范升级 v2(dagre自动布局+分支画进图+拖拽记忆)

对齐最终实现,修正 v1 的两大坑:
- 手写坐标→dagre 自动分层布局(脚本验证0重叠),杜绝线条/元素重叠交叉
- 分支藏面板文字→真实画进图(branch节点+多条EDGES,dagre自动分叉)

新增标准:
- 4文件结构(html骨架/flow-data.js/flow-render.js/dagre.min.js同目录)
- 节点/连线数据结构不含坐标;连线4类型(internal/cross/error/loop)直角折线
- 详情面板8字段(desc/pre/trigger/steps/backend/data/cross/code),删后续分支板块
- 可拖拽+localStorage记忆位置+重置布局+适应屏幕
- 颜色对齐实际实现(start绿/operate蓝/control橙/front紫/branch橙虚/error红)
- 第七章加脚本验证(0坏引用+0重叠)+浏览器截图双验流程
- 第九章标杆4文件,下次复制改NODES/EDGES即可

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
huangjie 1 день назад
Родитель
Сommit
b0a7f09f93

+ 0 - 479
项目文档/流程图制作规范-可复用模板.md

@@ -1,479 +0,0 @@
-# 时差培养箱业务流程图制作规范(可复用模板)
-
-> **用途**:统一流程图制作标准,确保任何功能的流程图都能:
-> 1. **100% 还原真实业务逻辑**(包括分支、回流、异常路径)
-> 2. **清晰展示三端联动**(operate / control / front 的交互与影响)
-> 3. **提供完整节点详情**(每个节点的触发条件、执行逻辑、后续分支、涉及数据、代码位置)
-> 4. **支持无限扩展**(画布宽高不限,业务有多复杂流程图就撑多大)
-
----
-
-## 一、流程图核心原则
-
-### 1.1 真实性原则:不折叠、不简化
-- ❌ **禁止**把分支写成文字描述:"点击后可选 A 或 B"
-- ✅ **必须**画出真实的分支路径:主干下方真实分叉,左边一路走 A,右边一路走 B
-
-### 1.2 完整性原则:覆盖所有路径
-- **正常路径**:用户正常操作的主流程(如:入箱→培养→拍照→看图→标记→结束)
-- **分支路径**:业务决策点的不同选择(如:平衡 vs 直接开始、移植 vs 冷冻 vs 删除 vs 作废)
-- **异常路径**:错误、失败、超时、权限不足等场景(如:硬件异常、校验失败、MQTT 超时)
-- **回流路径**:回到前面某步重新走(如:对焦失败→重新对焦、拍照失败→重试)
-
-### 1.3 三端联动原则:明确跨端影响
-每个操作都要说清楚:
-- **本端做了什么**(operate 点了按钮 / control 收到命令 / front 刷新界面)
-- **触发了什么**(调接口 / 发 MQTT / 改数据库 / 发 Kafka)
-- **影响了哪些端**(其他端的界面变化 / 状态变化 / 行为变化)
-
-### 1.4 无限画布原则:不限宽高
-- 流程往哪延伸就往哪画(上下左右+斜向都可以)
-- 画布尺寸根据内容自动撑开(CSS 不设 max-width / max-height)
-- 浏览器出现滚动条是正常的(不强制塞进一屏)
-
----
-
-## 二、流程图结构规范
-
-### 2.1 主画布布局
-```
-┌────────────────────────────────────────────────┐
-│  顶部固定栏:标题 + 导航 + 图例               │
-├────────────────────────────────────────────────┤
-│                                                │
-│  流程画布(无限宽高,可滚动):               │
-│    ├─ 节点(圆角矩形,点击弹详情面板)       │
-│    ├─ 连线(SVG,不同颜色区分本端/跨端)     │
-│    └─ 分支(水平并排 / 树形分叉 / 回流箭头) │
-│                                                │
-└────────────────────────────────────────────────┘
-```
-
-### 2.2 节点样式规范
-
-#### 节点类型与颜色
-- **operate 端节点**:蓝色边框 `#4A90E2`,浅蓝背景 `#E3F2FD`,图标 🖥️
-- **control 端节点**:橙色边框 `#FF9800`,浅橙背景 `#FFF3E0`,图标 ⚙️
-- **front 端节点**:紫色边框 `#9C27B0`,浅紫背景 `#F3E5F5`,图标 💻
-- **分支决策节点**:琥珀色边框 `#FFA726`,琥珀背景 `#FFF8E1`,图标 ❓
-- **异常节点**:红色边框 `#F44336`,浅红背景 `#FFEBEE`,图标 ⚠️
-
-#### 节点内容
-```html
-<div class="flow-node" data-node-id="operate-add-dish">
-  <div class="node-icon">🖥️</div>
-  <div class="node-title">新建患者入箱</div>
-  <div class="node-tag">operate 端</div>
-</div>
-```
-
-### 2.3 连线样式规范
-
-#### 连线类型与颜色
-- **本端流程**:实线 `stroke-width: 2px`
-  - operate 内部:`#4A90E2` 蓝色
-  - control 内部:`#FF9800` 橙色
-  - front 内部:`#9C27B0` 紫色
-- **跨端调用**:虚线 `stroke-dasharray: 5,5`
-  - 调接口/发 MQTT:`#4CAF50` 绿色
-  - 同步通知:`#00BCD4` 青色
-- **异常路径**:虚线 `stroke-dasharray: 3,3`
-  - 报警/失败:`#F44336` 红色
-
-#### 箭头样式
-```svg
-<marker id="arrowhead" markerWidth="8" markerHeight="6">
-  <polygon points="0 0, 8 3, 0 6" fill="继承连线颜色"/>
-</marker>
-```
-
----
-
-## 三、节点详情面板规范(右侧滑出)
-
-### 3.1 面板结构
-点击任意节点,右侧滑出详情面板,必须包含以下板块:
-
-#### 📋 板块 1:节点基本信息
-```markdown
-【节点标题】新建患者入箱
-
-【所属端】operate 操作端
-
-【这步是什么】
-医生在空舱点击,弹窗录入患者信息(夫妻姓名/病例号/周期/受精方式),
-选 16 孔位放哪些胚胎,可选先平衡或直接开始培养。
-```
-
-#### 🔑 板块 2:前置条件(能进这步的条件)
-```markdown
-【前置条件】
-✓ 舱室状态 = 空舱(无培养皿)
-✓ 用户已登录且有操作权限
-✓ control 后台已启动、该舱硬件正常(串口/相机/电机正常)
-```
-
-#### 🎯 板块 3:触发方式 / 界面位置
-```markdown
-【触发方式】
-- operate 主界面(A2)点某个空舱格 → 弹 AddDishWindowView 弹窗
-- 或 front 设备管理(D3)点空舱格 → 弹 AddDishWindow
-```
-
-#### 🔄 板块 4:交互步骤(用户做什么)
-```markdown
-【交互步骤】
-1. 在 16 孔圆周上点选要放胚胎的孔位(可多选)
-2. 填写表单:病例号、周期、女方/男方姓名、出生年月日、受精时间、受精方式
-3. 可选:勾选"重点关注"(VIP)
-4. 底部两个按钮:
-   - 【平衡】→ 先启动平衡流程(见分支 A)
-   - 【保存(开始培养)】→ 直接开始培养(见分支 B)
-```
-
-#### ⚙️ 板块 5:后端逻辑(代码做了什么)
-```markdown
-【后端逻辑】
-1. operate 前端校验:必填项非空、孔位至少选一个、受精时间合法
-2. 调接口:StartDishApi(POST /api/dish/start)
-   - 参数:舱号、患者信息、选中孔位列表、是否 VIP
-   - 后端落库:dish 表(培养皿)、embryo 表(胚胎,每个选中孔一条)
-3. 发 MQTT 命令:topic `tl/command/{tlSn}`, type=StartDish
-   - control 后台收到 → AppData.StartDish → 舱状态改"培养中"
-   - 启动对焦+拍照节拍(见 C6 舱主循环)
-```
-
-#### 💾 板块 6:涉及数据 / 状态变化
-```markdown
-【涉及数据】
-- 数据库:dish 表插入一条(status=培养中),embryo 表插入 N 条
-- 内存:HouseBin.CurrentDish 设为新 dishId,IsWorking=true
-- 舱室状态:空闲 → 培养中(主界面该舱格变色+显示患者信息)
-```
-
-#### 🌐 板块 7:三端联动影响(核心)
-```markdown
-【三端联动影响】
-本端动作:operate 点"开始培养"按钮
-    ↓ 触发
-后端/中间件:StartDishApi → dish 表插入 → MQTT 发 StartDish 命令
-    ↓ 通知
-control 后台:收到 MQTT → AppData.StartDish → HouseBin 状态改"培养中" → 启动对焦拍照循环
-    ↓ 同步到
-front 管理端:
-  - D3 设备管理首页:该舱格状态变"培养中",显示患者信息
-  - D7 培养记录列表:新增一条记录
-    ↓ 反向影响
-本端界面:A2 主界面该舱格变色+显示患者姓名
-```
-
-#### 🔀 板块 8:后续分支(接下来会走哪)
-```markdown
-【后续分支】
-→ 分支 A:点了"平衡"
-   → 启动平衡流程(换气若干轮,duration 可配置)
-   → 手动点"结束平衡"
-   → 再点"开始培养"
-   → 进入分支 B
-
-→ 分支 B:点了"保存(开始培养)"
-   → control 收到 StartDish
-   → 进入 C6 舱主循环(温压监测 → 判断是否对焦 → 判断是否拍照)
-```
-
-#### ⚠️ 板块 9:异常分支 / 边界情况
-```markdown
-【异常分支】
-- 若该舱硬件异常(串口断/相机丢失)→ 前端提示"该舱室不可用",操作失败
-- 若 control 后台未启动 → 前端提示"设备离线"
-- 若正在平衡中又点"开始培养" → 校验失败,提示"请先结束平衡"
-```
-
-#### 📍 板块 10:代码位置
-```markdown
-【代码位置】
-- operate 前端:AddDishWindowView.xaml.cs:451 StartDish_Click
-- operate 接口调用:StartDishApi (Urls.cs + ApiService)
-- control 后台:AppData.cs:1102 StartDish → HouseBin.cs:614 MainThread
-- 数据库表:dish、embryo、house_state
-- MQTT topic:tl/command/{tlSn}
-```
-
----
-
-## 四、分支绘制规范
-
-### 4.1 水平并排分支(两条平行路径)
-```
-         主干
-          ↓
-    ┌─────┴─────┐
-    ↓           ↓
-  分支A       分支B
-  (冷冻)      (鲜胚移植)
-    ↓           ↓
-  [节点]      [节点]
-```
-
-**适用场景**:两条独立且平行的路径(如冷冻 vs 鲜胚移植)
-
-### 4.2 树形分叉(多条分支)
-```
-         主干
-          ↓
-    ┌─────┼─────┐
-    ↓     ↓     ↓
-  分支A  分支B  分支C
-  (移植) (冷冻) (删除)
-```
-
-**适用场景**:三条及以上分支(如胚胎去向:移植/冷冻/删除/作废)
-
-### 4.3 回流路径(回到前面某步)
-```
-  [对焦] → [拍照]
-     ↑        ↓
-     └←─[失败重试]
-```
-
-**适用场景**:失败重试、循环逻辑(如对焦失败→重新对焦)
-
-### 4.4 条件分支(if-else)
-```
-       [判断条件]
-         ↙   ↘
-    [条件成立]  [条件不成立]
-```
-
-**适用场景**:业务判断(如:是否首次对焦、是否到达拍照时间)
-
----
-
-## 五、交互规范
-
-### 5.1 节点点击
-- **默认状态**:节点显示标题+图标+标签
-- **hover 状态**:节点放大 1.05 倍,出现阴影 `box-shadow: 0 4px 12px rgba(0,0,0,0.15)`
-- **激活状态**:节点放大 1.12 倍,边框加粗,右侧滑出详情面板
-
-### 5.2 详情面板
-- **滑出动画**:从右侧滑入,transition 0.3s ease
-- **面板宽度**:固定 `600px`(不遮挡主画布)
-- **关闭方式**:点击面板外区域 / 按 ESC 键 / 点右上角 × 按钮
-
-### 5.3 连线绘制
-- **使用 SVG**:动态计算节点中心点坐标,绘制路径
-- **路径算法**:
-  - 直线:`M x1,y1 L x2,y2`
-  - 折线:`M x1,y1 L x1,midY L x2,midY L x2,y2`(适用跨模块)
-  - 曲线:`M x1,y1 Q cpX,cpY x2,y2`(适用回流)
-
----
-
-## 六、代码结构规范
-
-### 6.1 HTML 结构
-```html
-<!DOCTYPE html>
-<html lang="zh-CN">
-<head>
-  <meta charset="UTF-8">
-  <title>时差培养箱 - [功能名]业务流程图</title>
-  <style>
-    /* CSS 变量定义颜色 */
-    :root {
-      --operate-color: #4A90E2;
-      --control-color: #FF9800;
-      --front-color: #9C27B0;
-      --branch-color: #FFA726;
-      --error-color: #F44336;
-    }
-    /* 流程图容器:无限宽高 */
-    .flow-container {
-      position: relative;
-      min-width: 100vw;
-      min-height: 100vh;
-      padding: 100px;
-    }
-  </style>
-</head>
-<body>
-  <!-- 顶部固定栏 -->
-  <header class="top-bar">
-    <h1>时差培养箱 - [功能名]业务流程图</h1>
-    <nav><!-- 导航按钮 --></nav>
-  </header>
-  
-  <!-- 流程画布 -->
-  <div class="flow-container" id="flowContainer">
-    <svg id="svgLines"></svg>
-    <!-- 节点由 JS 动态生成 -->
-  </div>
-  
-  <!-- 详情面板 -->
-  <div class="detail-panel" id="detailPanel">
-    <!-- 动态填充 -->
-  </div>
-  
-  <script>
-    // 节点数据 + 绘制逻辑
-  </script>
-</body>
-</html>
-```
-
-### 6.2 节点数据结构
-```javascript
-const flowData = {
-  nodes: [
-    {
-      id: 'operate-add-dish',
-      type: 'operate',  // operate / control / front / branch / error
-      title: '新建患者入箱',
-      icon: '🖥️',
-      x: 200,  // 绝对坐标
-      y: 100,
-      detail: {
-        description: '医生在空舱点击,弹窗录入患者信息...',
-        preconditions: ['舱室状态=空舱', '用户已登录'],
-        trigger: 'operate 主界面点空舱格',
-        steps: ['选孔位', '填表单', '点保存'],
-        backend: 'StartDishApi → dish表插入 → MQTT发StartDish',
-        dataChanges: ['dish表插入', 'HouseBin.CurrentDish设值'],
-        crossPlatform: {
-          from: 'operate 点按钮',
-          to: ['control 收到MQTT启动对焦拍照', 'front 界面显示新记录']
-        },
-        nextBranches: ['平衡流程', '直接开始培养'],
-        exceptions: ['硬件异常', 'control离线'],
-        codeLocation: ['AddDishWindowView.xaml.cs:451', 'AppData.cs:1102']
-      }
-    }
-  ],
-  edges: [
-    {
-      from: 'operate-add-dish',
-      to: 'control-start-dish',
-      type: 'mqtt',  // internal / api / mqtt / sync / error
-      label: 'MQTT StartDish'
-    }
-  ]
-};
-```
-
----
-
-## 七、制作清单(每次制作流程图必做)
-
-### 7.1 前期准备
-- [ ] 用 codegraph 挖通完整业务链路(从入口到出口,包括所有分支)
-- [ ] 列出涉及的三端文件(operate / control / front 各有哪些文件参与)
-- [ ] 列出涉及的数据表(哪些表会增删改查)
-- [ ] 列出涉及的接口/MQTT(哪些 API / topic 会被调用)
-
-### 7.2 节点设计
-- [ ] 每个操作/判断/事件都抽象成一个节点
-- [ ] 给每个节点定义唯一 ID(格式:`端-功能-动作`,如 `operate-add-dish-save`)
-- [ ] 给每个节点填写完整的 10 个板块内容(见第三章)
-- [ ] 确认每个节点的前驱节点和后继节点
-
-### 7.3 分支设计
-- [ ] 列出所有决策点(用户选择 / 业务判断 / 异常分叉)
-- [ ] 每个决策点画出真实的分支路径(不折叠成文字)
-- [ ] 确认分支的汇合点(是否回到主干 / 各走各的 / 结束流程)
-
-### 7.4 连线设计
-- [ ] 用不同颜色区分本端流程 / 跨端调用 / 异常路径
-- [ ] 回流路径用曲线 + 箭头清晰标注方向
-- [ ] 跨模块连线用折线(避免直线穿过其他节点)
-
-### 7.5 三端联动设计
-- [ ] 每个跨端操作都明确:谁触发 → 经过什么 → 影响谁
-- [ ] 在详情面板的"三端联动影响"板块写清楚完整链路
-- [ ] 用不同颜色连线体现三端关系
-
-### 7.6 测试与验收
-- [ ] 点击每个节点,详情面板能正常弹出
-- [ ] 详情面板的 10 个板块内容都完整
-- [ ] 所有分支路径都画出来了(没有折叠成文字)
-- [ ] 异常路径、回流路径都标注清楚
-- [ ] 三端联动的影响都写明白了
-- [ ] 画布能正常滚动(宽高没限制死)
-
----
-
-## 八、常见错误与纠正
-
-### ❌ 错误 1:把分支写成文字
-```
-[入箱] → [点击后可选:平衡 或 直接开始培养] → [开始培养]
-```
-**纠正**:必须画出真实分支
-```
-         [入箱]
-          ↓
-    ┌─────┴─────┐
-    ↓           ↓
- [平衡]    [直接开始培养]
-    ↓           ↓
-[结束平衡]      ↓
-    ↓           ↓
-    └─────┬─────┘
-          ↓
-     [开始培养]
-```
-
-### ❌ 错误 2:详情面板内容不完整
-**常见缺失**:
-- 缺前置条件(不知道什么时候能进这步)
-- 缺三端联动(不知道影响了哪些端)
-- 缺异常分支(不知道失败了怎么办)
-- 缺代码位置(不知道去哪改)
-
-**纠正**:严格按第三章的 10 个板块填写
-
-### ❌ 错误 3:连线颜色不区分
-**所有连线都一个颜色** → 看不出哪些是本端流程、哪些是跨端调用
-
-**纠正**:按第 2.3 节规范,用不同颜色 + 线型区分
-
-### ❌ 错误 4:画布限制死宽高
-```css
-.flow-container {
-  width: 1920px;  /* ❌ 限死了 */
-  height: 1080px; /* ❌ 限死了 */
-  overflow: hidden; /* ❌ 超出部分被裁 */
-}
-```
-
-**纠正**:使用 `min-width` / `min-height` + 允许滚动
-```css
-.flow-container {
-  min-width: 100vw;
-  min-height: 100vh;
-  /* 不设 max-width / max-height */
-}
-```
-
----
-
-## 九、参考示例
-
-### 示例 1:完整的"入箱→培养→拍照→看图→标记→结束"流程图
-- 文件:`时差培养箱-培养全流程详图.html`(本次制作)
-- 特点:覆盖正常路径、分支路径、异常路径、回流路径、三端联动
-
-### 示例 2:参考模板(flow-click.html)
-- 位置:`C:/Users/AIVFO/Desktop/flow-click.html`
-- 特点:点击节点弹浮动面板、多条并行分支、SVG 连线
-
----
-
-## 十、总结
-
-✅ **记住这三点,流程图就不会出错**:
-1. **分支必须真实画出来**(不折叠成文字)
-2. **详情面板必须写全 10 个板块**(不漏掉任何一项)
-3. **三端联动必须说清楚**(谁触发→经过什么→影响谁)
-
-📌 **每次制作前,先读这份规范,制作时对照清单逐项检查。**

+ 420 - 0
项目文档/流程图制作规范-可复用模板.mdc

@@ -0,0 +1,420 @@
+# 时差培养箱业务流程图制作规范(可复用模板)
+
+> **用途**:统一流程图制作标准,确保任何功能的流程图都能:
+> 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
+<script src="dagre.min.js"></script>
+<script src="flow-data.js"></script>
+<script src="flow-render.js"></script>
+```
+> 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
+<div class="flow-node operate" id="node-op-add">
+  <div class="node-head"><span class="node-icon">🖥️</span><span class="node-title">新建患者入箱</span></div>
+  <span class="node-tag">operate 操作端</span>
+  <div class="node-brief">录入患者信息 + 选 16 孔位 + 受精方式</div>  <!-- 一句话摘要,画布上直接可见 -->
+</div>
+```
+> **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 `<marker>`,每种 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;i<b.length;i++)for(let j=i+1;j<b.length;j++){const a=b[i],c=b[j];if(a.x<c.x+c.w&&a.x+a.w>c.x&&a.y<c.y+c.h&&a.y+a.h>c.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 文件改最省事。**