ooodc a7883dbed9 refactor(todo): 重构待办事项管理逻辑及更新状态规则
- 移除 TodoItem 中的 priority、created_at 和 updated_at 字段
- 强制每个任务都必须有唯一 id,且由用户负责生成
- 修改合并模式逻辑,merge=true 下保留未提及的旧任务
- 支持已完成和已取消任务重新激活(状态改回 pending 或 in_progress)
- 禁止 in_progress 状态退回到 pending,必须标记为 completed 或 cancelled
- 优化状态转换校验,允许特定状态间合法切换
- 简化任务变更消息,移除详细的新增/更新/移除统计
- 更新文档和示例,明确 id 必须由用户生成和使用
- 修复和补充测试,增强状态转换和合并模式验证
- 调整任务时间戳生成逻辑,统一使用当前时间及索引
- 该变更提供更合理的任务状态机械及管理模式,提升稳定性和易用性
2026-06-13 09:22:33 +08:00

375 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 布局系统
## 布局决策
> 不要靠关键词猜布局。先分析信息结构,再决定布局策略。
> 本文件负责说明通用布局原则与骨架模板;字段语义看 `elements/schema.md`,完整场景范式看各 `scenes/*.md`。
总原则:**先定主布局,再定子布局。**
**快速判断**
- **Flex**:按层分、按区排
- **Dagre**:关系网密、流程链主导
- **绝对定位**:空间位置承载信息(地理方位、拓扑坐标、物理面板等),用脚本计算坐标
- **默认选择**:拿不准时优先用 **Flex**
**Dagre 版式统一原则**
1. Dagre 解决的是**拓扑关系**,不是自动把画布铺满。
2. Dagre 作为子容器嵌套时默认是不透明节点Opaque Node先根据内部拓扑计算自身包围盒再作为原子节点参与父层布局。若需连线穿透边界须声明 `layout: "dagre"` + `layoutOptions: { isCluster: true }`
3. 混合布局时Flex 更适合负责分区与层次Dagre 更适合负责局部复杂关系;但如果 Dagre 本身就是主布局,也完全可以直接承担整张图的主体拓扑。
4. 选用 Dagre 前先看三件事:**最长链路方向、分支是否对称、是否有长回边/重试回路**。哪一项失衡,哪一项就会把包围盒撑歪。
5. 长回边、失败重试、跨层返回等关系,优先收敛到局部;必要时拆成局部流程区或旁路说明,不要让一条边把整个 Dagre 宽度拉爆。
6. 若 Dagre 产物在父容器中出现明显单侧留白、宽高失衡或内容只占很小一部分,必须调整 `rankdir`、重构拓扑,或在父层补充对称信息区,不能原样交付。
**读代码画架构图**:扫目录结构(按层分 → Flex按功能模块分 → 看依赖方向)→ grep import单向→Flex网状→ Dagre 或 Flex + Dagre→ 拿不准 → 默认 Flex。
> **flex 容器内的 `x/y` 会被完全忽略!**
❌ 致命错误:
```json
{ "type": "frame", "layout": "vertical", "children": [
{ "type": "rect", "x": 100, "y": 0, "text": "成都" },
{ "type": "rect", "x": 540, "y": 0, "text": "康定" }
]}
```
✅ 正确:用 `layout: "none"` 或放在顶层 nodes 用 x/y。
> **`layout: "none"`(绝对定位)的容器必须有明确的固定宽高!**
❌ 致命错误:
```json
{ "type": "frame", "layout": "none", "width": "fit-content", "height": "fit-content", "children": [
{ "type": "rect", "x": 0, "y": 0, "text": "区域A" },
{ "type": "rect", "x": 500, "y": 0, "text": "区域B" }
]}
```
✅ 正确:必须给绝对定位容器明确的固定宽高:
```json
{ "type": "frame", "layout": "none", "width": 1064, "height": 680, "children": [
{ "type": "rect", "x": 0, "y": 0, "text": "区域A" },
{ "type": "rect", "x": 554, "y": 0, "text": "区域B" }
]}
```
**构建方式**
| 布局类型 | 做法 |
| ---------------------- | ----------------------------------------------------------------------------- |
| 纯 Flex / Dagre | 直接写 JSON |
| 混合布局 (Flex包Dagre) | 直接写 JSON外层先做分区局部复杂关系交给 Dagre若被嵌套默认为不透明节点 |
| 极度依赖几何坐标的图 | 写脚本生成 JSONnode xxx.cjs |
| 需要精确避让的特殊线 | 脚本 + `--layout` 两阶段 |
---
## 网格方法论
核心理念:**先画网格,再填内容**。
先回答三个问题:
1. **信息分几行几列?** 每组一行或一列
2. **每格多大?** 等宽还是有主次?
3. **行列间距多大?** 分区间 24-32px同区内 12-16px
---
## 布局模式选择
| 模式 | 适用场景 | DSL 映射 |
| ---- | ---------------------------- | -------------------------------------------------------- |
| grid | 架构图、对比表、卡片墙、看板 | vertical frame 嵌套 horizontal frame |
| flow | 复杂流程图、微服务交互 | `layout: "dagre"`,由引擎自动计算网状连线排版 |
| tree | 组织架构、模块依赖 | `layout: "dagre"``rankdir: "TB"` 或根节点居中的 Flex |
| free | 地理位置布局、物理面板还原 | `layout: "none"` + x/y |
大多数图表用 grid 或 flow 模式。只有节点坐标本身有强语义(如地图)时才用 free。
> 以上都是布局策略名称DSL 的 `layout` 属性值只支持 `'horizontal'`、`'vertical'`、`'none'`、`'dagre'` 四种。
---
## DSL 与 CSS Flexbox 属性映射
| DSL 属性 | 对应的 CSS 心智模型 | 限制 |
| -------------------------------- | -------------------------------------------------- | -------------------------------------------------------------------------- |
| `layout: 'horizontal'` | `flex-direction: row` | 不写 layout = 绝对定位 |
| `layout: 'vertical'` | `flex-direction: column` | 同上 |
| `layout: 'none'` | `position: absolute`(子节点用 x/y | 子节点不能用 `fill-container`;容器必须有固定宽高 |
| `layout: 'dagre'` | 类似 Mermaid / DOT 的有向图布局 | 宽高只支持 `fit-content`;先按拓扑算包围盒再参与父层布局;嵌套时默认为不透明节点 |
| `width/height: 'fill-container'` | `flex: 1`(主轴)/ `align-self: stretch`(交叉轴) | 祖先必须有确定尺寸 |
| `width/height: 'fit-content'` | `width/height: auto` | — |
| `alignItems` | 同 CSS `align-items` | 仅 `'start'`/`'center'`/`'end'`/`'stretch'`(无 flex- 前缀) |
| `justifyContent` | 同 CSS `justify-content` | 仅 `'start'`/`'center'`/`'end'`/`'space-between'`/`'space-around'` |
| `gap` | 同 CSS `gap` | 必须显式写(不写节点会粘连) |
| `padding` | 同 CSS `padding` | 必须显式写。支持 `number` / `[v,h]` / `[t,r,b,l]` |
`alignItems` 默认值为 `'start'`CSS Flexbox 默认 `stretch`)。需要等高卡片时必须显式写 `alignItems: 'stretch'`
DSL 的语法是严格白名单,不能写原生 CSS 属性(不支持 `alignSelf``flexWrap``margin` 等)。
---
## DSL 注意事项
1. **frame 必须写 layout 属性**,不写时子节点全堆在左上角。
2. **fill-container 死锁陷阱**:使用 `fill-container` 时,祖先链中必须有固定宽度(或高度),否则和 `fit-content` 形成死锁,尺寸退化为 0。
错误示例:
```json
{ "type": "frame", "layout": "horizontal", "width": "fit-content", "children": [
{ "type": "rect", "width": "fill-container" }
]}
```
正确示例:
```json
{ "type": "frame", "layout": "horizontal", "width": 1200, "children": [
{ "type": "rect", "width": "fill-container" }
]}
```
3. **不要给 Dagre 套固定宽高的外框**Dagre 产物尺寸由拓扑决定,无法提前预知。父容器应使用 `fit-content` 自适应,或直接让 Dagre 作为顶层容器,不要用固定像素框住它。
4. **`layout: 'none'` 的容器必须有固定宽高**,不要写成 `fit-content`,否则子节点绝对定位容易错乱。
5. **含文字节点高度用 fit-content**,引擎不支持 overflow写死高度会截断文字。
6. **Shape 节点有内边距**rect/ellipse/diamond/triangle 各边 12pxcylinder 垂直 +42px。
7. **不支持 flex-wrap**,需要换行时用嵌套 frame 模拟。
8. **图层顺序**:数组中越靠后的节点层级越高。需要叠加标注时放在数组最后。
---
## 布局选择指南
| 你要表达的关系 | 怎么排 | DSL 写法 |
| -------------------------- | ------------------------ | ---------------------------------------------------------------------------- |
| 先后顺序、层级从上到下 | 纵向堆叠 | `layout: 'vertical'` |
| 并列、同等重要、可对比 | 横向等分 | `layout: 'horizontal'` + `alignItems: 'stretch'` + `width: 'fill-container'` |
| 区域有名称,名称在侧边 | 侧标签 + 内容并排 | 横向 frame: [text(标签), frame(内容)] |
| 多个大分区,各自独立 | 分区纵向排列 | 纵向 frame 包多个彩色 frame |
| 一行放不下,需要换行 | 嵌套横向 frame 模拟换行 | 纵向 frame 包多个横向 frame |
| 复杂的网状关系、拓扑图 | **Dagre 有向图自动布局** | `layout: 'dagre'` + `layoutOptions.edges` |
| 节点位置本身有含义(地图) | 绝对定位 | `layout: 'none'` + x/y |
这些可以自由嵌套组合。比如:纵向堆叠(标题) + 分区纵向排列(多个层) + 每个层内横向等分(节点)。
---
## 布局示例
### 纵向堆叠(标题 + 内容)
```json
{
"type": "frame", "layout": "vertical", "gap": 28, "padding": 32,
"width": 1200, "height": "fit-content",
"children": [
{ "type": "text", "width": "fill-container", "height": "fit-content",
"text": "图表标题", "fontSize": 24, "textAlign": "center" },
...内容...
]
}
```
### 横向等分(并列元素)
```json
{
"type": "frame", "layout": "horizontal", "gap": 16, "padding": 0,
"width": "fill-container", "height": "fit-content",
"alignItems": "stretch",
"children": [
{ "type": "rect", "width": "fill-container", "height": "fit-content",
"textAlign": "center", "verticalAlign": "middle", "text": "A" },
{ "type": "rect", "width": "fill-container", "height": "fit-content",
"textAlign": "center", "verticalAlign": "middle", "text": "B" }
]
}
```
`alignItems: 'stretch'` + `width: 'fill-container'` = 等宽等高。
### 侧标签 + 内容
```json
{
"type": "frame", "layout": "horizontal", "gap": 24, "padding": 0,
"width": "fill-container", "height": "fit-content",
"alignItems": "center",
"children": [
{ "type": "text", "width": 160, "height": "fit-content",
"text": "区域名称", "fontSize": 20, "textColor": "#1F2329", "textAlign": "right" },
{ "type": "frame", "width": "fill-container", "height": "fit-content",
...区域内容...
}
]
}
```
不要用 frame 的 `title` 属性做标签——渲染为极小标题栏,不可读。
### 分区纵向排列
把内容划分为几个大区域,每个区域用不同颜色区分(颜色从 style 文件的色板选取):
```json
{
"type": "frame", "layout": "vertical", "gap": 28, "padding": 0,
"width": "fill-container", "height": "fit-content",
"children": [
{ "type": "frame", "borderRadius": 8,
"layout": "horizontal", "gap": 16, "padding": 20, ...区域1... },
{ "type": "frame", "borderRadius": 8,
"layout": "horizontal", "gap": 16, "padding": 20, ...区域2... }
]
}
```
### 模拟换行
一行放不下时,拆成多个横向 frame
```json
{
"type": "frame", "layout": "vertical", "gap": 8, "padding": 0,
"children": [
{ "type": "frame", "layout": "horizontal", "gap": 8, "padding": 0,
"children": [item1, item2, item3, item4] },
{ "type": "frame", "layout": "horizontal", "gap": 8, "padding": 0,
"children": [item5, item6] }
]
}
```
## 复杂拓扑混合布局 (Dagre + Flex)
当你在处理**连线众多、关系杂乱的拓扑图 / 链路流程图 / 复杂架构图**时,不用手动去算每个节点坐标,优先考虑 **Flex + Dagre 的混合布局策略**。这主要包含两种维度的嵌套:
* **外层 Dagre + 内层 Flex复杂节点****这是最推荐的复杂架构画法**。整图拓扑交由 `layout: "dagre"` 自动计算并顺滑布线,而图中的节点不再只是单调的矩形,可以是一个用 Flex 自由拼装的复杂 `frame` 卡片(包含图标、主次标题、状态等),让节点承载更丰富的信息。
* **外层 Flex + 内层 Dagre局部流程**:外层用 Flex 或绝对定位划分大的业务区域,而某个特定区域内部放入 `layout: "dagre"` 容器负责处理局部的业务流。
* **嵌套前先做宽度预判**Dagre 会根据拓扑尽情往两侧撑出包围盒。如果可能横跨导致溢出,优先改 `rankdir` 为 `TB`、缩短文案、调小 `nodesep/ranksep`,必要时将超长的链路拆成分步区。
```json
{
"type": "frame", "id": "arch_root",
"layout": "dagre", "padding": 40,
"width": "fit-content", "height": "fit-content",
"layoutOptions": {
"rankdir": "LR", "nodesep": 60, "ranksep": 100,
"edges": [
["client", "auth_svc", "request"],
["auth_svc", "order_svc"],
["order_svc", "order_db"]
]
},
"children": [
{
"type": "frame", "id": "client",
"layout": "vertical", "gap": 6, "padding": [12, 16],
"alignItems": "center",
"fillColor": "#F8FAFC", "borderColor": "#CBD5E1", "borderWidth": 2, "borderRadius": 10,
"children": [
{ "type": "text", "text": "Client App", "fontSize": 14, "textColor": "#0F172A" },
{ "type": "text", "text": "React 18", "fontSize": 10, "textColor": "#64748B" }
]
},
{
"type": "frame", "id": "cluster_gateway",
"layout": "dagre", "layoutOptions": { "isCluster": true, "clusterTitle": "Gateway Tier", "clusterTitleColor": "#15803D" },
"fillColor": "#F0FDF4", "borderColor": "#86EFAC",
"borderWidth": 2, "borderDash": "dashed", "borderRadius": 16,
"children": [
{ "type": "rect", "id": "auth_svc", "width": 120, "height": 40, "text": "Auth Service", "fillColor": "#DCFCE7", "borderColor": "#86EFAC", "borderWidth": 1, "borderRadius": 6, "fontSize": 12 },
{ "type": "rect", "id": "order_svc", "width": 120, "height": 40, "text": "Order Service", "fillColor": "#DCFCE7", "borderColor": "#86EFAC", "borderWidth": 1, "borderRadius": 6, "fontSize": 12 }
]
},
{
"type": "frame", "id": "order_db",
"layout": "vertical", "gap": 4, "padding": [10, 14],
"alignItems": "center",
"fillColor": "#FFFFFF", "borderColor": "#FECACA", "borderWidth": 2, "borderRadius": 10,
"children": [
{ "type": "cylinder", "width": 50, "height": 36, "fillColor": "#FCA5A5", "borderColor": "#DC2626", "borderWidth": 1 },
{ "type": "text", "text": "Order DB", "fontSize": 12, "textColor": "#7F1D1D" }
]
}
]
}
```
**示例要点**
- `client` 和 `order_db` 是 **Flex 复合节点**(不透明节点),内部用 vertical 布局组合多行信息,对外层 Dagre 是固定宽高的原子。
- `cluster_gateway` 是 **透明子图**`layout: "dagre"` + `isCluster: true`),外部连线可穿越边界直达 `auth_svc` 和 `order_svc`。
- 所有 `edges` 统一写在最外层根 Dagre 的 `layoutOptions` 中。
**Dagre 嵌套排版规则**
1. **不透明节点Opaque Node**Dagre 内的子容器,无论其内部 layout 是 flex、absolute 还是 dagre只要未声明 isCluster: true对外层 Dagre 就是具有确定宽高的不透明原子节点。外层连线无法寻址其内部子节点。
2. **连线兜底重定向Edge Redirect Fallback**:当 edges 引用了某不透明节点内部的子节点 ID 时,引擎自动将该连线端点重定向至其最近的不透明祖先节点。不报错,不产生悬空连线。
3. **透明子图Compound Cluster**:子容器同时声明 `layout: "dagre"` 与 `layoutOptions: { isCluster: true }` 时,成为外层 Dagre 的复合子图。其内部子节点直接参与外层拓扑运算,连线可穿越子图边界。子图自身不执行独立排版,尺寸由外层 Dagre 根据内部节点包围盒自动撑开。
---
## 绝对定位
当节点位置本身有含义(拓扑图、地图、时间线轴)时用绝对定位。大多数图表优先用 Flex。
### 混合布局
模块内部用 Flex 自动排版,模块之间用绝对定位自由摆放。注意:承载这些模块的 `layout: "none"` 父容器必须先给出**固定宽高**,再在里面摆放子模块。
```json
{
"type": "frame", "layout": "none", "width": 1200, "height": 800,
"children": [
{
"type": "frame", "id": "module-a", "x": 100, "y": 100,
"width": 300, "height": "fit-content",
"layout": "vertical", "gap": 8, "padding": 16,
"children": [
{ "type": "rect", "width": "fill-container", "height": "fit-content", "text": "内容1" },
{ "type": "rect", "width": "fill-container", "height": "fit-content", "text": "内容2" }
]
}
]
}
```
### 两阶段绘图
先出骨架图导出坐标,再基于坐标补充连线和注解:
```bash
npx -y @larksuite/whiteboard-cli@^0.2.11 -i skeleton.json -o step1.png -l coords.json
```
`coords.json` 包含每个带 id 节点的精确坐标absX, absY, width, height
---
## 常用间距和尺寸
| 参数 | 常用范围 | 说明 |
| ---------------- | ----------- | ------------ |
| 整图宽度 | 1000-1400px | — |
| 分区之间间距 | 24-32px | — |
| 同分区内节点间距 | 12-16px | — |
| 有连线的节点间距 | >= 40px | 给箭头留空间 |
| 分区内边距 | 16-24px | — |
| 侧标签宽度 | 120-180px | — |
---
## 等大卡片
一排卡片需要等宽等高时,不要写固定像素:
```json
{
"type": "frame", "layout": "horizontal", "gap": 16, "padding": 0,
"alignItems": "stretch",
"children": [
{ "type": "rect", "width": "fill-container", "height": "fit-content", "text": "A" },
{ "type": "rect", "width": "fill-container", "height": "fit-content", "text": "B" }
]
}
```
`alignItems: 'stretch'` + `width: 'fill-container'` = 等宽等高。