forked from AxTps/Starlight_Lancher
feat:移除了弹窗,服务器添加sls
This commit is contained in:
433
.claude/skills/lark-whiteboard/scenes/architecture.md
Normal file
433
.claude/skills/lark-whiteboard/scenes/architecture.md
Normal file
@ -0,0 +1,433 @@
|
||||
# 系统架构图
|
||||
|
||||
适用于:分层架构图、微服务架构图、前后端架构图等有明确模块划分的场景。
|
||||
|
||||
## Content 约束
|
||||
|
||||
- **充分展开**:用户说"IM 架构",要展开到接入层(Web/iOS/Android/桌面)、网关层(接入/路由/安全)、服务层(核心服务+支撑服务两个子区域)、存储层(MySQL/Redis/MongoDB + 括号说明用途)
|
||||
- 每层节点 3-6 个。超过 6 个分两排或拆为子区域(如"核心服务"和"支撑服务"各一个子 frame)
|
||||
- 层标签简短(2-4 字),如"接入层""网关层"
|
||||
- 每个节点有标题 + 简短说明(如"用户服务\n注册登录和权限管理")
|
||||
- 技术组件加括号注明技术栈(如"消息队列\n(Kafka)")
|
||||
- 存储节点必须用 `cylinder` 类型(弧度固定 16px,禁止 `fill-container` 宽度,用 120-200 固定宽度)。每行最多 4 个 cylinder(超过 4 个换行或合并同类项,如多个 MySQL 合并为"关系数据库\n(MySQL)")
|
||||
- 侧边栏(如运维监控、基础设施)只在用户明确要求时才加,最多 2-3 项。不要自作主张添加侧边栏
|
||||
- 可使用 icon+text 组合更直观的进行内容展示和增强辨识度
|
||||
- **连线:非必要不画。** 架构图的分层结构本身已表达了调用方向(上层调下层),不需要每对节点都连线。只在需要强调特定调用关系时才画,且总数不超过 3-5 条
|
||||
|
||||
## Layout 选型
|
||||
|
||||
| 模式 | 适用条件 | 特征 |
|
||||
| -------------------- | ----------------------------------------- | -------------------------------------------------------------------------------------------- |
|
||||
| **grid(分层条带)** | 有明确上下层级关系(接入→网关→服务→存储) | 行=层级,每行 horizontal frame 等分节点。左侧 text 标签 + 右侧层 frame(Label-Outside 模式) |
|
||||
| **grid(网格矩阵)** | 多模块平级,无明确层级 | N×M 网格等分,每格一个模块 |
|
||||
| **混合(岛屿式)** | 模块间网状互联,无清晰分层 | 宏观 `layout: "none"` + x/y 定位各模块岛屿,微观每个岛屿内部用 flex 布局 |
|
||||
|
||||
## Layout 规则
|
||||
|
||||
- **根节点**:固定宽度(1200),`height: "fit-content"`,`layout: "vertical"`,`gap: 20`,`padding: 24`
|
||||
- **主体双栏**(有侧边栏时):horizontal frame,`alignItems: "stretch"`,`gap: 16`
|
||||
- 左侧 layers-container:`width: "fill-container"`,vertical,`gap: 16`
|
||||
- 右侧 sidebar:固定宽度 160-180,`height: "fill-container"`,`justifyContent: "space-between"`
|
||||
- **单层(Label-Outside)**:horizontal frame,左侧 text 标签(`width: 80`,`textAlign: "right"`),右侧层 frame(`fill-container`,带 borderWidth/borderRadius,`padding: 24`,`gap: 16`)。**为什么用 Label-Outside**:标签放在 frame 外部更简洁,避免在 frame 内部嵌套窄 rect 导致竖排文字和对齐问题。
|
||||
- **子区域**:在层 frame 内嵌套 horizontal wrapper(`alignItems: "stretch"` 保证同行等高),内含多个 vertical frame(各子区域),每个子区域有自己的标题 text + 内容行。行内组件 `width: "fill-container"` 自动均分。
|
||||
- **侧边栏**:拆成独立的逻辑块 frame(如"运维监控"和"基础设施"分开),各块 `height: "fill-container"`。外层 `justifyContent: "space-between"` 保证与左侧对齐,内部可设 `justifyContent: "center"` 使内容居中。
|
||||
- **行内标签**:层内如有贯穿多列的特殊组件(如中间件),可采用"左侧小标签 + 右侧组件组"的横向布局
|
||||
|
||||
## 骨架示例
|
||||
|
||||
### 分层条带(Label-Outside + 侧边栏)
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 2,
|
||||
"nodes": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "root",
|
||||
"x": 0, "y": 0,
|
||||
"width": 1200,
|
||||
"height": "fit-content",
|
||||
"layout": "vertical",
|
||||
"gap": 20,
|
||||
"padding": 24,
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "title",
|
||||
"width": "fill-container",
|
||||
"height": "fit-content",
|
||||
"text": "[图表标题]",
|
||||
"fontSize": 24,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle"
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "main-container",
|
||||
"width": "fill-container",
|
||||
"height": "fit-content",
|
||||
"layout": "horizontal",
|
||||
"alignItems": "stretch",
|
||||
"gap": 16,
|
||||
"padding": 0,
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "layers-container",
|
||||
"width": "fill-container",
|
||||
"height": "fit-content",
|
||||
"layout": "vertical",
|
||||
"alignItems": "stretch",
|
||||
"gap": 16,
|
||||
"padding": 0,
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "row-layer-1",
|
||||
"width": "fill-container",
|
||||
"height": "fit-content",
|
||||
"layout": "horizontal",
|
||||
"gap": 24,
|
||||
"padding": 0,
|
||||
"alignItems": "center",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "label-1",
|
||||
"width": 80,
|
||||
"height": "fit-content",
|
||||
"text": "[层标签]",
|
||||
"fontSize": 20,
|
||||
"textAlign": "right"
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "layer-1",
|
||||
"width": "fill-container",
|
||||
"height": "fit-content",
|
||||
"borderWidth": 2,
|
||||
"borderRadius": 8,
|
||||
"layout": "horizontal",
|
||||
"gap": 16,
|
||||
"padding": 24,
|
||||
"alignItems": "stretch",
|
||||
"children": [
|
||||
{ "type": "rect", "id": "n-1-1", "width": "fill-container", "height": "fit-content", "text": "[节点名]", "borderRadius": 8, "borderWidth": 2, "fontSize": 14, "textAlign": "center", "verticalAlign": "middle" },
|
||||
{ "type": "rect", "id": "n-1-2", "width": "fill-container", "height": "fit-content", "text": "[节点名]", "borderRadius": 8, "borderWidth": 2, "fontSize": 14, "textAlign": "center", "verticalAlign": "middle" },
|
||||
{ "type": "rect", "id": "n-1-3", "width": "fill-container", "height": "fit-content", "text": "[节点名]", "borderRadius": 8, "borderWidth": 2, "fontSize": 14, "textAlign": "center", "verticalAlign": "middle" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "row-layer-2",
|
||||
"width": "fill-container",
|
||||
"height": "fit-content",
|
||||
"layout": "horizontal",
|
||||
"gap": 24,
|
||||
"padding": 0,
|
||||
"alignItems": "center",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "label-2",
|
||||
"width": 80,
|
||||
"height": "fit-content",
|
||||
"text": "[层标签]",
|
||||
"fontSize": 20,
|
||||
"textAlign": "right"
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "layer-2",
|
||||
"width": "fill-container",
|
||||
"height": "fit-content",
|
||||
"borderWidth": 2,
|
||||
"borderRadius": 8,
|
||||
"layout": "vertical",
|
||||
"gap": 16,
|
||||
"padding": 24,
|
||||
"alignItems": "stretch",
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "subareas-wrapper",
|
||||
"width": "fill-container",
|
||||
"height": "fit-content",
|
||||
"layout": "horizontal",
|
||||
"alignItems": "stretch",
|
||||
"gap": 16,
|
||||
"padding": 0,
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "subarea-a",
|
||||
"width": "fill-container",
|
||||
"height": "fit-content",
|
||||
"layout": "vertical",
|
||||
"gap": 8,
|
||||
"padding": 12,
|
||||
"borderRadius": 8,
|
||||
"borderWidth": 2,
|
||||
"children": [
|
||||
{ "type": "text", "id": "title-a", "width": "fill-container", "height": "fit-content", "text": "[子区域名]", "fontSize": 14, "textAlign": "center", "verticalAlign": "middle" },
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "row-a-1",
|
||||
"width": "fill-container",
|
||||
"height": "fit-content",
|
||||
"layout": "horizontal",
|
||||
"gap": 8,
|
||||
"padding": 0,
|
||||
"children": [
|
||||
{ "type": "rect", "id": "sa-1", "width": "fill-container", "height": "fit-content", "text": "[节点名]", "borderRadius": 8, "borderWidth": 2, "fontSize": 14, "textAlign": "center", "verticalAlign": "middle" },
|
||||
{ "type": "rect", "id": "sa-2", "width": "fill-container", "height": "fit-content", "text": "[节点名]", "borderRadius": 8, "borderWidth": 2, "fontSize": 14, "textAlign": "center", "verticalAlign": "middle" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "subarea-b",
|
||||
"width": "fill-container",
|
||||
"height": "fit-content",
|
||||
"layout": "vertical",
|
||||
"gap": 8,
|
||||
"padding": 12,
|
||||
"borderRadius": 8,
|
||||
"borderWidth": 2,
|
||||
"children": [
|
||||
{ "type": "text", "id": "title-b", "width": "fill-container", "height": "fit-content", "text": "[子区域名]", "fontSize": 14, "textAlign": "center", "verticalAlign": "middle" },
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "row-b-1",
|
||||
"width": "fill-container",
|
||||
"height": "fit-content",
|
||||
"layout": "horizontal",
|
||||
"gap": 8,
|
||||
"padding": 0,
|
||||
"children": [
|
||||
{ "type": "rect", "id": "sb-1", "width": "fill-container", "height": "fit-content", "text": "[节点名]", "borderRadius": 8, "borderWidth": 2, "fontSize": 14, "textAlign": "center", "verticalAlign": "middle" },
|
||||
{ "type": "rect", "id": "sb-2", "width": "fill-container", "height": "fit-content", "text": "[节点名]", "borderRadius": 8, "borderWidth": 2, "fontSize": 14, "textAlign": "center", "verticalAlign": "middle" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "row-layer-3",
|
||||
"width": "fill-container",
|
||||
"height": "fit-content",
|
||||
"layout": "horizontal",
|
||||
"gap": 24,
|
||||
"padding": 0,
|
||||
"alignItems": "center",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "label-3",
|
||||
"width": 80,
|
||||
"height": "fit-content",
|
||||
"text": "[层标签]",
|
||||
"fontSize": 20,
|
||||
"textAlign": "right"
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "layer-3",
|
||||
"width": "fill-container",
|
||||
"height": "fit-content",
|
||||
"borderWidth": 2,
|
||||
"borderRadius": 8,
|
||||
"layout": "horizontal",
|
||||
"gap": 0,
|
||||
"padding": 24,
|
||||
"justifyContent": "space-around",
|
||||
"children": [
|
||||
{ "type": "cylinder", "id": "db-1", "width": 140, "height": "fit-content", "text": "[存储名]", "borderWidth": 2, "fontSize": 14, "textAlign": "center", "verticalAlign": "middle" },
|
||||
{ "type": "cylinder", "id": "db-2", "width": 140, "height": "fit-content", "text": "[存储名]", "borderWidth": 2, "fontSize": 14, "textAlign": "center", "verticalAlign": "middle" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "right-sidebar-wrapper",
|
||||
"width": 180,
|
||||
"height": "fill-container",
|
||||
"layout": "vertical",
|
||||
"alignItems": "stretch",
|
||||
"justifyContent": "space-between",
|
||||
"gap": 16,
|
||||
"padding": 0,
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "side-block-1",
|
||||
"width": "fill-container",
|
||||
"height": "fill-container",
|
||||
"layout": "vertical",
|
||||
"alignItems": "stretch",
|
||||
"justifyContent": "center",
|
||||
"gap": 12,
|
||||
"padding": 16,
|
||||
"borderRadius": 8,
|
||||
"borderWidth": 2,
|
||||
"children": [
|
||||
{ "type": "text", "id": "side-title-1", "width": "fill-container", "height": "fit-content", "text": "[侧边栏模块名]", "fontSize": 14, "textAlign": "center", "verticalAlign": "middle" },
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "side-items-1",
|
||||
"width": "fill-container",
|
||||
"height": "fit-content",
|
||||
"layout": "vertical",
|
||||
"gap": 8,
|
||||
"padding": 0,
|
||||
"children": [
|
||||
{ "type": "rect", "id": "s-1", "width": "fill-container", "height": "fit-content", "text": "[节点名]", "borderRadius": 8, "borderWidth": 2, "fontSize": 14, "textAlign": "center", "verticalAlign": "middle" },
|
||||
{ "type": "rect", "id": "s-2", "width": "fill-container", "height": "fit-content", "text": "[节点名]", "borderRadius": 8, "borderWidth": 2, "fontSize": 14, "textAlign": "center", "verticalAlign": "middle" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "side-block-2",
|
||||
"width": "fill-container",
|
||||
"height": "fill-container",
|
||||
"layout": "vertical",
|
||||
"alignItems": "stretch",
|
||||
"justifyContent": "center",
|
||||
"gap": 12,
|
||||
"padding": 16,
|
||||
"borderRadius": 8,
|
||||
"borderWidth": 2,
|
||||
"children": [
|
||||
{ "type": "text", "id": "side-title-2", "width": "fill-container", "height": "fit-content", "text": "[侧边栏模块名]", "fontSize": 14, "textAlign": "center", "verticalAlign": "middle" },
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "side-items-2",
|
||||
"width": "fill-container",
|
||||
"height": "fit-content",
|
||||
"layout": "vertical",
|
||||
"gap": 8,
|
||||
"padding": 0,
|
||||
"children": [
|
||||
{ "type": "rect", "id": "s-3", "width": "fill-container", "height": "fit-content", "text": "[节点名]", "borderRadius": 8, "borderWidth": 2, "fontSize": 14, "textAlign": "center", "verticalAlign": "middle" },
|
||||
{ "type": "rect", "id": "s-4", "width": "fill-container", "height": "fit-content", "text": "[节点名]", "borderRadius": 8, "borderWidth": 2, "fontSize": 14, "textAlign": "center", "verticalAlign": "middle" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 岛屿式(网状互联)
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 2,
|
||||
"nodes": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "root",
|
||||
"x": 0, "y": 0,
|
||||
"width": 1200,
|
||||
"height": 800,
|
||||
"layout": "none",
|
||||
"padding": 24,
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "title",
|
||||
"x": 0, "y": 0,
|
||||
"width": 1152,
|
||||
"height": "fit-content",
|
||||
"text": "[图表标题]",
|
||||
"fontSize": 24,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle"
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "island-a",
|
||||
"x": 40, "y": 60,
|
||||
"width": 320,
|
||||
"height": "fit-content",
|
||||
"layout": "vertical",
|
||||
"gap": 12,
|
||||
"padding": 20,
|
||||
"borderWidth": 2,
|
||||
"borderRadius": 8,
|
||||
"children": [
|
||||
{ "type": "text", "id": "island-a-title", "width": "fill-container", "height": "fit-content", "text": "[模块名]", "fontSize": 16, "textAlign": "center", "verticalAlign": "middle" },
|
||||
{ "type": "rect", "id": "ia-1", "width": "fill-container", "height": "fit-content", "text": "[节点名]", "borderRadius": 8, "borderWidth": 2, "fontSize": 14, "textAlign": "center", "verticalAlign": "middle" },
|
||||
{ "type": "rect", "id": "ia-2", "width": "fill-container", "height": "fit-content", "text": "[节点名]", "borderRadius": 8, "borderWidth": 2, "fontSize": 14, "textAlign": "center", "verticalAlign": "middle" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "island-b",
|
||||
"x": 440, "y": 60,
|
||||
"width": 320,
|
||||
"height": "fit-content",
|
||||
"layout": "vertical",
|
||||
"gap": 12,
|
||||
"padding": 20,
|
||||
"borderWidth": 2,
|
||||
"borderRadius": 8,
|
||||
"children": [
|
||||
{ "type": "text", "id": "island-b-title", "width": "fill-container", "height": "fit-content", "text": "[模块名]", "fontSize": 16, "textAlign": "center", "verticalAlign": "middle" },
|
||||
{ "type": "rect", "id": "ib-1", "width": "fill-container", "height": "fit-content", "text": "[节点名]", "borderRadius": 8, "borderWidth": 2, "fontSize": 14, "textAlign": "center", "verticalAlign": "middle" },
|
||||
{ "type": "rect", "id": "ib-2", "width": "fill-container", "height": "fit-content", "text": "[节点名]", "borderRadius": 8, "borderWidth": 2, "fontSize": 14, "textAlign": "center", "verticalAlign": "middle" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "island-c",
|
||||
"x": 240, "y": 340,
|
||||
"width": 320,
|
||||
"height": "fit-content",
|
||||
"layout": "vertical",
|
||||
"gap": 12,
|
||||
"padding": 20,
|
||||
"borderWidth": 2,
|
||||
"borderRadius": 8,
|
||||
"children": [
|
||||
{ "type": "text", "id": "island-c-title", "width": "fill-container", "height": "fit-content", "text": "[模块名]", "fontSize": 16, "textAlign": "center", "verticalAlign": "middle" },
|
||||
{ "type": "rect", "id": "ic-1", "width": "fill-container", "height": "fit-content", "text": "[节点名]", "borderRadius": 8, "borderWidth": 2, "fontSize": 14, "textAlign": "center", "verticalAlign": "middle" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{ "type": "connector", "connector": { "from": "ia-1", "to": "ib-1", "fromAnchor": "right", "toAnchor": "left", "lineShape": "straight", "lineWidth": 2, "endArrow": "arrow" } },
|
||||
{ "type": "connector", "connector": { "from": "island-a", "to": "ic-1", "fromAnchor": "bottom", "toAnchor": "top", "lineShape": "rightAngle", "lineWidth": 2, "endArrow": "arrow" } },
|
||||
{ "type": "connector", "connector": { "from": "island-b", "to": "ic-1", "fromAnchor": "bottom", "toAnchor": "top", "lineShape": "rightAngle", "lineWidth": 2, "endArrow": "arrow" } }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 陷阱
|
||||
|
||||
- **所有架构图都用分层条带**:多模块平级网状互联时应选岛屿式;无明确层级时应选网格矩阵。先判断信息结构再选布局。
|
||||
- **连线过多导致交叉**:架构图非必要不画连线。分层结构本身已表达调用方向,不需要每对节点连线。如果一定要画,最多 3-5 条关键路径。
|
||||
- **层标签用 frame title(不可读)**:层标签必须用独立的 text 节点放在 frame 外侧(Label-Outside 模式),不要嵌入 frame 内部。
|
||||
- **cylinder 用 fill-container 宽度**:cylinder 弧度固定 16px 不随宽度缩放,必须用固定宽度(120-200)。
|
||||
- **侧边栏逻辑混合**:"运维监控"和"基础设施"必须是独立 frame,不可合并成一个长条。
|
||||
- **根节点没有固定宽度**:根 frame 必须有明确宽度(如 1200),否则子节点的 `fill-container` 无法计算。
|
||||
187
.claude/skills/lark-whiteboard/scenes/bar-chart.md
Normal file
187
.claude/skills/lark-whiteboard/scenes/bar-chart.md
Normal file
@ -0,0 +1,187 @@
|
||||
# 柱状图
|
||||
|
||||
## Content 约束
|
||||
|
||||
- 数据点 ≤ 12
|
||||
- 同一数据系列用同一颜色(不要每个柱不同色)
|
||||
- Y 轴必须有单位标注(如 "万元"、"人次")
|
||||
|
||||
## Layout 选型
|
||||
|
||||
- **脚本生成坐标**(推荐):用 .cjs 脚本计算柱体位置和高度,脚本输出 JSON 文件后调用 `npx -y @larksuite/whiteboard-cli@^0.2.13` 渲染
|
||||
- **绝对定位手写**:简单柱状图(≤ 5 个柱)可手写坐标
|
||||
|
||||
## Layout 规则
|
||||
|
||||
- 白板坐标系 Y 轴向下为正,图表"底部原点"拥有最大 Y 值,柱体向上生长时 Y 减小
|
||||
- 柱体等宽等间距,底部对齐 X 轴
|
||||
- 柱体高度:`height = (value / maxValue) * chartHeight`
|
||||
- 柱体 Y 坐标:`y = originY - height`
|
||||
- 坐标轴用 connector 直线,末端带箭头(endArrow: "arrow")
|
||||
- 格线用虚线 connector(lineStyle: "dashed",endArrow: "none")
|
||||
- 刻度线短横线 connector(endArrow: "none")
|
||||
- 数值标注放在柱体顶部上方
|
||||
- 类别标签放在 X 轴下方,居中对齐柱体
|
||||
|
||||
## 坐标与尺寸计算指南
|
||||
|
||||
白板坐标系中,**X 轴向右为正,Y 轴向下为正**。因此图表的"底部原点"实际上拥有最大的 Y 坐标,图形向上生长时 Y 坐标在不断减小。
|
||||
|
||||
1. **确定图表区域**:
|
||||
- 设定图表区高度 `chartHeight` 和宽度 `chartWidth`
|
||||
- 设定左下角坐标原点 `(originX, originY)`
|
||||
- 示例:originX=80, originY=480, chartWidth=1000, chartHeight=400
|
||||
2. **Y 轴映射(计算高度)**:
|
||||
- 找出数据的最大值 `maxValue`
|
||||
- 将 maxValue 向上取整到"整数刻度"(如数据最大 190 → maxValue 取 200)
|
||||
- 柱子高度:`height = (value / maxValue) * chartHeight`
|
||||
- 柱子 Y 坐标:`y = originY - height`
|
||||
3. **X 轴映射(计算宽度与 X 坐标)**:
|
||||
- 将 chartWidth 按数据个数均分:`slotWidth = chartWidth / barCount`
|
||||
- 设定柱子间距 `barGap`(推荐 slotWidth 的 25%-30%)
|
||||
- 柱子宽度:`barWidth = slotWidth - barGap`
|
||||
- 第 i 根柱子 X 坐标:`x = originX + i * slotWidth + barGap / 2`
|
||||
4. **Y 轴刻度计算**:
|
||||
- 将 0 到 maxValue 等分为 4-6 个刻度
|
||||
- 每个刻度的 Y 坐标:`gridY = originY - (tickValue / maxValue) * chartHeight`
|
||||
- 刻度线:从 (originX-10, gridY) 到 (originX, gridY) 的短横线
|
||||
- 网格线:从 (originX, gridY) 到 (originX+chartWidth, gridY) 的虚线
|
||||
|
||||
## 完整 JSON 示例
|
||||
|
||||
以下示例:3 根柱子,数据 [120, 200, 150],maxValue=200,originX=80, originY=480, chartWidth=900, chartHeight=400。
|
||||
|
||||
- slotWidth = 900 / 3 = 300
|
||||
- barGap = 80, barWidth = 220
|
||||
- 刻度:0, 50, 100, 150, 200(每 50 一格,gridInterval = 80px)
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 2,
|
||||
"nodes": [
|
||||
{ "type": "rect", "x": 0, "y": 0, "width": 1100, "height": 580 },
|
||||
|
||||
{ "type": "text", "x": 80, "y": 10, "width": 900, "height": "fit-content",
|
||||
"text": "季度销售额对比", "fontSize": 24, "textAlign": "center" },
|
||||
|
||||
{ "type": "text", "x": 10, "y": 40, "width": 60, "height": "fit-content",
|
||||
"text": "万元", "fontSize": 12, "textAlign": "center" },
|
||||
|
||||
{ "type": "connector", "connector": {
|
||||
"from": { "x": 80, "y": 480 }, "to": { "x": 80, "y": 55 },
|
||||
"lineShape": "straight", "lineWidth": 2, "endArrow": "arrow"
|
||||
}},
|
||||
{ "type": "connector", "connector": {
|
||||
"from": { "x": 80, "y": 480 }, "to": { "x": 1000, "y": 480 },
|
||||
"lineShape": "straight", "lineWidth": 2, "endArrow": "arrow"
|
||||
}},
|
||||
|
||||
{ "type": "connector", "connector": {
|
||||
"from": { "x": 70, "y": 480 }, "to": { "x": 80, "y": 480 },
|
||||
"lineShape": "straight", "lineWidth": 1,
|
||||
"startArrow": "none", "endArrow": "none"
|
||||
}},
|
||||
{ "type": "text", "x": 20, "y": 470, "width": 50, "height": 20,
|
||||
"text": "0", "fontSize": 12, "textAlign": "right" },
|
||||
|
||||
{ "type": "connector", "connector": {
|
||||
"from": { "x": 70, "y": 400 }, "to": { "x": 80, "y": 400 },
|
||||
"lineShape": "straight", "lineWidth": 1,
|
||||
"startArrow": "none", "endArrow": "none"
|
||||
}},
|
||||
{ "type": "text", "x": 20, "y": 390, "width": 50, "height": 20,
|
||||
"text": "50", "fontSize": 12, "textAlign": "right" },
|
||||
{ "type": "connector", "connector": {
|
||||
"from": { "x": 80, "y": 400 }, "to": { "x": 980, "y": 400 },
|
||||
"lineShape": "straight", "lineWidth": 1, "lineStyle": "dashed",
|
||||
"startArrow": "none", "endArrow": "none"
|
||||
}},
|
||||
|
||||
{ "type": "connector", "connector": {
|
||||
"from": { "x": 70, "y": 320 }, "to": { "x": 80, "y": 320 },
|
||||
"lineShape": "straight", "lineWidth": 1,
|
||||
"startArrow": "none", "endArrow": "none"
|
||||
}},
|
||||
{ "type": "text", "x": 20, "y": 310, "width": 50, "height": 20,
|
||||
"text": "100", "fontSize": 12, "textAlign": "right" },
|
||||
{ "type": "connector", "connector": {
|
||||
"from": { "x": 80, "y": 320 }, "to": { "x": 980, "y": 320 },
|
||||
"lineShape": "straight", "lineWidth": 1, "lineStyle": "dashed",
|
||||
"startArrow": "none", "endArrow": "none"
|
||||
}},
|
||||
|
||||
{ "type": "connector", "connector": {
|
||||
"from": { "x": 70, "y": 240 }, "to": { "x": 80, "y": 240 },
|
||||
"lineShape": "straight", "lineWidth": 1,
|
||||
"startArrow": "none", "endArrow": "none"
|
||||
}},
|
||||
{ "type": "text", "x": 20, "y": 230, "width": 50, "height": 20,
|
||||
"text": "150", "fontSize": 12, "textAlign": "right" },
|
||||
{ "type": "connector", "connector": {
|
||||
"from": { "x": 80, "y": 240 }, "to": { "x": 980, "y": 240 },
|
||||
"lineShape": "straight", "lineWidth": 1, "lineStyle": "dashed",
|
||||
"startArrow": "none", "endArrow": "none"
|
||||
}},
|
||||
|
||||
{ "type": "connector", "connector": {
|
||||
"from": { "x": 70, "y": 160 }, "to": { "x": 80, "y": 160 },
|
||||
"lineShape": "straight", "lineWidth": 1,
|
||||
"startArrow": "none", "endArrow": "none"
|
||||
}},
|
||||
{ "type": "text", "x": 20, "y": 150, "width": 50, "height": 20,
|
||||
"text": "200", "fontSize": 12, "textAlign": "right" },
|
||||
{ "type": "connector", "connector": {
|
||||
"from": { "x": 80, "y": 160 }, "to": { "x": 980, "y": 160 },
|
||||
"lineShape": "straight", "lineWidth": 1, "lineStyle": "dashed",
|
||||
"startArrow": "none", "endArrow": "none"
|
||||
}},
|
||||
|
||||
{ "type": "rect", "id": "bar-0", "x": 120, "y": 240,
|
||||
"width": 220, "height": 240, "borderRadius": 4 },
|
||||
{ "type": "text", "x": 120, "y": 215,
|
||||
"width": 220, "height": 20,
|
||||
"text": "120", "fontSize": 14, "textAlign": "center" },
|
||||
{ "type": "text", "x": 120, "y": 490,
|
||||
"width": 220, "height": 30,
|
||||
"text": "Q1", "fontSize": 14, "textAlign": "center" },
|
||||
|
||||
{ "type": "rect", "id": "bar-1", "x": 420, "y": 80,
|
||||
"width": 220, "height": 400, "borderRadius": 4 },
|
||||
{ "type": "text", "x": 420, "y": 55,
|
||||
"width": 220, "height": 20,
|
||||
"text": "200", "fontSize": 14, "textAlign": "center" },
|
||||
{ "type": "text", "x": 420, "y": 490,
|
||||
"width": 220, "height": 30,
|
||||
"text": "Q2", "fontSize": 14, "textAlign": "center" },
|
||||
|
||||
{ "type": "rect", "id": "bar-2", "x": 720, "y": 180,
|
||||
"width": 220, "height": 300, "borderRadius": 4 },
|
||||
{ "type": "text", "x": 720, "y": 155,
|
||||
"width": 220, "height": 20,
|
||||
"text": "150", "fontSize": 14, "textAlign": "center" },
|
||||
{ "type": "text", "x": 720, "y": 490,
|
||||
"width": 220, "height": 30,
|
||||
"text": "Q3", "fontSize": 14, "textAlign": "center" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
坐标推导验证:
|
||||
- bar-0 (120): height = (120/200)*400 = 240, y = 480-240 = 240
|
||||
- bar-1 (200): height = (200/200)*400 = 400, y = 480-400 = 80
|
||||
- bar-2 (150): height = (150/200)*400 = 300, y = 480-300 = 180
|
||||
- bar-0 x = 80 + 0*300 + 80/2 = 120, bar-1 x = 80 + 1*300 + 40 = 420, bar-2 x = 80 + 2*300 + 40 = 720
|
||||
|
||||
## 陷阱
|
||||
|
||||
- 单系列用多色(不专业):同一数据系列所有柱体应使用同一颜色
|
||||
- 缺 Y 轴单位标注,读者无法理解数值含义
|
||||
- 柱体间距不均匀(脚本需统一计算 barGap)
|
||||
- Y 轴刻度线和格线误带箭头
|
||||
- 坐标轴忘记带箭头
|
||||
|
||||
此场景必须用 .cjs 脚本生成。Agent 使用时只需修改 `data` 数组,其余坐标与柱体高度全自动计算。
|
||||
|
||||
```javascript
|
||||
const { writeFileSync } = require('fs');
|
||||
```
|
||||
135
.claude/skills/lark-whiteboard/scenes/comparison.md
Normal file
135
.claude/skills/lark-whiteboard/scenes/comparison.md
Normal file
@ -0,0 +1,135 @@
|
||||
# 对比图 / 矩阵图
|
||||
|
||||
适用于:方案对比、功能矩阵、技术选型等多选项按多维度比较的场景。
|
||||
|
||||
## Content 约束
|
||||
|
||||
- **每格内容要充实**:不要只写一个关键词,给出具体说明(如"MVCC 多版本并发控制,支持行级锁"而非仅"支持")
|
||||
- 单格内容不同格子允许不同长度,但每格不超过 5 行
|
||||
- 长文本(超过 15 字)用 `textAlign: "left"`(不要居中)
|
||||
- 第一行是标题行(对象名称),第一列是维度标签列
|
||||
- 维度数量至少 4 个,充分展开对比维度
|
||||
|
||||
## Layout 选型
|
||||
|
||||
| 模式 | 适用条件 | 特征 |
|
||||
|------|---------|------|
|
||||
| **严格 grid(默认)** | 所有对比场景 | 表头行 + 数据行,每行 horizontal frame,行内 rect 等分 |
|
||||
| **卡片式对比(替代)** | 维度较少(2-3 个) | 每个对象做一张独立卡片,卡片内纵向列出各维度。卡片横向等分:外层 `layout: "horizontal"`,每张卡片 `width: "fill-container"` |
|
||||
|
||||
## Layout 规则
|
||||
|
||||
- 最外层 frame:`layout: "vertical"`,固定 `width`(如 1000),`height: "fit-content"`
|
||||
- 每行:horizontal frame,`width: "fill-container"`,`alignItems: "stretch"`
|
||||
- 行内单元格全部 `width: "fill-container"` 等分列宽
|
||||
- 行间 `gap >= 12`(不要 8,太紧)
|
||||
- 行内列间 `gap: 8-12`
|
||||
- 标题行:深色底白字(由 style 控制具体颜色)
|
||||
- 每列同色边框保持视觉一致性
|
||||
- 单元格 `height: "fit-content"`,不要写固定 height
|
||||
|
||||
## 骨架示例
|
||||
|
||||
### 3 列 4 行表格
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 2,
|
||||
"nodes": [
|
||||
{
|
||||
"type": "frame",
|
||||
"width": 1000,
|
||||
"height": "fit-content",
|
||||
"layout": "vertical",
|
||||
"gap": 12,
|
||||
"padding": 0,
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "title",
|
||||
"width": "fill-container",
|
||||
"height": "fit-content",
|
||||
"text": "[对比图标题]",
|
||||
"fontSize": 24,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle"
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "header-row",
|
||||
"width": "fill-container",
|
||||
"height": "fit-content",
|
||||
"layout": "horizontal",
|
||||
"gap": 8,
|
||||
"padding": 0,
|
||||
"alignItems": "stretch",
|
||||
"children": [
|
||||
{ "type": "rect", "id": "h-dim", "width": "fill-container", "height": "fit-content", "text": "[维度]", "fontSize": 15, "textAlign": "center", "verticalAlign": "middle", "borderRadius": 0, "borderWidth": 2 },
|
||||
{ "type": "rect", "id": "h-col-1", "width": "fill-container", "height": "fit-content", "text": "[对象A]", "fontSize": 15, "textAlign": "center", "verticalAlign": "middle", "borderRadius": 8, "borderWidth": 2 },
|
||||
{ "type": "rect", "id": "h-col-2", "width": "fill-container", "height": "fit-content", "text": "[对象B]", "fontSize": 15, "textAlign": "center", "verticalAlign": "middle", "borderRadius": 8, "borderWidth": 2 },
|
||||
{ "type": "rect", "id": "h-col-3", "width": "fill-container", "height": "fit-content", "text": "[对象C]", "fontSize": 15, "textAlign": "center", "verticalAlign": "middle", "borderRadius": 8, "borderWidth": 2 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "data-row-1",
|
||||
"width": "fill-container",
|
||||
"height": "fit-content",
|
||||
"layout": "horizontal",
|
||||
"gap": 8,
|
||||
"padding": 0,
|
||||
"alignItems": "stretch",
|
||||
"children": [
|
||||
{ "type": "rect", "id": "d1-dim", "width": "fill-container", "height": "fit-content", "text": "[维度1]", "fontSize": 14, "textAlign": "center", "verticalAlign": "middle", "borderRadius": 8, "borderWidth": 2 },
|
||||
{ "type": "rect", "id": "d1-c1", "width": "fill-container", "height": "fit-content", "text": "[...]", "fontSize": 14, "textAlign": "center", "verticalAlign": "middle", "borderRadius": 8, "borderWidth": 2 },
|
||||
{ "type": "rect", "id": "d1-c2", "width": "fill-container", "height": "fit-content", "text": "[...]", "fontSize": 14, "textAlign": "center", "verticalAlign": "middle", "borderRadius": 8, "borderWidth": 2 },
|
||||
{ "type": "rect", "id": "d1-c3", "width": "fill-container", "height": "fit-content", "text": "[...]", "fontSize": 14, "textAlign": "center", "verticalAlign": "middle", "borderRadius": 8, "borderWidth": 2 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "data-row-2",
|
||||
"width": "fill-container",
|
||||
"height": "fit-content",
|
||||
"layout": "horizontal",
|
||||
"gap": 8,
|
||||
"padding": 0,
|
||||
"alignItems": "stretch",
|
||||
"children": [
|
||||
{ "type": "rect", "id": "d2-dim", "width": "fill-container", "height": "fit-content", "text": "[维度2]", "fontSize": 14, "textAlign": "center", "verticalAlign": "middle", "borderRadius": 8, "borderWidth": 2 },
|
||||
{ "type": "rect", "id": "d2-c1", "width": "fill-container", "height": "fit-content", "text": "[...]", "fontSize": 14, "textAlign": "center", "verticalAlign": "middle", "borderRadius": 8, "borderWidth": 2 },
|
||||
{ "type": "rect", "id": "d2-c2", "width": "fill-container", "height": "fit-content", "text": "[...]", "fontSize": 14, "textAlign": "center", "verticalAlign": "middle", "borderRadius": 8, "borderWidth": 2 },
|
||||
{ "type": "rect", "id": "d2-c3", "width": "fill-container", "height": "fit-content", "text": "[...]", "fontSize": 14, "textAlign": "center", "verticalAlign": "middle", "borderRadius": 8, "borderWidth": 2 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "data-row-3",
|
||||
"width": "fill-container",
|
||||
"height": "fit-content",
|
||||
"layout": "horizontal",
|
||||
"gap": 8,
|
||||
"padding": 0,
|
||||
"alignItems": "stretch",
|
||||
"children": [
|
||||
{ "type": "rect", "id": "d3-dim", "width": "fill-container", "height": "fit-content", "text": "[维度3]", "fontSize": 14, "textAlign": "center", "verticalAlign": "middle", "borderRadius": 8, "borderWidth": 2 },
|
||||
{ "type": "rect", "id": "d3-c1", "width": "fill-container", "height": "fit-content", "text": "[...]", "fontSize": 14, "textAlign": "center", "verticalAlign": "middle", "borderRadius": 8, "borderWidth": 2 },
|
||||
{ "type": "rect", "id": "d3-c2", "width": "fill-container", "height": "fit-content", "text": "[...]", "fontSize": 14, "textAlign": "center", "verticalAlign": "middle", "borderRadius": 8, "borderWidth": 2 },
|
||||
{ "type": "rect", "id": "d3-c3", "width": "fill-container", "height": "fit-content", "text": "[...]", "fontSize": 14, "textAlign": "center", "verticalAlign": "middle", "borderRadius": 8, "borderWidth": 2 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 陷阱
|
||||
|
||||
- **行间距 8px 太紧**:行间 gap 至少 12,8 会让行与行视觉粘连。
|
||||
- **长文本居中对齐**:超过一行的文本应改为 `textAlign: "left"`,居中多行文本可读性差。
|
||||
- **列数太多导致每列太窄**:对比对象建议 ≤ 5 列(含维度列),超过时合并维度或拆分为多张表。
|
||||
- **列宽不等**:所有数据列必须用 `width: "fill-container"` 等分,不要给某列写固定宽度。
|
||||
- **行高不等**:每行 frame 必须 `alignItems: "stretch"`,否则同行单元格因文字行数不同高矮不齐。
|
||||
- **忘记维度标签列**:第一列放维度名称,标题行(维度列)用与数据列不同的视觉处理。
|
||||
- **单元格用固定 height**:单元格必须 `height: "fit-content"`,固定高度会导致文字截断。
|
||||
238
.claude/skills/lark-whiteboard/scenes/fishbone.md
Normal file
238
.claude/skills/lark-whiteboard/scenes/fishbone.md
Normal file
@ -0,0 +1,238 @@
|
||||
# 鱼骨图(因果图)
|
||||
|
||||
> **必须写脚本生成 JSON。** 鱼骨图的分支角度、原因小骨坐标需要三角函数计算,直接手写 JSON 极易导致节点重叠和连线穿模。请用下方脚本模板。
|
||||
|
||||
## Content 约束
|
||||
|
||||
- 分类 4-6 个
|
||||
- 每个分类的原因 ≤ 4
|
||||
- 总原因 ≤ 20(超过必须合并分类)
|
||||
|
||||
## Layout 选型
|
||||
|
||||
- **脚本生成坐标**(必须):用 .cjs 脚本通过三角函数计算鱼骨坐标,脚本输出 JSON 文件后调用 `npx -y @larksuite/whiteboard-cli@^0.2.13` 渲染
|
||||
|
||||
## Layout 规则
|
||||
|
||||
- 主干水平居中,从左向右延伸
|
||||
- 分类节点按 spineX 从左到右排列,奇数(第 1、3、5...)在上方,偶数(第 2、4...)在下方
|
||||
- 每个分类的原因沿斜线(分支骨)等距排列
|
||||
- 鱼头(中心问题)在右侧,用 ellipse
|
||||
- 主干连线带箭头指向鱼头,分支骨和原因小骨连线 endArrow: "none"
|
||||
- 原因小骨水平延伸到原因框右侧,Y 坐标精准对齐
|
||||
|
||||
## 骨架示例
|
||||
|
||||
**上下交替**:分类标签按 spineX 从左到右排列,奇数(第1、3、5...)在上方,偶数(第2、4...)在下方。
|
||||
|
||||
**视觉同色系**:同一个分支的分类标签、连线及其下的所有原因节点,必须使用同一个色系(如相同的背景色与边框色组合),以保持图形风格统一和逻辑连贯。可以预定义一组颜色数组,按分支轮询使用。
|
||||
|
||||
### 坐标计算脚本模板(必须严格参照此算法生成)
|
||||
|
||||
以下 Node.js 脚本模板包含了完整的动态布局算法,能够自动适配任意数量的分类和原因,生成完美不重叠的鱼骨图:
|
||||
|
||||
```javascript
|
||||
const fs = require('fs');
|
||||
|
||||
const nodes = [];
|
||||
|
||||
// 1. 数据定义 (根据用户需求填充)
|
||||
const categories = [
|
||||
{ id: "c0", text: "前端代码", reasons: ["未压缩资源", "冗余请求", "超大图片未懒加载"] },
|
||||
{ id: "c1", text: "后端服务", reasons: ["数据库慢查询", "缓存失效", "并发量过大"] },
|
||||
{ id: "c2", text: "网络环境", reasons: ["CDN配置错误", "DNS解析缓慢", "带宽限制", "网络抖动"] }
|
||||
];
|
||||
|
||||
// 2. 动态布局计算
|
||||
const catWidth = 120;
|
||||
const catHeight = 40;
|
||||
const reasonWidth = 140; // 调整原因框宽度以适应长文本
|
||||
const reasonHeight = 32;
|
||||
const lineLength = 20; // 原因小骨连线的水平延伸长度
|
||||
const paddingX = 40; // 同侧节点间的水平安全间距
|
||||
|
||||
// 预置的分支色系数组(分支骨分类和具体原因保持同一色系)
|
||||
const branchColors = [
|
||||
{ fill: "#E8F3FF", stroke: "#1664FF" }, // 蓝色系
|
||||
{ fill: "#E6FFED", stroke: "#00B42A" }, // 绿色系
|
||||
{ fill: "#FFF7E8", stroke: "#FF7D00" }, // 橙色系
|
||||
{ fill: "#FFECE8", stroke: "#F5319D" }, // 粉色系
|
||||
{ fill: "#F2E8FF", stroke: "#722ED1" }, // 紫色系
|
||||
{ fill: "#E8FFFF", stroke: "#14C9C9" } // 青色系
|
||||
];
|
||||
|
||||
let maxSpineY_up = 0;
|
||||
let maxSpineY_down = 0;
|
||||
|
||||
// 第一步:计算每个 category 的内部尺寸和相对包围盒
|
||||
categories.forEach((cat, index) => {
|
||||
const isTop = index % 2 === 0;
|
||||
const numReasons = cat.reasons.length;
|
||||
|
||||
// 动态计算分支高度,确保原因小骨不会垂直重叠
|
||||
// 每个原因需要 reasonHeight + 上下间距(约 16)
|
||||
const requiredY = (numReasons + 1) * (reasonHeight + 16);
|
||||
const branchDY = Math.max(160, requiredY);
|
||||
const branchDX = -branchDY * 0.7; // 保持固定的倾斜角度向左延伸
|
||||
|
||||
cat.isTop = isTop;
|
||||
cat.branchDX = branchDX;
|
||||
cat.branchDY = branchDY;
|
||||
|
||||
// 记录最大分支高度,用于计算背景高度和主骨 Y 坐标
|
||||
if (isTop) maxSpineY_up = Math.max(maxSpineY_up, branchDY + catHeight + 40);
|
||||
else maxSpineY_down = Math.max(maxSpineY_down, branchDY + catHeight + 40);
|
||||
|
||||
// 计算该分类的相对包围盒的极值(相对于 spineX 锚点)
|
||||
// 最左侧可能由分类框或原因框决定
|
||||
cat.minX = Math.min(branchDX - catWidth / 2, branchDX - lineLength - reasonWidth);
|
||||
// 最右侧为主骨挂载点 0 或 分类框右侧
|
||||
cat.maxX = Math.max(0, branchDX + catWidth / 2);
|
||||
});
|
||||
|
||||
// 第二步:计算每个 category 在主骨上的绝对 X 坐标 (spineX)
|
||||
let currentSpineX = 100; // 初始偏移
|
||||
for (let i = 0; i < categories.length; i++) {
|
||||
const cat = categories[i];
|
||||
let startX = currentSpineX;
|
||||
|
||||
// 需要和上一个同侧的 category 保持距离,防止水平重叠
|
||||
if (i >= 2) {
|
||||
const prevSameSideCat = categories[i - 2];
|
||||
const requiredX = prevSameSideCat.spineX + prevSameSideCat.maxX - cat.minX + paddingX;
|
||||
startX = Math.max(startX, requiredX);
|
||||
}
|
||||
|
||||
// 确保左侧最长分支不会超出画布左边界
|
||||
if (startX + cat.minX < 50) {
|
||||
startX = 50 - cat.minX;
|
||||
}
|
||||
|
||||
cat.spineX = startX;
|
||||
// 每次略微向前推进,确保异侧节点也能稍微错开
|
||||
currentSpineX = startX + 80;
|
||||
}
|
||||
|
||||
// 第三步:计算全局画布尺寸
|
||||
const lastCat = categories[categories.length - 1];
|
||||
const spineY = maxSpineY_up + 50; // 动态推导主骨 Y 坐标
|
||||
const totalWidth = lastCat.spineX + 350; // 右侧留出鱼头的空间
|
||||
const totalHeight = spineY + maxSpineY_down + 50;
|
||||
|
||||
// 4. 生成节点数据
|
||||
// 背景
|
||||
nodes.push({ type: "rect", x: 0, y: 0, width: totalWidth, height: totalHeight, fillColor: "#FFFFFF", borderWidth: 0 });
|
||||
|
||||
// 鱼头
|
||||
const headWidth = 180;
|
||||
const headHeight = 80;
|
||||
const headX = totalWidth - headWidth - 40;
|
||||
const headY = spineY - headHeight / 2;
|
||||
nodes.push({ type: "ellipse", id: "head", x: headX, y: headY, width: headWidth, height: headHeight, text: "核心问题" });
|
||||
|
||||
// 主骨连线
|
||||
const firstSpineX = categories[0].spineX + categories[0].minX;
|
||||
nodes.push({
|
||||
type: "connector",
|
||||
connector: { from: { x: firstSpineX, y: spineY }, to: "head", toAnchor: "left", lineShape: "straight", endArrow: "arrow" }
|
||||
});
|
||||
|
||||
// 遍历生成分类和原因小骨
|
||||
categories.forEach((cat, index) => {
|
||||
const isTop = cat.isTop;
|
||||
const branchDY = cat.branchDY;
|
||||
const branchDX = cat.branchDX;
|
||||
const color = branchColors[index % branchColors.length];
|
||||
|
||||
// 分类标签
|
||||
const catX = cat.spineX + branchDX - catWidth / 2;
|
||||
const catY = spineY + (isTop ? -branchDY - catHeight : branchDY);
|
||||
|
||||
nodes.push({
|
||||
type: "rect", id: cat.id, x: catX, y: catY, width: catWidth, height: catHeight, text: cat.text,
|
||||
fillColor: color.fill, strokeColor: color.stroke
|
||||
});
|
||||
// 分支骨连线
|
||||
nodes.push({
|
||||
type: "connector",
|
||||
connector: { from: { x: cat.spineX, y: spineY }, to: cat.id, toAnchor: isTop ? "bottom" : "top", lineShape: "straight", endArrow: "none", lineColor: color.stroke }
|
||||
});
|
||||
|
||||
// 原因小骨
|
||||
cat.reasons.forEach((reason, rIndex) => {
|
||||
// 线性插值,均匀分布在分支骨上
|
||||
const t = (rIndex + 1) / (cat.reasons.length + 1);
|
||||
const attachX = cat.spineX + branchDX * t;
|
||||
const attachY = spineY + (isTop ? -branchDY : branchDY) * t;
|
||||
|
||||
// 关键对齐:确保原因盒子完全在连线左侧,并且 Y 坐标中心精准对齐
|
||||
const boxX = attachX - lineLength - reasonWidth;
|
||||
const boxY = attachY - reasonHeight / 2;
|
||||
|
||||
const rId = `${cat.id}-r${rIndex}`;
|
||||
nodes.push({
|
||||
type: "rect", id: rId, x: boxX, y: boxY, width: reasonWidth, height: reasonHeight, text: reason,
|
||||
fillColor: color.fill, strokeColor: color.stroke
|
||||
});
|
||||
// 原因小骨连线
|
||||
nodes.push({
|
||||
type: "connector",
|
||||
connector: { from: { x: attachX, y: attachY }, to: rId, toAnchor: "right", lineShape: "straight", endArrow: "none", lineColor: color.stroke }
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
fs.writeFileSync('diagram.json', JSON.stringify({ version: 2, nodes }, null, 2));
|
||||
```
|
||||
|
||||
## 连线格式与注意点
|
||||
|
||||
所有 connector 都用 `{ "type": "connector", "connector": { ... } }` 格式。
|
||||
**注意:除了主骨外,其他所有连线(分支骨、原因小骨)都必须设置 `"endArrow": "none"`,否则会默认带箭头,导致方向混乱。**
|
||||
|
||||
分支骨:从主骨上的绝对坐标点 → 分类标签节点:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 2,
|
||||
"nodes": [
|
||||
{ "type": "rect", "x": 0, "y": 0, "width": "__totalWidth__", "height": "__totalHeight__" },
|
||||
|
||||
{ "type": "ellipse", "id": "head", "x": "__headX__", "y": "__headY__",
|
||||
"width": 180, "height": 80, "text": "[中心问题]" },
|
||||
|
||||
{ "type": "connector", "connector": {
|
||||
"from": { "x": "__spineStartX__", "y": "__spineY__" },
|
||||
"to": "head", "toAnchor": "left",
|
||||
"lineShape": "straight", "endArrow": "arrow"
|
||||
}},
|
||||
|
||||
{ "type": "rect", "id": "c0", "x": "__catX__", "y": "__catY__",
|
||||
"width": 120, "height": 40, "text": "[分类A]" },
|
||||
{ "type": "connector", "connector": {
|
||||
"from": { "x": "__spineX0__", "y": "__spineY__" },
|
||||
"to": "c0", "toAnchor": "bottom",
|
||||
"lineShape": "straight", "endArrow": "none"
|
||||
}},
|
||||
|
||||
{ "type": "rect", "id": "c0-r0", "x": "__reasonX__", "y": "__reasonY__",
|
||||
"width": 140, "height": 32, "text": "[原因1]" },
|
||||
{ "type": "connector", "connector": {
|
||||
"from": { "x": "__attachX__", "y": "__attachY__" },
|
||||
"to": "c0-r0", "toAnchor": "right",
|
||||
"lineShape": "straight", "endArrow": "none"
|
||||
}}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
上述骨架展示一个分类(上方)+ 一条原因的模式。完整鱼骨图重复此模式,上下交替。每个分类下可有多条原因,均匀插值分布在分支骨上。
|
||||
|
||||
## 陷阱
|
||||
|
||||
- **代码生成**:必须使用带有动态防重叠算法的脚本来计算坐标并输出 JSON。
|
||||
- **分支骨防重叠**:同一侧的相邻分支骨和原因框必须没有任何交叉。
|
||||
- **自适应高度**:原因数量较多时,分支骨自动拉长以容纳所有小骨。
|
||||
- **原因小骨水平**:原因框右侧的附着点必须与连线起点 Y 坐标一致。
|
||||
- **无箭头**:所有分类的分支连线、小骨连线均必须关闭箭头。
|
||||
- **同色系**:同一个分支骨、分类标签节点以及原因小骨节点和连线,必须使用同色系的颜色以保持视觉连贯性。
|
||||
185
.claude/skills/lark-whiteboard/scenes/flowchart.md
Normal file
185
.claude/skills/lark-whiteboard/scenes/flowchart.md
Normal file
@ -0,0 +1,185 @@
|
||||
# 流程图 (Flowchart)
|
||||
|
||||
适用于:各种业务流转图、决策树、审批流、时序控制逻辑、带条件判断的链路、系统架构拓扑等。
|
||||
|
||||
通用字段语义详见 `elements/schema.md`,通用布局原则详见 `elements/layout.md`;本文件只描述流程图场景下的选型边界与范式。
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **流程图必须走 DSL 路径,不再使用 Mermaid!**
|
||||
> 复杂分支、判断、回路、跳级关系优先使用 `layout: "dagre"` 计算拓扑;如果只是规整的单线流水线,且卡片强对齐比自动拓扑更重要,也可以使用 Flex + 顶层 `connector` 组合实现。
|
||||
|
||||
## 美学规范
|
||||
|
||||
- **摒弃简陋节点,推崇全卡片化**:核心业务节点不要只用一个纯文本 `rect`。**应优先采用 Flex 组合卡片**(如:在 `vertical` frame 内上下组合【Emoji 标题项】和【补充说明项】),使得节点信息结构化、层级分明。
|
||||
- **语义化色彩编排**:节点底色严禁随机分配。必须按状态语义映射:常规链路用浅蓝/浅紫、核心风控/检查用预警黄、成功通过用生命绿、失败熔断用危险红。边框颜色可同色系加深,以凸显卡片边缘。
|
||||
- **统一判定逻辑**:条件分支必须使用 `diamond` 菱形节点,并且**严禁漏掉** `layoutOptions.edges` 边定义里的第三个标签参数(必须清晰写明"是/否"、"通过/拒绝")。
|
||||
- **形状多样化**:合理搭配不同形状来表达语义 —— `ellipse` 用于外部实体/起终点、`diamond` 用于判断路由、`rect` 用于业务处理节点、`cylinder` 用于持久化存储。
|
||||
|
||||
## Layout 选型
|
||||
|
||||
| 模式 | 适用条件 | 核心配置 |
|
||||
| ---------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| **主体用 Dagre** | 有判断、分支、回路、回退、跳级关系的标准流程图 | 主体 frame 设定 `layout: "dagre"`,按需配置 `rankdir: "TB"` 或 `rankdir: "LR"`。 |
|
||||
| **局部复合节点** | 流程中的某一步本身是一个小型 UI 组合体 | 外层仍用 `dagre`,复合步骤内部改用 `layout: "vertical"` / `"horizontal"`。此类节点为**不透明节点**,外层连线只能连到外壳。 |
|
||||
| **透明子图** | 需按业务区域分组,且连线穿越区域边界 | 子容器声明 `layout: "dagre"` + `layoutOptions: { isCluster: true }`,成为透明子图。内部节点直接参与外层拓扑运算。 |
|
||||
| **规整流水线** | 基本是单线 A → B → C → D,且卡片对齐要求极高 | 主体可用 Flex 排版,连线改用顶层 `connector`;不要为了"自动"而硬上 Dagre。 |
|
||||
|
||||
## 核心属性
|
||||
|
||||
- **`rankdir`**: `TB`(上下)或 `LR`(左右)。**强烈推荐优先使用 `LR`**,充分利用宽屏横向空间。
|
||||
- **`edges`**: 在根 Dagre 的 `layoutOptions.edges` 中按 `[fromId, toId, "标签"]` 声明。**支持反向连接**实现闭环。所有 edges 统一写在**最外层根 Dagre**,不要写在 cluster 内部。
|
||||
- **`ranksep` 与边文本**: 若边上标注了说明文字,**必须根据字数调大间距**:`ranksep = max(60, 字数 × 16)`。
|
||||
- **自适应尺寸**: dagre 容器**必须**设定 `width: "fit-content"` 和 `height: "fit-content"`。
|
||||
- **`clusterTitle`**: 透明子图可通过 `clusterTitle` 声明悬浮标题(自动吸附左上角、加粗 14px),搭配 `clusterTitleColor` 指定标题颜色。
|
||||
|
||||
## 两种嵌套模式
|
||||
|
||||
### 不透明节点(Opaque Node)
|
||||
Dagre 内的子容器,只要未声明 `isCluster: true`,对外层 Dagre 就是具有确定宽高的原子节点。外层连线无法寻址其内部子节点。适合封装复杂的组合卡片(如带图标、版本号、多行描述的业务模块)。
|
||||
|
||||
### 透明子图(Compound Cluster)
|
||||
子容器同时声明 `layout: "dagre"` 与 `layoutOptions: { isCluster: true }` 时,成为外层 Dagre 的复合子图。其内部子节点直接参与外层拓扑运算,连线可穿越子图边界。适合划分网络区域、功能层级、命名空间等边界容器。推荐搭配 `borderDash: "dashed"` 虚线边框 + 淡色背景。
|
||||
|
||||
## 骨架示例(推荐范本)
|
||||
|
||||
以下是一个混合架构拓扑的完整示例。它同时展示了**透明子图**(Kubernetes Zone,连线可穿透)和**不透明复合节点**(DB 集群、AI 引擎,连线只能连外壳)的标准写法,以及多种形状(ellipse / diamond / rect / cylinder)和语义化配色规范。
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 2,
|
||||
"nodes": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "root",
|
||||
"x": 20, "y": 20,
|
||||
"layout": "dagre",
|
||||
"width": "fit-content", "height": "fit-content",
|
||||
"padding": 60,
|
||||
"fillColor": "#F8FAFC",
|
||||
"borderColor": "#CBD5E1",
|
||||
"borderWidth": 1,
|
||||
"borderRadius": 16,
|
||||
"layoutOptions": {
|
||||
"rankdir": "LR",
|
||||
"nodesep": 60,
|
||||
"ranksep": 120,
|
||||
"edges": [
|
||||
["user", "k8s_ingress", "HTTPS request"],
|
||||
["k8s_ingress", "web_pod", "Route UI"],
|
||||
["k8s_ingress", "api_pod", "Route API"],
|
||||
["web_pod", "api_pod", "Internal REST"],
|
||||
["api_pod", "db_cluster", "SQL Query"],
|
||||
["api_pod", "ai_service", "gRPC Stream"]
|
||||
]
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "ellipse", "id": "user", "text": "Global Users",
|
||||
"width": 110, "height": 60,
|
||||
"fillColor": "#E2E8F0", "borderColor": "#64748B", "borderWidth": 1,
|
||||
"fontSize": 14, "textColor": "#334155"
|
||||
},
|
||||
{
|
||||
"type": "frame", "id": "zone_k8s",
|
||||
"layout": "dagre",
|
||||
"layoutOptions": {
|
||||
"isCluster": true,
|
||||
"clusterTitle": "☸️ Kubernetes Zone (isCluster)",
|
||||
"clusterTitleColor": "#2563EB"
|
||||
},
|
||||
"fillColor": "#EFF6FF", "borderColor": "#60A5FA",
|
||||
"borderWidth": 2, "borderDash": "dashed", "borderRadius": 24,
|
||||
"children": [
|
||||
{
|
||||
"type": "diamond", "id": "k8s_ingress", "text": "Nginx Ingress",
|
||||
"width": 130, "height": 70,
|
||||
"fillColor": "#DBEAFE", "borderColor": "#3B82F6", "borderWidth": 2,
|
||||
"textColor": "#1E40AF"
|
||||
},
|
||||
{
|
||||
"type": "rect", "id": "web_pod", "text": "Next.js SSR Pod",
|
||||
"width": 140, "height": 48,
|
||||
"fillColor": "#BFDBFE", "borderColor": "#2563EB", "borderWidth": 2,
|
||||
"borderRadius": 8, "textColor": "#1E3A8A"
|
||||
},
|
||||
{
|
||||
"type": "rect", "id": "api_pod", "text": "Go Lang API Pod",
|
||||
"width": 140, "height": 48,
|
||||
"fillColor": "#BFDBFE", "borderColor": "#2563EB", "borderWidth": 2,
|
||||
"borderRadius": 8, "textColor": "#1E3A8A"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame", "id": "db_cluster",
|
||||
"layout": "vertical", "gap": 16, "padding": [20, 24],
|
||||
"alignItems": "center",
|
||||
"fillColor": "#F0FDF4", "borderColor": "#22C55E",
|
||||
"borderWidth": 2, "borderRadius": 16,
|
||||
"children": [
|
||||
{
|
||||
"type": "text", "id": "db_title",
|
||||
"text": "🗄️ Highly Available DB (不透明)", "fontSize": 14, "textColor": "#14532D"
|
||||
},
|
||||
{
|
||||
"type": "frame", "id": "db_row", "layout": "horizontal", "gap": 20,
|
||||
"children": [
|
||||
{
|
||||
"type": "cylinder", "id": "db_master", "text": "Master",
|
||||
"width": 80, "height": 50,
|
||||
"fillColor": "#DCFCE7", "borderColor": "#16A34A", "borderWidth": 1,
|
||||
"textColor": "#166534"
|
||||
},
|
||||
{
|
||||
"type": "cylinder", "id": "db_replica", "text": "Replica",
|
||||
"width": 80, "height": 50,
|
||||
"fillColor": "#DCFCE7", "borderColor": "#16A34A", "borderWidth": 1,
|
||||
"textColor": "#166534"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame", "id": "ai_service",
|
||||
"layout": "vertical", "gap": 10, "padding": [16, 20],
|
||||
"alignItems": "center",
|
||||
"fillColor": "#FAF5FF", "borderColor": "#A855F7",
|
||||
"borderWidth": 2, "borderRadius": 12,
|
||||
"children": [
|
||||
{
|
||||
"type": "text", "id": "ai_title",
|
||||
"text": "🧠 Multi-Modal Engine (不透明)", "fontSize": 14, "textColor": "#6B21A8"
|
||||
},
|
||||
{
|
||||
"type": "rect", "id": "ai_version",
|
||||
"text": "v4.2.1-beta", "width": 90, "height": 22,
|
||||
"fillColor": "#E9D5FF", "borderColor": "#C084FC", "borderWidth": 1,
|
||||
"borderRadius": 4, "fontSize": 11, "textColor": "#581C87"
|
||||
},
|
||||
{
|
||||
"type": "text", "id": "ai_desc",
|
||||
"text": "Includes Vector Store\n& Transformer Blocks",
|
||||
"fontSize": 12, "textColor": "#7E22CE", "textAlign": "center"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**范本要点**:
|
||||
- `zone_k8s` 是**透明子图**(`isCluster: true` + `clusterTitle`),外部连线穿越虚线边界直达 `k8s_ingress`、`web_pod`、`api_pod`。
|
||||
- `db_cluster` 和 `ai_service` 是**不透明节点**(`layout: "vertical"`),内部用 Flex 组合了多行结构化信息,对外层 Dagre 是固定宽高的原子。连线只能连到外壳 ID。
|
||||
- 所有 `edges` 统一写在最外层根 Dagre 的 `layoutOptions` 中。
|
||||
- 本范本中用到了 `ellipse`(外部实体)、`diamond`(路由判断)、`rect`(业务节点)、`cylinder`(数据库存储)四种形状。
|
||||
|
||||
## 陷阱与常见报错防范
|
||||
|
||||
- **误用 Mermaid**:只要用户没有带 `mermaid` 具体语法代码,哪怕描述明确是"流程图",也**强制使用 DSL 框架下的 Dagre 模式**。
|
||||
- **重复画线**:`dagre` 里的所有子节点关系通过 `edges` 定义,引擎会自动生成连线。**绝对不要再去外层用 `connector` 节点重复连一次**。
|
||||
- **穿透黑盒**:普通子容器是不透明节点,外部连线无法直接寻址其内部子节点(引擎会自动重定向至外壳)。若需穿透,必须声明 `layout: "dagre"` 与 `layoutOptions: { isCluster: true }`。
|
||||
- **`id` 缺失**:只要是在 `edges` 里出现的标识符,`children` 里一定能找到同名 `id` 的节点对应,拼写必须完全一致。
|
||||
- **宽度灾难**:Dagre 内容器禁止子框使用 `fill-container`,因为 dagre 父容器本身是被内容撑开的。
|
||||
195
.claude/skills/lark-whiteboard/scenes/flywheel.md
Normal file
195
.claude/skills/lark-whiteboard/scenes/flywheel.md
Normal file
@ -0,0 +1,195 @@
|
||||
# 增长飞轮图 (Flywheel)
|
||||
|
||||
> **必须写脚本生成 JSON。** 飞轮图需要极坐标计算阶段标签位置和 SVG 圆环切割,直接手写 JSON 无法正确实现同心圆环结构。请用下方脚本模板。
|
||||
|
||||
## Content 约束
|
||||
|
||||
- 阶段 4-6 个,每阶段短标签(title + 可选 subtitle/desc)
|
||||
- 中心放置飞轮主题标题
|
||||
|
||||
## Layout 选型
|
||||
|
||||
- **脚本生成坐标**(必须):用 .cjs 脚本极坐标计算阶段标签位置、SVG 圆环切割,脚本输出 JSON 文件后调用 `npx -y @larksuite/whiteboard-cli@^0.2.13` 渲染
|
||||
|
||||
## Layout 规则
|
||||
|
||||
- 同心圆遮挡法构建圆环:大圆(底色)+ 小圆(白色遮罩)+ 中心文字
|
||||
- nodes 数组顺序决定 z-index:先大圆 -> 小圆 -> 中心文字 -> SVG 切割 -> 外围卡片
|
||||
- 阶段标签均匀分布在圆环外围,每个标签到圆心距离相等
|
||||
- SVG polyline 切割圆环形成分段 + 箭头方向感
|
||||
- 阶段数多时需动态放大半径、缩小箭头折角、收紧文字容器
|
||||
|
||||
### 同心圆遮挡法详解
|
||||
|
||||
画一个大圆(作为飞轮的底层颜色),然后在它正中心画一个小圆(填充为白色 `#FFFFFF`)。大圆和小圆都设置 `borderWidth: 0`,通过叠加遮挡形成圆环。
|
||||
|
||||
nodes 数组中的图层顺序(必须严格遵守):
|
||||
|
||||
1. **底层大圆** (`type: 'ellipse'`, 填色, `borderWidth: 0`)
|
||||
2. **遮罩小圆** (`type: 'ellipse'`, 白色填色, `borderWidth: 0`)
|
||||
3. **中心文字** — 必须在两个圆之后添加,否则被白色小圆盖住
|
||||
4. **SVG 切割箭头** — 覆盖在圆环上,用白色粗线 polyline 切出分段
|
||||
5. **外围阶段卡片** — 极坐标计算位置
|
||||
|
||||
### SVG 箭头线切割分段
|
||||
|
||||
通过插入一个铺满大圆区域的 `svg` 节点,利用极坐标计算每个分段交界处的坐标,使用 `<polyline>` 画与背景色相同的粗线条(白色、20px+ 宽度)。线条从内圆边缘穿过大圆边缘,并在穿过时产生一定角度的偏转(`da` 参数),在视觉上"切断"圆环并形成箭头方向感。
|
||||
|
||||
### 外围文字环绕布局
|
||||
|
||||
- 利用极坐标 `x = cx + R * cos(θ)` 计算每个分段的中心角度
|
||||
- 在计算出的坐标点放置 `frame` 容器(`layout: 'vertical'`)
|
||||
- 外围文字容器内部的 `text` 节点不能用 `width: 'fill-container'`,必须指定固定 width 配合 `height: 'fit-content'`
|
||||
|
||||
### 动态缩放优化(阶段数 >= 8 时必须)
|
||||
|
||||
当阶段数量较多(8 个、12 个或 16 个以上)时,必须动态调整:
|
||||
|
||||
- **放大画布与圆环半径**:节点越多,需要越长的圆周容纳外围文字。适当调大 `rOut` 和 `rIn`(如 16 阶段时 `rOut` 可设为 400+),同步放大 `cx`/`cy` 避免超出边界
|
||||
- **缩小箭头切割角度**:段数增多时每段夹角变小,保持默认折角会导致缝隙过大。应减小 `da`(如 `da = 4`)
|
||||
- **收紧外围文字容器**:缩窄 `boxWidth`,减小文字字号,确保相邻文本框不互相覆盖
|
||||
|
||||
## 骨架示例
|
||||
|
||||
此场景必须用 .cjs 脚本生成。Agent 使用时只需修改 `stages` 数组和 `centerTitle`/`centerSubtitle`,其余坐标全自动计算。
|
||||
|
||||
```javascript
|
||||
const { writeFileSync } = require('fs');
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// 只需修改这里 -- 填入用户要求的阶段数据和中心标题
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
const centerTitle = '{{CENTER_TITLE}}';
|
||||
const centerSubtitle = '{{CENTER_SUBTITLE}}'; // 可选,不需要就留空字符串
|
||||
|
||||
const stages = [
|
||||
{ title: '{{STAGE_1}}', subtitle: '{{SUB_1}}', desc: '{{DESC_1}}' },
|
||||
{ title: '{{STAGE_2}}', subtitle: '{{SUB_2}}', desc: '{{DESC_2}}' },
|
||||
{ title: '{{STAGE_3}}', subtitle: '{{SUB_3}}', desc: '{{DESC_3}}' },
|
||||
{ title: '{{STAGE_4}}', subtitle: '{{SUB_4}}', desc: '{{DESC_4}}' },
|
||||
];
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// 以下是自动计算逻辑,不需要修改
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
// --- 布局参数 ---
|
||||
const numSegments = stages.length;
|
||||
const cx = 600, cy = 450; // 画布中心
|
||||
const rOut = 240, rIn = 160; // 内外圆半径
|
||||
const textDist = rOut + 40; // 文字离圆心距离
|
||||
const boxWidth = 220; // 外围文字卡片宽度
|
||||
const boxHeight = 80; // 估算高度(用于偏移计算)
|
||||
const da = 8; // 箭头折角
|
||||
|
||||
const nodes = [];
|
||||
|
||||
// --- 图层 1:底层大圆(圆环底色) ---
|
||||
nodes.push({
|
||||
type: 'ellipse',
|
||||
x: cx - rOut, y: cy - rOut,
|
||||
width: rOut * 2, height: rOut * 2,
|
||||
borderWidth: 0,
|
||||
});
|
||||
|
||||
// --- 图层 2:遮罩小圆(白色) ---
|
||||
nodes.push({
|
||||
type: 'ellipse',
|
||||
x: cx - rIn, y: cy - rIn,
|
||||
width: rIn * 2, height: rIn * 2,
|
||||
borderWidth: 0,
|
||||
});
|
||||
|
||||
// --- 图层 3:中心文字(必须在两个圆之后) ---
|
||||
nodes.push({
|
||||
type: 'text',
|
||||
x: cx - rIn, y: cy - (centerSubtitle ? 30 : 20),
|
||||
width: rIn * 2, height: 'fit-content',
|
||||
text: [{ content: centerTitle, bold: true, fontSize: 32 }],
|
||||
textAlign: 'center',
|
||||
});
|
||||
if (centerSubtitle) {
|
||||
nodes.push({
|
||||
type: 'text',
|
||||
x: cx - rIn, y: cy + 20,
|
||||
width: rIn * 2, height: 'fit-content',
|
||||
text: [{ content: centerSubtitle, fontSize: 18 }],
|
||||
textAlign: 'center',
|
||||
});
|
||||
}
|
||||
|
||||
// --- 图层 4:SVG 切割箭头 ---
|
||||
let svg = `<svg viewBox="0 0 ${rOut * 2} ${rOut * 2}" xmlns="http://www.w3.org/2000/svg">`;
|
||||
for (let i = 0; i < numSegments; i++) {
|
||||
const a = -90 + i * (360 / numSegments);
|
||||
const rad = (a * Math.PI) / 180;
|
||||
const radMid = ((a + da) * Math.PI) / 180;
|
||||
const R1 = rIn - 5, R2 = rOut + 5, Rm = (rIn + rOut) / 2;
|
||||
const x1 = rOut + R1 * Math.cos(rad), y1 = rOut + R1 * Math.sin(rad);
|
||||
const x2 = rOut + Rm * Math.cos(radMid), y2 = rOut + Rm * Math.sin(radMid);
|
||||
const x3 = rOut + R2 * Math.cos(rad), y3 = rOut + R2 * Math.sin(rad);
|
||||
svg += `<polyline points="${x1},${y1} ${x2},${y2} ${x3},${y3}" stroke="#FFFFFF" stroke-width="20" fill="none" stroke-linejoin="round" stroke-linecap="round" />`;
|
||||
}
|
||||
svg += `</svg>`;
|
||||
nodes.push({
|
||||
type: 'svg',
|
||||
x: cx - rOut, y: cy - rOut,
|
||||
width: rOut * 2, height: rOut * 2,
|
||||
svg: { code: svg },
|
||||
});
|
||||
|
||||
// --- 图层 5:外围阶段卡片(极坐标计算位置) ---
|
||||
for (let i = 0; i < numSegments; i++) {
|
||||
const stage = stages[i];
|
||||
const a = -90 + (360 / numSegments) / 2 + i * (360 / numSegments);
|
||||
const rad = (a * Math.PI) / 180;
|
||||
const tx = cx + textDist * Math.cos(rad);
|
||||
const ty = cy + textDist * Math.sin(rad);
|
||||
|
||||
// 动态偏移:根据角度将文本框向外推
|
||||
let offsetX = 0, offsetY = 0;
|
||||
if (Math.cos(rad) > 0.1) offsetX = 0;
|
||||
else if (Math.cos(rad) < -0.1) offsetX = -boxWidth;
|
||||
else offsetX = -boxWidth / 2;
|
||||
if (Math.sin(rad) > 0.1) offsetY = 0;
|
||||
else if (Math.sin(rad) < -0.1) offsetY = -boxHeight;
|
||||
else offsetY = -boxHeight / 2;
|
||||
|
||||
const textW = boxWidth - 24; // 卡片 padding 12 * 2
|
||||
nodes.push({
|
||||
type: 'frame',
|
||||
x: tx + offsetX, y: ty + offsetY,
|
||||
width: boxWidth, height: 'fit-content',
|
||||
layout: 'vertical', gap: 8, padding: 12,
|
||||
alignItems: 'start',
|
||||
borderWidth: 2, borderRadius: 8,
|
||||
children: [
|
||||
{ type: 'text', width: textW, height: 'fit-content',
|
||||
text: [{ content: stage.title, bold: true, fontSize: 18 }], textAlign: 'left' },
|
||||
{ type: 'text', width: textW, height: 'fit-content',
|
||||
text: [{ content: stage.subtitle, fontSize: 14 }], textAlign: 'left' },
|
||||
{ type: 'text', width: textW, height: 'fit-content',
|
||||
text: [{ content: stage.desc, fontSize: 12 }], textAlign: 'left' },
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// --- 图表标题 ---
|
||||
nodes.push({
|
||||
type: 'text',
|
||||
x: cx - rOut - 100, y: 30,
|
||||
width: (rOut + 100) * 2, height: 'fit-content',
|
||||
text: [{ content: centerTitle, bold: true, fontSize: 24 }],
|
||||
textAlign: 'center',
|
||||
});
|
||||
|
||||
writeFileSync('diagram.json', JSON.stringify({ version: 2, nodes }, null, 2));
|
||||
```
|
||||
|
||||
## 陷阱
|
||||
|
||||
- **中心文字被 SVG 遮挡**:中心文字节点必须在大圆和小圆之后、SVG 之前添加,确保 z-index 正确
|
||||
- **缺方向指示箭头**:SVG polyline 切割线必须带角度偏转(da 参数),形成顺时针/逆时针箭头感
|
||||
- **标签位置不对称**:外围卡片必须用极坐标公式 `x = cx + R * cos(θ)` 均匀分布,不可手动摆放
|
||||
- **外围文字容器死锁**:`layout: 'vertical'` 的 frame 内部 text 节点不能用 `width: 'fill-container'`,必须指定固定 width
|
||||
101
.claude/skills/lark-whiteboard/scenes/funnel.md
Normal file
101
.claude/skills/lark-whiteboard/scenes/funnel.md
Normal file
@ -0,0 +1,101 @@
|
||||
# 漏斗图 (Funnel)
|
||||
|
||||
## Content 约束
|
||||
|
||||
- 阶段 3-6 个
|
||||
- 每阶段一行标签 + 数值(如 "{{STAGE_NAME}} ({{PERCENTAGE}})")
|
||||
- 文案尽量简短;长文案外置到漏斗旁边,图形内仅保留核心短文案
|
||||
|
||||
## Layout 选型
|
||||
|
||||
绝对定位。用 `trapezoid` / `triangle` 节点从宽到窄排列,height 用 `fit-content`。
|
||||
|
||||
## Layout 规则
|
||||
|
||||
- 外层 frame 使用 `layout: "vertical"` + `alignItems: "center"` 居中对齐
|
||||
- 所有层必须使用脚本计算宽度,以保证**绝对完美的等斜率(直线边缘)**。切勿手写拍脑袋的宽度!
|
||||
- 每层间 gap 0-8px(紧密堆叠视觉效果好),从上到下宽度递减。注意 children 数组第一个元素是最顶层(最宽)
|
||||
- 所有图形节点必须设置 `"vFlip": false`(引擎默认朝上翻转,漏斗需要朝下)
|
||||
- 注意:因为 `vFlip: false` 且是倒金字塔结构,所以 `topWidth` 实际控制的是漏斗各层的**底部较窄边缘**。底层可用 `triangle`(`topWidth: 0`)收窄为尖角,或继续用 `trapezoid` 保持平底。
|
||||
|
||||
> **严格的斜率算法(必须在脚本中实现)**:
|
||||
> 要让漏斗的侧边形成一条完美的直线,**宽度的递减必须与高度和 gap 严格挂钩**。
|
||||
> 1. 设定整体宽度收缩系数 `angleK`(建议值 1.5 到 2.5,表示高度每增加1px,总宽度减少的像素数)。
|
||||
> 2. 因为从上往下变窄,所以公式是减法:`bottomWidth(即 topWidth 属性) = currentWidth - (height * angleK)`
|
||||
> 3. 下一层的顶宽公式(必须考虑 gap 带来的额外内收):`nextLayerWidth = bottomWidth - (gap * angleK)`
|
||||
|
||||
## 脚本构建模板
|
||||
|
||||
此场景必须用 .cjs 脚本生成。
|
||||
|
||||
```javascript
|
||||
const fs = require('fs');
|
||||
|
||||
// 1. 配置基础参数
|
||||
const GAP = 4;
|
||||
const ANGLE_K = 2; // 斜率系数:高度每下降1px,宽度减少2px
|
||||
const LAYER_HEIGHT = 80;
|
||||
|
||||
const data = [
|
||||
{ text: "展现 (100%)", fillColor: "#F0F4FC", textColor: "#1F2329" },
|
||||
{ text: "点击 (50%)", fillColor: "#EAE2FE", textColor: "#1F2329" },
|
||||
{ text: "加购 (20%)", fillColor: "#DFF5E5", textColor: "#1F2329" },
|
||||
{ text: "成交 (5%)", fillColor: "#1F2329", textColor: "#FFFFFF" }
|
||||
];
|
||||
|
||||
// 计算第一层的初始宽度 (保证最底层缩到0或平底)
|
||||
// 倒推公式:startWidth = 最后一层底宽 + 所有高度消耗 + 所有gap消耗
|
||||
const totalHeightLoss = data.length * LAYER_HEIGHT * ANGLE_K;
|
||||
const totalGapLoss = (data.length - 1) * GAP * ANGLE_K;
|
||||
// 设定最底层为一个尖角 (底宽为0)
|
||||
let currentWidth = 0 + totalHeightLoss + totalGapLoss;
|
||||
|
||||
const children = data.map((layer, index) => {
|
||||
// 2. 根据公式计算当前层的底宽 (对应节点的 topWidth 属性)
|
||||
const currentBottomWidth = currentWidth - (LAYER_HEIGHT * ANGLE_K);
|
||||
|
||||
const node = {
|
||||
type: currentBottomWidth <= 0 ? "triangle" : "trapezoid",
|
||||
width: currentWidth,
|
||||
// 注意:漏斗中 topWidth 表示的是下方的窄边!如果 <=0 就用 triangle
|
||||
topWidth: Math.max(0, currentBottomWidth),
|
||||
height: LAYER_HEIGHT,
|
||||
vFlip: false, // 必须为 false
|
||||
text: layer.text,
|
||||
textAlign: "center",
|
||||
fillColor: layer.fillColor,
|
||||
borderColor: layer.fillColor,
|
||||
borderWidth: 2,
|
||||
fontSize: 16,
|
||||
textColor: layer.textColor
|
||||
};
|
||||
|
||||
// 3. 关键:计算下一层的顶宽。必须减去 gap 的向内收缩量!
|
||||
currentWidth = currentBottomWidth - (GAP * ANGLE_K);
|
||||
|
||||
return node;
|
||||
});
|
||||
|
||||
const output = {
|
||||
version: 2,
|
||||
nodes: [
|
||||
{
|
||||
type: "frame",
|
||||
layout: "vertical",
|
||||
alignItems: "center",
|
||||
gap: GAP,
|
||||
padding: 40,
|
||||
children: children
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
fs.writeFileSync('diagram.json', JSON.stringify(output, null, 2));
|
||||
```
|
||||
|
||||
## 陷阱
|
||||
|
||||
- **不要手写随意递减的宽度**:这会导致漏斗侧边变成折线,不直。必须严格使用上述 `angleK` 公式计算。
|
||||
- **忘记计算 gap 带来的收缩**:如果下一层的 `width` 只是简单等于上一层的 `topWidth`,在有 gap 的情况下,衔接处会产生锯齿折角。必须减去 `gap * angleK`。
|
||||
- **vFlip 未设置**:忘记 `"vFlip": false` 会导致梯形朝上翻转,漏斗形状错误
|
||||
- **文字溢出底层**:底层越窄空间越小,短文案用 `\n` 换行,长文案外置到漏斗旁边(外层套 `layout: "horizontal"` 的 frame,漏斗一侧,说明文字另一侧)
|
||||
214
.claude/skills/lark-whiteboard/scenes/line-chart.md
Normal file
214
.claude/skills/lark-whiteboard/scenes/line-chart.md
Normal file
@ -0,0 +1,214 @@
|
||||
# 折线图
|
||||
|
||||
## Content 约束
|
||||
|
||||
- 数据点 ≤ 15
|
||||
- Y 轴必须有单位标注(如 "万元"、"%")
|
||||
- 折线系列 ≤ 3(超过太密看不清)
|
||||
|
||||
## Layout 选型
|
||||
|
||||
- **脚本生成坐标**(推荐):用 .cjs 脚本计算数据点坐标和折线路径,脚本输出 JSON 文件后调用 `npx -y @larksuite/whiteboard-cli@^0.2.13` 渲染
|
||||
|
||||
## Layout 规则
|
||||
|
||||
- 白板坐标系 Y 轴向下为正,图表"底部原点"拥有最大 Y 值,数据点向上分布时 Y 减小
|
||||
- 数据点用小 ellipse 标记(width: 12, height: 12)
|
||||
- 折线用 connector straight 连接相邻数据点,endArrow: "none"
|
||||
- 坐标轴用 connector 直线,末端带箭头(endArrow: "arrow")
|
||||
- 格线用虚线 connector(lineStyle: "dashed",endArrow: "none")
|
||||
- 刻度线短横线 connector(endArrow: "none")
|
||||
- 数值标注放在数据点上方
|
||||
- 类别标签放在 X 轴下方,居中对齐数据点
|
||||
|
||||
## 坐标与尺寸计算指南
|
||||
|
||||
白板坐标系中,**X 轴向右为正,Y 轴向下为正**。图表的"底部原点"拥有最大的 Y 坐标,数据点向上分布时 Y 坐标减小。
|
||||
|
||||
1. **确定图表区域**:
|
||||
- 设定图表区高度 `chartHeight` 和宽度 `chartWidth`
|
||||
- 设定左下角坐标原点 `(originX, originY)`
|
||||
- 示例:originX=80, originY=480, chartWidth=900, chartHeight=400
|
||||
2. **Y 轴范围自适应**:
|
||||
- 找出数据最小值 `dataMin` 和最大值 `dataMax`
|
||||
- yMin 不一定为 0:若数据集中在 80-120,Y 轴从 0 开始会让折线挤在顶部一小段区域
|
||||
- 推荐:yMin = 向下取整到合适刻度(如 dataMin=82 → yMin=80),yMax = 向上取整(如 dataMax=118 → yMax=120)
|
||||
- 当数据波动极小时(如 98-102),适当扩大范围避免折线过于平坦
|
||||
3. **数据点坐标计算**:
|
||||
- X 坐标:在可用宽度内均匀分布。`pointX = originX + (i / (pointCount - 1)) * chartWidth`
|
||||
- Y 坐标:按比例映射到高度。`pointY = originY - ((value - yMin) / (yMax - yMin)) * chartHeight`
|
||||
- ellipse 定位:`ellipseX = pointX - 6, ellipseY = pointY - 6`(圆心对齐数据点)
|
||||
4. **连线逻辑**:
|
||||
- 用 connector straight 将相邻数据点连接
|
||||
- `from` = 点[i] 的 (pointX, pointY),`to` = 点[i+1] 的 (pointX, pointY)
|
||||
- startArrow: "none", endArrow: "none"
|
||||
5. **Y 轴刻度计算**:
|
||||
- 将 yMin 到 yMax 等分为 4-5 个刻度
|
||||
- 每个刻度的 Y 坐标:`gridY = originY - ((tickValue - yMin) / (yMax - yMin)) * chartHeight`
|
||||
|
||||
## 完整 JSON 示例
|
||||
|
||||
以下示例:4 个数据点,数据 [120, 200, 150, 180],yMin=100, yMax=220,originX=80, originY=480, chartWidth=900, chartHeight=400。
|
||||
|
||||
- 刻度:100, 130, 160, 190, 220(每 30 一格)
|
||||
- 点0 (120): pointX=80, pointY=480-((120-100)/120)*400=480-66.7=413
|
||||
- 点1 (200): pointX=80+300=380, pointY=480-((200-100)/120)*400=480-333.3=147
|
||||
- 点2 (150): pointX=80+600=680, pointY=480-((150-100)/120)*400=480-166.7=313
|
||||
- 点3 (180): pointX=80+900=980, pointY=480-((180-100)/120)*400=480-266.7=213
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 2,
|
||||
"nodes": [
|
||||
{ "type": "rect", "x": 0, "y": 0, "width": 1100, "height": 580 },
|
||||
|
||||
{ "type": "text", "x": 80, "y": 10, "width": 900, "height": "fit-content",
|
||||
"text": "季度销售额趋势", "fontSize": 24, "textAlign": "center" },
|
||||
|
||||
{ "type": "text", "x": 10, "y": 40, "width": 60, "height": "fit-content",
|
||||
"text": "万元", "fontSize": 12, "textAlign": "center" },
|
||||
|
||||
{ "type": "connector", "connector": {
|
||||
"from": { "x": 80, "y": 480 }, "to": { "x": 80, "y": 55 },
|
||||
"lineShape": "straight", "lineWidth": 2, "endArrow": "arrow"
|
||||
}},
|
||||
{ "type": "connector", "connector": {
|
||||
"from": { "x": 80, "y": 480 }, "to": { "x": 1000, "y": 480 },
|
||||
"lineShape": "straight", "lineWidth": 2, "endArrow": "arrow"
|
||||
}},
|
||||
|
||||
{ "type": "connector", "connector": {
|
||||
"from": { "x": 70, "y": 480 }, "to": { "x": 80, "y": 480 },
|
||||
"lineShape": "straight", "lineWidth": 1,
|
||||
"startArrow": "none", "endArrow": "none"
|
||||
}},
|
||||
{ "type": "text", "x": 20, "y": 470, "width": 50, "height": 20,
|
||||
"text": "100", "fontSize": 12, "textAlign": "right" },
|
||||
|
||||
{ "type": "connector", "connector": {
|
||||
"from": { "x": 70, "y": 380 }, "to": { "x": 80, "y": 380 },
|
||||
"lineShape": "straight", "lineWidth": 1,
|
||||
"startArrow": "none", "endArrow": "none"
|
||||
}},
|
||||
{ "type": "text", "x": 20, "y": 370, "width": 50, "height": 20,
|
||||
"text": "130", "fontSize": 12, "textAlign": "right" },
|
||||
{ "type": "connector", "connector": {
|
||||
"from": { "x": 80, "y": 380 }, "to": { "x": 980, "y": 380 },
|
||||
"lineShape": "straight", "lineWidth": 1, "lineStyle": "dashed",
|
||||
"startArrow": "none", "endArrow": "none"
|
||||
}},
|
||||
|
||||
{ "type": "connector", "connector": {
|
||||
"from": { "x": 70, "y": 280 }, "to": { "x": 80, "y": 280 },
|
||||
"lineShape": "straight", "lineWidth": 1,
|
||||
"startArrow": "none", "endArrow": "none"
|
||||
}},
|
||||
{ "type": "text", "x": 20, "y": 270, "width": 50, "height": 20,
|
||||
"text": "160", "fontSize": 12, "textAlign": "right" },
|
||||
{ "type": "connector", "connector": {
|
||||
"from": { "x": 80, "y": 280 }, "to": { "x": 980, "y": 280 },
|
||||
"lineShape": "straight", "lineWidth": 1, "lineStyle": "dashed",
|
||||
"startArrow": "none", "endArrow": "none"
|
||||
}},
|
||||
|
||||
{ "type": "connector", "connector": {
|
||||
"from": { "x": 70, "y": 180 }, "to": { "x": 80, "y": 180 },
|
||||
"lineShape": "straight", "lineWidth": 1,
|
||||
"startArrow": "none", "endArrow": "none"
|
||||
}},
|
||||
{ "type": "text", "x": 20, "y": 170, "width": 50, "height": 20,
|
||||
"text": "190", "fontSize": 12, "textAlign": "right" },
|
||||
{ "type": "connector", "connector": {
|
||||
"from": { "x": 80, "y": 180 }, "to": { "x": 980, "y": 180 },
|
||||
"lineShape": "straight", "lineWidth": 1, "lineStyle": "dashed",
|
||||
"startArrow": "none", "endArrow": "none"
|
||||
}},
|
||||
|
||||
{ "type": "connector", "connector": {
|
||||
"from": { "x": 70, "y": 80 }, "to": { "x": 80, "y": 80 },
|
||||
"lineShape": "straight", "lineWidth": 1,
|
||||
"startArrow": "none", "endArrow": "none"
|
||||
}},
|
||||
{ "type": "text", "x": 20, "y": 70, "width": 50, "height": 20,
|
||||
"text": "220", "fontSize": 12, "textAlign": "right" },
|
||||
{ "type": "connector", "connector": {
|
||||
"from": { "x": 80, "y": 80 }, "to": { "x": 980, "y": 80 },
|
||||
"lineShape": "straight", "lineWidth": 1, "lineStyle": "dashed",
|
||||
"startArrow": "none", "endArrow": "none"
|
||||
}},
|
||||
|
||||
{ "type": "connector", "connector": {
|
||||
"from": { "x": 80, "y": 413 }, "to": { "x": 380, "y": 147 },
|
||||
"lineShape": "straight", "lineWidth": 3,
|
||||
"startArrow": "none", "endArrow": "none"
|
||||
}},
|
||||
{ "type": "connector", "connector": {
|
||||
"from": { "x": 380, "y": 147 }, "to": { "x": 680, "y": 313 },
|
||||
"lineShape": "straight", "lineWidth": 3,
|
||||
"startArrow": "none", "endArrow": "none"
|
||||
}},
|
||||
{ "type": "connector", "connector": {
|
||||
"from": { "x": 680, "y": 313 }, "to": { "x": 980, "y": 213 },
|
||||
"lineShape": "straight", "lineWidth": 3,
|
||||
"startArrow": "none", "endArrow": "none"
|
||||
}},
|
||||
|
||||
{ "type": "ellipse", "id": "pt-0", "x": 74, "y": 407,
|
||||
"width": 12, "height": 12 },
|
||||
{ "type": "text", "x": 55, "y": 383,
|
||||
"width": 50, "height": 20,
|
||||
"text": "120", "fontSize": 14, "textAlign": "center" },
|
||||
{ "type": "text", "x": 50, "y": 490,
|
||||
"width": 60, "height": 30,
|
||||
"text": "Q1", "fontSize": 14, "textAlign": "center" },
|
||||
|
||||
{ "type": "ellipse", "id": "pt-1", "x": 374, "y": 141,
|
||||
"width": 12, "height": 12 },
|
||||
{ "type": "text", "x": 355, "y": 117,
|
||||
"width": 50, "height": 20,
|
||||
"text": "200", "fontSize": 14, "textAlign": "center" },
|
||||
{ "type": "text", "x": 350, "y": 490,
|
||||
"width": 60, "height": 30,
|
||||
"text": "Q2", "fontSize": 14, "textAlign": "center" },
|
||||
|
||||
{ "type": "ellipse", "id": "pt-2", "x": 674, "y": 307,
|
||||
"width": 12, "height": 12 },
|
||||
{ "type": "text", "x": 655, "y": 283,
|
||||
"width": 50, "height": 20,
|
||||
"text": "150", "fontSize": 14, "textAlign": "center" },
|
||||
{ "type": "text", "x": 650, "y": 490,
|
||||
"width": 60, "height": 30,
|
||||
"text": "Q3", "fontSize": 14, "textAlign": "center" },
|
||||
|
||||
{ "type": "ellipse", "id": "pt-3", "x": 974, "y": 207,
|
||||
"width": 12, "height": 12 },
|
||||
{ "type": "text", "x": 955, "y": 183,
|
||||
"width": 50, "height": 20,
|
||||
"text": "180", "fontSize": 14, "textAlign": "center" },
|
||||
{ "type": "text", "x": 950, "y": 490,
|
||||
"width": 60, "height": 30,
|
||||
"text": "Q4", "fontSize": 14, "textAlign": "center" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
坐标推导验证:
|
||||
- 点0 (Q1, 120): pointX = 80 + (0/3)*900 = 80, pointY = 480 - ((120-100)/120)*400 = 413
|
||||
- 点1 (Q2, 200): pointX = 80 + (1/3)*900 = 380, pointY = 480 - ((200-100)/120)*400 = 147
|
||||
- 点2 (Q3, 150): pointX = 80 + (2/3)*900 = 680, pointY = 480 - ((150-100)/120)*400 = 313
|
||||
- 点3 (Q4, 180): pointX = 80 + (3/3)*900 = 980, pointY = 480 - ((180-100)/120)*400 = 213
|
||||
- ellipse 定位:ellipseX = pointX - 6, ellipseY = pointY - 6
|
||||
|
||||
## 陷阱
|
||||
|
||||
- Y 轴范围不合理:若数据集中在 80-120,Y 轴从 0 到 120 会让折线挤在顶部一小段区域,应设 yMin 接近数据最小值
|
||||
- 缺 Y 轴单位标注,读者无法理解数值含义
|
||||
- 数据点太密时标注互相遮挡(超过 10 个点考虑隔一个标注一次)
|
||||
- 折线段忘记设 endArrow: "none",默认带箭头
|
||||
- 多系列时折线颜色相近难以区分,应使用对比度高的不同色系
|
||||
|
||||
此场景必须用 .cjs 脚本生成。Agent 使用时只需修改 `data` 数组,其余坐标与折线生成全自动计算。
|
||||
|
||||
```javascript
|
||||
const { writeFileSync } = require('fs');
|
||||
```
|
||||
130
.claude/skills/lark-whiteboard/scenes/mermaid.md
Normal file
130
.claude/skills/lark-whiteboard/scenes/mermaid.md
Normal file
@ -0,0 +1,130 @@
|
||||
# Mermaid 图表路径
|
||||
|
||||
本场景与 DSL 路径互斥。
|
||||
|
||||
| | DSL 路径 | Mermaid 路径 |
|
||||
|---|---|---|
|
||||
| 中间格式 | JSON(WBDocument) | Mermaid 文本(.mmd 文件) |
|
||||
| 布局控制 | 精确控制(x/y 坐标、Flex) | 由 parser-kit 自动布局 |
|
||||
| 视觉定制 | 完全可控(颜色、字号、圆角等) | 有限(Mermaid 语法) |
|
||||
| 参考模块 | elements/ + 对应 scene | 仅本文件 |
|
||||
|
||||
## 适用条件
|
||||
|
||||
满足以下任一条件时使用:
|
||||
- 用户明确要求 "用 Mermaid" 或 "输出 Mermaid"
|
||||
- 用户直接粘贴了 Mermaid 语法文本
|
||||
- 图表类型为思维导图、时序图、类图、饼图(自动路由)
|
||||
|
||||
## 思维导图 (Mindmap)
|
||||
|
||||
```mermaid
|
||||
mindmap
|
||||
root((主题))
|
||||
分支A
|
||||
子项A1
|
||||
子项A2
|
||||
分支B
|
||||
子项B1
|
||||
分支C
|
||||
```
|
||||
|
||||
## 时序图 (Sequence Diagram)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant A as 浏览器
|
||||
participant B as 服务器
|
||||
participant C as 数据库
|
||||
A->>B: 请求数据
|
||||
B->>C: 查询
|
||||
C-->>B: 返回结果
|
||||
B-->>A: 响应数据
|
||||
```
|
||||
|
||||
消息类型:
|
||||
- `->>` 实线箭头(同步请求)
|
||||
- `-->>` 虚线箭头(响应/异步)
|
||||
- `-x` 带 x 箭头(失败)
|
||||
|
||||
## 类图 (Class Diagram)
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Animal {
|
||||
+String name
|
||||
+int age
|
||||
+makeSound()
|
||||
}
|
||||
class Dog {
|
||||
+fetch()
|
||||
}
|
||||
Animal <|-- Dog
|
||||
```
|
||||
|
||||
## 饼图 (Pie Chart)
|
||||
|
||||
```mermaid
|
||||
pie title 分布
|
||||
"类别A" : 40
|
||||
"类别B" : 30
|
||||
"类别C" : 20
|
||||
"类别D" : 10
|
||||
```
|
||||
|
||||
## 流程图 (Flowchart)
|
||||
|
||||
> [!WARNING]
|
||||
> **流程图不推荐使用 Mermaid 路径!**
|
||||
> 带复杂分支、复合节点、高保真卡片样式的流程图应优先走 **DSL 路径**(参见 `scenes/flowchart.md`)。只有用户明确给出 Mermaid 代码,或场景本身就是极简文字流程时,才走此路径。
|
||||
|
||||
适用于:极简的文字节点判断业务流。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A([开始]) --> B{条件判断}
|
||||
B -->|是| C[处理步骤]
|
||||
B -->|否| D[另一步骤]
|
||||
C --> E([结束])
|
||||
D --> E
|
||||
```
|
||||
|
||||
### 约束与规范
|
||||
|
||||
- **节点文字 ≤ 8 字**(超过必须缩写,必要时加图例说明)
|
||||
- 判断节点(菱形)只写条件关键词,不写长描述
|
||||
- 步骤数 ≤ 12(超过需合并步骤或拆分为子流程)
|
||||
- 遵循标准流程图符号:开始/结束用体育场形状或圆形 `A([开始])`,判断用菱形 `B{判断}`,步骤用矩形 `C[步骤]`
|
||||
|
||||
### 语法参考
|
||||
|
||||
方向:`TD`(上到下)、`LR`(左到右)、`BT`(下到上)、`RL`(右到左)
|
||||
|
||||
节点形状:`A[矩形]`、`A(圆角)`、`A{菱形}`、`A((圆形))`、`A([体育场])`、`A[[子程序]]`
|
||||
|
||||
连线:`-->`(实线)、`-.->`(虚线)、`==>`(粗线)、`-->|标签|`(带标签)
|
||||
|
||||
## State Diagram
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Idle
|
||||
Idle --> Processing : 收到请求
|
||||
Processing --> Success : 处理成功
|
||||
Processing --> Failed : 处理失败
|
||||
Success --> [*]
|
||||
Failed --> Idle : 重试
|
||||
```
|
||||
|
||||
## 其他支持的图表类型
|
||||
|
||||
- **甘特图**:`gantt`
|
||||
- **ER 图**:`erDiagram`
|
||||
- **Git 分支图**:`gitGraph`
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 输出纯 Mermaid 文本,不是 JSON,不要混用 DSL
|
||||
- 节点文字含特殊字符时用双引号包裹:`A["包含(括号)的文字"]`
|
||||
- `subgraph` 用于逻辑分组
|
||||
- Mermaid 的流程图样式较基础,也无法在节点内部嵌套复杂排版;复杂流程优先走 DSL(见 `scenes/flowchart.md`),极简文字流程或用户显式给 Mermaid 代码时再使用 Mermaid。
|
||||
139
.claude/skills/lark-whiteboard/scenes/milestone.md
Normal file
139
.claude/skills/lark-whiteboard/scenes/milestone.md
Normal file
@ -0,0 +1,139 @@
|
||||
# 里程碑时间线 (Milestone)
|
||||
|
||||
## Content 约束
|
||||
|
||||
- 节点 4-8 个
|
||||
- 每节点:标题 + 日期 + 可选描述
|
||||
- 时间从左到右递增
|
||||
|
||||
## Layout 选型
|
||||
|
||||
两种方案按需选择:
|
||||
|
||||
1. **横向时间线**:horizontal frame,节点等分
|
||||
2. **交替上下**:绝对定位,节点交替在时间轴上下方(节点多时更紧凑)
|
||||
|
||||
## 结构特征
|
||||
|
||||
- **标题居中**:顶部放置图表标题
|
||||
- **年份/时间轴条**:箭头形色块承载年份,按时间从左到右递增
|
||||
- **里程碑卡片**:下方虚线圆角卡片承载标题与描述
|
||||
- **严格对齐**:年份条与对应卡片等宽,左右对齐
|
||||
- **文字层级**:标题加粗在上,描述文字更小更浅在下,居中对齐
|
||||
|
||||
## Layout 规则
|
||||
|
||||
- 绝对定位为主(`layout: "none"`),节点位置承载时间序列含义
|
||||
- 先确定里程碑数量,计算等距的 x 坐标序列
|
||||
- 时间轴用 connector 贯穿所有节点
|
||||
- 节点与时间轴用短竖线连接
|
||||
- 节点间水平间距一致
|
||||
- 年份条宽度 = 卡片宽度,垂直间距统一
|
||||
- 标题与年份区域保留足够留白
|
||||
|
||||
## 骨架示例
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 2,
|
||||
"nodes": [
|
||||
{
|
||||
"type": "frame",
|
||||
"x": 0, "y": 0,
|
||||
"width": 1200, "height": 360,
|
||||
"layout": "none",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"x": 300, "y": 12,
|
||||
"width": 600, "height": "fit-content",
|
||||
"text": [{ "content": "{{CHART_TITLE}}", "bold": true, "fontSize": 24 }],
|
||||
"textAlign": "center"
|
||||
},
|
||||
|
||||
{
|
||||
"type": "svg",
|
||||
"x": 50, "y": 56,
|
||||
"width": 190, "height": 36,
|
||||
"svg": {
|
||||
"code": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 190 36\"><polygon points=\"0,0 170,0 190,18 170,36 0,36\"/></svg>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"x": 50, "y": 64,
|
||||
"width": 190, "height": "fit-content",
|
||||
"text": "{{DATE_1}}",
|
||||
"textAlign": "center"
|
||||
},
|
||||
{
|
||||
"type": "rect",
|
||||
"x": 50, "y": 132,
|
||||
"width": 190, "height": 120,
|
||||
"borderDash": "dashed",
|
||||
"borderRadius": 8
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"x": 50, "y": 150,
|
||||
"width": 190, "height": "fit-content",
|
||||
"text": [{ "content": "{{MILESTONE_1_TITLE}}", "bold": true, "fontSize": 16 }],
|
||||
"textAlign": "center"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"x": 50, "y": 180,
|
||||
"width": 190, "height": "fit-content",
|
||||
"text": "{{MILESTONE_1_DESC}}",
|
||||
"fontSize": 13,
|
||||
"textAlign": "center"
|
||||
},
|
||||
|
||||
{
|
||||
"type": "svg",
|
||||
"x": 290, "y": 56,
|
||||
"width": 190, "height": 36,
|
||||
"svg": {
|
||||
"code": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 190 36\"><polygon points=\"0,0 170,0 190,18 170,36 0,36\"/></svg>"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"x": 290, "y": 64,
|
||||
"width": 190, "height": "fit-content",
|
||||
"text": "{{DATE_2}}",
|
||||
"textAlign": "center"
|
||||
},
|
||||
{
|
||||
"type": "rect",
|
||||
"x": 290, "y": 132,
|
||||
"width": 190, "height": 120,
|
||||
"borderDash": "dashed",
|
||||
"borderRadius": 8
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"x": 290, "y": 150,
|
||||
"width": 190, "height": "fit-content",
|
||||
"text": [{ "content": "{{MILESTONE_2_TITLE}}", "bold": true, "fontSize": 16 }],
|
||||
"textAlign": "center"
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"x": 290, "y": 180,
|
||||
"width": 190, "height": "fit-content",
|
||||
"text": "{{MILESTONE_2_DESC}}",
|
||||
"fontSize": 13,
|
||||
"textAlign": "center"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 陷阱
|
||||
|
||||
- **节点太多时太拥挤**:超过 6 个节点时考虑交替上下布局或增大画布宽度
|
||||
- **右侧节点与时间轴末端重叠**:最后一个节点的 x + width 不要超出画布边界
|
||||
- **年份条与卡片不对齐**:年份条和卡片的 x、width 必须完全一致
|
||||
173
.claude/skills/lark-whiteboard/scenes/organization.md
Normal file
173
.claude/skills/lark-whiteboard/scenes/organization.md
Normal file
@ -0,0 +1,173 @@
|
||||
# 组织架构图
|
||||
|
||||
适用于:公司组织架构、模块依赖树、分类层级树等树形层级结构的场景。
|
||||
|
||||
## Content 约束
|
||||
|
||||
- 层级 ≤ 4
|
||||
- 每个父节点下 ≤ 5 个子节点
|
||||
- 叶节点有意义(不要只为凑数添加空节点)
|
||||
- 长文本用 `\n` 手动换行(如"研发负责人\n(CTO)")
|
||||
|
||||
## Layout 选型
|
||||
|
||||
| 模式 | 适用条件 | 特征 |
|
||||
|------|---------|------|
|
||||
| **tree(居中展开)** | 有明确从属关系的层级结构 | 根节点居中,子节点横向排列,逐层展开。每个"父+子"用 vertical frame 包裹(子树模块) |
|
||||
| **grid(矩阵式)** | 多部门平级,每部门内部有细分 | 横向等分各部门,每部门内部 vertical 列表 |
|
||||
|
||||
## Layout 规则
|
||||
|
||||
以下规则违反会导致连线错乱或排版崩溃:
|
||||
|
||||
1. **子树包裹模式(关键)**:每个父节点和它的子节点群用一个 `layout: "vertical"` + `alignItems: "center"` 的 frame 包裹。**不要**把所有父节点放一层、所有子节点放另一层。*违反后果:父节点与子节点群中心偏移,正交连线无法合并,分裂成两条平行线。*
|
||||
2. **同层节点建议等高**:同层节点统一 `height`(如 60-70),保证连线横向主轴平直。如果文字长度差异大可用 `fit-content`,但要确保同层文字行数接近。*违反后果:同层节点高低不平,rightAngle 连线横向弯折错乱。*
|
||||
3. **垂直间距 >= 60**:父子间纵向 `gap: 60`。*违反后果:连线引擎没有足够空间折弯与合并,导致连线穿模或提前分叉。*
|
||||
4. **叶子容器偶数宽度**:包含叶子节点的横向 frame,宽度应手动计算(子节点宽度之和 + gap × (n-1)),如 2 个 120px 节点 + 20px gap = `width: 260`。或用 `fill-container` 自动等分。*违反后果:父节点中心与子节点群中心有像素级偏差。*
|
||||
5. **同层兄弟间横向 gap: 20-40**
|
||||
6. 最小字号 14px
|
||||
7. 连线:所有父子连线必须 `lineShape: "rightAngle"`(总线风格),`fromAnchor: "bottom"`, `toAnchor: "top"`。*违反后果:失去组织架构图专属的总线视觉效果。*
|
||||
8. 根 frame 宽度要足够(如 1200-1600),避免叶节点被挤压重叠
|
||||
9. 不同层级在 fontSize、borderWidth、颜色上递进区分(如 Root 深灰 → L1 浅蓝 → L2 浅绿 → L3 浅紫)
|
||||
10. 长文本用 `\n` 主动换行(如 "基础架构部\n(包含云原生)"),确保节点高度足够容纳
|
||||
|
||||
## 骨架示例
|
||||
|
||||
### 树形展开(子树包裹模式)
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 2,
|
||||
"nodes": [
|
||||
{
|
||||
"type": "frame",
|
||||
"width": 1200,
|
||||
"height": "fit-content",
|
||||
"layout": "vertical",
|
||||
"gap": 48,
|
||||
"padding": 40,
|
||||
"alignItems": "center",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "title",
|
||||
"width": "fill-container",
|
||||
"height": "fit-content",
|
||||
"text": "[图表标题]",
|
||||
"fontSize": 24,
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle"
|
||||
},
|
||||
{
|
||||
"type": "rect",
|
||||
"id": "root-node",
|
||||
"width": 240,
|
||||
"height": "fit-content",
|
||||
"borderWidth": 3,
|
||||
"borderRadius": 8,
|
||||
"text": "[根节点名]",
|
||||
"fontSize": 18,
|
||||
"padding": 12
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"width": "fill-container",
|
||||
"height": "fit-content",
|
||||
"layout": "horizontal",
|
||||
"gap": 40,
|
||||
"padding": 0,
|
||||
"alignItems": "stretch",
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"width": "fill-container",
|
||||
"height": "fit-content",
|
||||
"layout": "vertical",
|
||||
"gap": 48,
|
||||
"padding": 0,
|
||||
"alignItems": "center",
|
||||
"children": [
|
||||
{
|
||||
"type": "rect",
|
||||
"id": "child-a",
|
||||
"width": 200,
|
||||
"height": "fit-content",
|
||||
"borderWidth": 2,
|
||||
"borderRadius": 8,
|
||||
"text": "[子节点名]",
|
||||
"fontSize": 16,
|
||||
"padding": 10
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"width": "fill-container",
|
||||
"height": "fit-content",
|
||||
"layout": "horizontal",
|
||||
"gap": 40,
|
||||
"padding": 0,
|
||||
"alignItems": "stretch",
|
||||
"children": [
|
||||
{ "type": "rect", "id": "leaf-a1", "width": "fill-container", "height": "fit-content", "borderWidth": 1, "borderRadius": 8, "text": "[叶节点名]", "fontSize": 14, "padding": 8 },
|
||||
{ "type": "rect", "id": "leaf-a2", "width": "fill-container", "height": "fit-content", "borderWidth": 1, "borderRadius": 8, "text": "[叶节点名]", "fontSize": 14, "padding": 8 }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"width": "fill-container",
|
||||
"height": "fit-content",
|
||||
"layout": "vertical",
|
||||
"gap": 48,
|
||||
"padding": 0,
|
||||
"alignItems": "center",
|
||||
"children": [
|
||||
{
|
||||
"type": "rect",
|
||||
"id": "child-b",
|
||||
"width": 200,
|
||||
"height": "fit-content",
|
||||
"borderWidth": 2,
|
||||
"borderRadius": 8,
|
||||
"text": "[子节点名]",
|
||||
"fontSize": 16,
|
||||
"padding": 10
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"width": "fill-container",
|
||||
"height": "fit-content",
|
||||
"layout": "horizontal",
|
||||
"gap": 40,
|
||||
"padding": 0,
|
||||
"alignItems": "stretch",
|
||||
"children": [
|
||||
{ "type": "rect", "id": "leaf-b1", "width": "fill-container", "height": "fit-content", "borderWidth": 1, "borderRadius": 8, "text": "[叶节点名]", "fontSize": 14, "padding": 8 },
|
||||
{ "type": "rect", "id": "leaf-b2", "width": "fill-container", "height": "fit-content", "borderWidth": 1, "borderRadius": 8, "text": "[叶节点名]", "fontSize": 14, "padding": 8 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{ "type": "connector", "connector": { "from": "root-node", "to": "child-a", "fromAnchor": "bottom", "toAnchor": "top", "lineShape": "rightAngle", "lineWidth": 2 } },
|
||||
{ "type": "connector", "connector": { "from": "root-node", "to": "child-b", "fromAnchor": "bottom", "toAnchor": "top", "lineShape": "rightAngle", "lineWidth": 2 } },
|
||||
{ "type": "connector", "connector": { "from": "child-a", "to": "leaf-a1", "fromAnchor": "bottom", "toAnchor": "top", "lineShape": "rightAngle", "lineWidth": 2 } },
|
||||
{ "type": "connector", "connector": { "from": "child-a", "to": "leaf-a2", "fromAnchor": "bottom", "toAnchor": "top", "lineShape": "rightAngle", "lineWidth": 2 } },
|
||||
{ "type": "connector", "connector": { "from": "child-b", "to": "leaf-b1", "fromAnchor": "bottom", "toAnchor": "top", "lineShape": "rightAngle", "lineWidth": 2 } },
|
||||
{ "type": "connector", "connector": { "from": "child-b", "to": "leaf-b2", "fromAnchor": "bottom", "toAnchor": "top", "lineShape": "rightAngle", "lineWidth": 2 } }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 陷阱
|
||||
|
||||
- **分离父子层级(致命错误)**:不要把所有同级父节点放一个 horizontal frame、所有子节点放另一个。必须用 `alignItems: "center"` 的 vertical frame 把每个父节点和它的子节点包裹在一起。
|
||||
- **同层节点高低不平**:同层节点高度应一致(或文字行数接近),否则 rightAngle 连线横向弯折错乱。
|
||||
- **垂直间距不足**:父子间 gap 必须 >= 60。不够时连线引擎无法折弯合并。也不要用 80,3-4 层会纵向拉伸过度。
|
||||
- **做成线性链而非树形展开**:每个父节点的子节点必须横向展开,不要做单链。
|
||||
- **连线混用 straight**:所有父子连线必须 `lineShape: "rightAngle"`,`fromAnchor: "bottom"`,`toAnchor: "top"`。
|
||||
- **叶节点字号 12px 看不清**:最小字号 14px。
|
||||
- **所有节点同一大小和样式**:不同层级必须在 fontSize、borderWidth、颜色上有区分(根>子>叶)。
|
||||
126
.claude/skills/lark-whiteboard/scenes/photo-showcase.md
Normal file
126
.claude/skills/lark-whiteboard/scenes/photo-showcase.md
Normal file
@ -0,0 +1,126 @@
|
||||
# 图片展示 (Photo Showcase)
|
||||
|
||||
适用于:用户**显式要求使用图片/配图/插图**的场景(如"画一个带配图的旅行路线"、"做一个有图片的产品展示")。
|
||||
|
||||
> **注意**:仅当用户明确说了「图片/配图/插图/照片」等词时才进入本场景。单纯说"旅行路线图"、"产品展示"等不触发。
|
||||
|
||||
> **前置条件**:进入本场景前,必须已完成 [`elements/image.md`](../elements/image.md) 的 Step 0(图片准备),拿到所有 media token。
|
||||
|
||||
## Content 约束
|
||||
|
||||
- 图片 3-6 张,每张配标题(必需)+ 简短描述(可选,15字内)
|
||||
- **每张图必须是不同的真实图片**(不同 media token),下载时用不同关键词/URL
|
||||
- 下载后用 `ls -l` 比较文件大小确保每张图不重复
|
||||
- 文字仅作辅助说明,图片是信息主体
|
||||
|
||||
## Layout 选型
|
||||
|
||||
| 模式 | 适用条件 | 特征 |
|
||||
|------|---------|------|
|
||||
| **卡片网格(默认)** | 多图平级展示(产品墙、团队介绍、美食推荐) | horizontal frame 内放等尺寸图文卡片 |
|
||||
| **路线时间线** | 有先后顺序(旅行路线、团建路线、项目演进) | 图文卡片 + connector 串联 |
|
||||
| **中心辐射** | 有一个核心主题 + 周围子项 | 中心标题 + 周围图文卡片 |
|
||||
|
||||
## Layout 规则
|
||||
|
||||
- **图文卡片结构**:vertical frame(图上文下),image 宽度 = 卡片宽度,height 按 3:2 比例
|
||||
- **卡片统一尺寸**:所有卡片宽高一致(推荐 240×280 或 200×250)
|
||||
- **图片统一尺寸**:所有 image 节点用相同 width/height(推荐 240×160 或 200×133)
|
||||
- **卡片间距**:gap: 24(比纯文字图表间距更大,让图片呼吸)
|
||||
- **卡片样式**:白色底 + 圆角 12 + 细边框,image 无圆角(紧贴卡片顶部)
|
||||
- **有序路线时**:卡片间用 connector 连接,connector 放顶层 nodes 数组
|
||||
|
||||
## 骨架示例
|
||||
|
||||
### 卡片网格(产品展示/团队介绍/美食推荐)
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 2,
|
||||
"nodes": [
|
||||
{
|
||||
"type": "frame", "id": "grid", "layout": "vertical", "gap": 24, "padding": 32,
|
||||
"width": 840, "height": "fit-content",
|
||||
"children": [
|
||||
{ "type": "text", "id": "title", "width": 776, "height": 36,
|
||||
"text": "图表标题", "fontSize": 24, "textAlign": "center" },
|
||||
{
|
||||
"type": "frame", "id": "row", "layout": "horizontal", "gap": 24, "padding": 0,
|
||||
"width": "fit-content", "height": "fit-content",
|
||||
"children": [
|
||||
{
|
||||
"type": "frame", "id": "card-1", "layout": "vertical", "gap": 8, "padding": [0, 0, 12, 0],
|
||||
"width": 240, "height": "fit-content",
|
||||
"fillColor": "#FFFFFF", "borderWidth": 1, "borderColor": "#E0E0E0", "borderRadius": 12,
|
||||
"children": [
|
||||
{ "type": "image", "id": "img-1", "width": 240, "height": 160, "image": { "src": "<token_1>" } },
|
||||
{ "type": "text", "id": "t-1", "text": "标题", "fontSize": 14, "width": 216, "height": 20 },
|
||||
{ "type": "text", "id": "d-1", "text": "简短描述", "fontSize": 11, "textColor": "#666666", "width": 216, "height": 16 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
每张图文卡片结构相同,复制并替换 `<token_N>`、标题和描述即可。3 张卡片一行,超过 3 张换行(嵌套第二个 horizontal frame)。
|
||||
|
||||
### 路线时间线(旅行路线/团建路线)
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 2,
|
||||
"nodes": [
|
||||
{
|
||||
"type": "frame", "id": "route", "layout": "vertical", "gap": 24, "padding": 32,
|
||||
"width": 1100, "height": "fit-content",
|
||||
"children": [
|
||||
{ "type": "text", "id": "title", "width": 1036, "height": 36,
|
||||
"text": "路线标题", "fontSize": 24, "textAlign": "center" },
|
||||
{
|
||||
"type": "frame", "id": "stops", "layout": "horizontal", "gap": 32, "padding": 0,
|
||||
"width": "fit-content", "height": "fit-content",
|
||||
"children": [
|
||||
{
|
||||
"type": "frame", "id": "stop-1", "layout": "vertical", "gap": 8, "padding": [0, 0, 12, 0],
|
||||
"width": 240, "height": "fit-content",
|
||||
"fillColor": "#FFFFFF", "borderWidth": 1, "borderColor": "#E0E0E0", "borderRadius": 12,
|
||||
"children": [
|
||||
{ "type": "image", "id": "img-1", "width": 240, "height": 160, "image": { "src": "<token_1>" } },
|
||||
{ "type": "text", "id": "t-1", "text": "第1站:地点名", "fontSize": 14, "width": 216, "height": 20 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame", "id": "stop-2", "layout": "vertical", "gap": 8, "padding": [0, 0, 12, 0],
|
||||
"width": 240, "height": "fit-content",
|
||||
"fillColor": "#FFFFFF", "borderWidth": 1, "borderColor": "#E0E0E0", "borderRadius": 12,
|
||||
"children": [
|
||||
{ "type": "image", "id": "img-2", "width": 240, "height": 160, "image": { "src": "<token_2>" } },
|
||||
{ "type": "text", "id": "t-2", "text": "第2站:地点名", "fontSize": 14, "width": 216, "height": 20 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{ "type": "connector", "id": "c1", "connector": { "from": "stop-1", "to": "stop-2", "fromAnchor": "right", "toAnchor": "left" } }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
注意:connector 必须放在**顶层 nodes 数组**,不能嵌套在 frame.children 内。connector 的属性须包裹在 `connector` 字段中。
|
||||
|
||||
## 图片准备检查清单
|
||||
|
||||
生成 DSL 前确认:
|
||||
|
||||
- [ ] 所有 image 节点的 `image.src` 都是通过 `docs +media-upload --parent-type whiteboard` 上传的 media token(非 URL、非 Drive file token)
|
||||
- [ ] 所有图片已上传到目标画板(`--parent-node` 设为目标画板 token)
|
||||
- [ ] 每个 media token 不同(对应不同的真实图片)
|
||||
- [ ] 所有图片尺寸一致(同一画板内统一 width×height)
|
||||
- [ ] 图片宽高比合理(推荐 3:2,即 240×160)
|
||||
- [ ] 渲染 PNG 后查看图片内容,确认每张图片与主题相关
|
||||
- [ ] 未使用随机占位图服务(关键词参数不影响返回内容的图库)
|
||||
99
.claude/skills/lark-whiteboard/scenes/pyramid.md
Normal file
99
.claude/skills/lark-whiteboard/scenes/pyramid.md
Normal file
@ -0,0 +1,99 @@
|
||||
# 金字塔图 (Pyramid)
|
||||
|
||||
## Content 约束
|
||||
|
||||
- 层级 3-6 个,从底到顶宽度递减
|
||||
- 每层一个短标签(如关键词或短语)
|
||||
- 长文案外置到金字塔旁边,图形内仅保留核心短文案
|
||||
|
||||
## Layout 选型
|
||||
|
||||
vertical frame + 每层宽度递减。gap 4px 保持紧密。
|
||||
|
||||
## Layout 规则
|
||||
|
||||
- 外层 frame 使用 `layout: "vertical"` + `alignItems: "center"`
|
||||
- 所有层必须使用脚本计算宽度,以保证**绝对完美的等斜率(直线边缘)**。切勿手写拍脑袋的宽度!
|
||||
- children 数组中第一个元素是顶层(最窄),最后一个是底层(最宽)。
|
||||
- 顶层通常用 `triangle`(`topWidth: 0`),中间和底层用 `trapezoid`。
|
||||
- gap 通常设为 4px 保持紧密的金字塔感。
|
||||
|
||||
> **严格的斜率算法(必须在脚本中实现)**:
|
||||
> 要让金字塔的侧边形成一条完美的直线,**宽度的增量必须与高度和 gap 严格挂钩**。
|
||||
> 1. 设定整体宽度扩张系数 `angleK`(建议值 1.5 到 2.5,表示高度每增加1px,总宽度增加的像素数)。
|
||||
> 2. 当前层的底宽公式:`width = topWidth + (height * angleK)`
|
||||
> 3. 下一层的顶宽公式(必须考虑 gap 带来的额外外扩):`nextTopWidth = width + (gap * angleK)`
|
||||
|
||||
## 脚本构建模板
|
||||
|
||||
必须使用 `node` 运行脚本生成 JSON。
|
||||
|
||||
```javascript
|
||||
const fs = require('fs');
|
||||
|
||||
// 1. 配置基础参数
|
||||
const GAP = 4;
|
||||
const ANGLE_K = 2; // 斜率系数:高度每增加1px,宽度增加2px
|
||||
const LAYER_HEIGHT = 80;
|
||||
|
||||
const data = [
|
||||
{ text: "顶层核心", fillColor: "#1F2329", textColor: "#FFFFFF" },
|
||||
{ text: "中间层 B", fillColor: "#DFF5E5", textColor: "#1F2329" },
|
||||
{ text: "中间层 A", fillColor: "#EAE2FE", textColor: "#1F2329" },
|
||||
{ text: "最底层基础", fillColor: "#F0F4FC", textColor: "#1F2329" }
|
||||
];
|
||||
|
||||
let currentTopWidth = 0; // 顶层如果是尖角,初始为 0
|
||||
const children = data.map((layer, index) => {
|
||||
// 2. 根据公式计算当前层的底宽
|
||||
const currentBottomWidth = currentTopWidth + (LAYER_HEIGHT * ANGLE_K);
|
||||
|
||||
const node = {
|
||||
type: currentTopWidth === 0 ? "triangle" : "trapezoid",
|
||||
width: currentBottomWidth,
|
||||
topWidth: currentTopWidth,
|
||||
height: LAYER_HEIGHT,
|
||||
text: layer.text,
|
||||
textAlign: "center",
|
||||
fillColor: layer.fillColor,
|
||||
borderColor: layer.fillColor,
|
||||
borderWidth: 2,
|
||||
fontSize: 16,
|
||||
textColor: layer.textColor
|
||||
};
|
||||
|
||||
// 3. 关键:计算下一层的顶宽。必须把 gap 的延伸也算进去!
|
||||
currentTopWidth = currentBottomWidth + (GAP * ANGLE_K);
|
||||
|
||||
return node;
|
||||
});
|
||||
|
||||
const output = {
|
||||
version: 2,
|
||||
nodes: [
|
||||
{
|
||||
type: "frame",
|
||||
layout: "vertical",
|
||||
alignItems: "center",
|
||||
gap: GAP,
|
||||
padding: 40,
|
||||
children: children
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
fs.writeFileSync('diagram.json', JSON.stringify(output, null, 2));
|
||||
```
|
||||
|
||||
## 陷阱
|
||||
|
||||
- **不要手写随意递增的宽度**:这会导致金字塔侧边变成折线,不直。必须严格使用上述 `angleK` 公式计算。
|
||||
- **忘记计算 gap 带来的扩展**:如果下一层的 `topWidth` 只是简单等于上一层的 `width`,在有 gap 的情况下,衔接处会产生锯齿折角。必须加上 `gap * angleK`。
|
||||
- **从上到下排列错误**:children 数组第一个是顶层(最窄),最后一个是底层(最宽),宽度依次递增。
|
||||
- **文字溢出顶层三角形**:顶层三角形内部可用空间极小。短文案用 `\n` 手动换行;长文案外置到金字塔旁边(外层套 horizontal frame,金字塔左侧,说明文字右侧)
|
||||
- **倒金字塔误用**:如果用户要求"倒金字塔"、"漏斗图"或"自上而下递减的结构",**不要**使用本文件,切换到 `scenes/funnel.md`
|
||||
|
||||
## 扩展
|
||||
|
||||
- **辅助说明**:需要在旁边添加文字说明时,在最外层套一个 `layout: "horizontal"` 的 frame,金字塔放左侧,说明文字(vertical 排列的 text 节点)放右侧
|
||||
- **配色**:各层颜色应从色板中选取不同颜色以示区分(如蓝→紫→绿→黄递进)
|
||||
371
.claude/skills/lark-whiteboard/scenes/swimlane.md
Normal file
371
.claude/skills/lark-whiteboard/scenes/swimlane.md
Normal file
@ -0,0 +1,371 @@
|
||||
# 泳道图(Swimlane)
|
||||
|
||||
适用于:跨角色/跨系统的端到端流程(用户/网关/服务/存储/回调)、多泳道协作流程、系统交互链路图。
|
||||
|
||||
支持两种方向:
|
||||
- **水平泳道**:泳道为横向条带(自上而下排列),流程从左到右推进
|
||||
- **垂直泳道**:泳道为纵向列(自左向右排列),流程从上到下推进
|
||||
|
||||
## Content 约束
|
||||
|
||||
- 泳道数(lanes)建议 3-7,超过 7 会显著降低可读性;如必须更多泳道,优先合并同类或拆成两张图
|
||||
- 阶段数(stages)建议 4-8;超过 8 优先合并相邻阶段或改成“代表性阶段”
|
||||
- 每个阶段在每条泳道中最多放 1 个“主步骤卡片”;如同一阶段需要多个步骤,放在同一格内做纵向堆叠(2-3 个为上限)
|
||||
- 节点文本 1-2 行为主;长文本用 `\n` 手动换行,避免单行超长导致卡片过宽
|
||||
- 仅画必要连线:泳道图的结构已经表达了“属于哪个角色/系统 + 发生顺序”,连线只用于表达跨泳道交互、关键因果关系或异步事件流
|
||||
|
||||
## Layout 选型
|
||||
|
||||
| 模式 | 适用条件 | 特征 |
|
||||
|------|---------|------|
|
||||
| **水平泳道** | 默认推荐;流程天然左→右推进 | lanes=行,stages=列;跨泳道同一阶段严格 x 对齐 |
|
||||
| **垂直泳道** | 用户明确要求竖版、或画布更适合纵向滚动阅读 | lanes=列,stages=行;跨泳道同一阶段严格 y 对齐 |
|
||||
|
||||
## Layout 规则
|
||||
|
||||
### 通用规则(两种方向都适用)
|
||||
|
||||
1. **网格对齐是第一优先级**:跨泳道同一阶段必须严格对齐(水平对齐 x;垂直对齐 y)。对齐通过“共享阶段标尺(stage ruler / stage slots)”实现,不靠肉眼估算,也不靠逐节点随意手写坐标
|
||||
2. **只生成真实节点**:为保证跨泳道阶段严格对齐,所有阶段统一保留透明的 **stage cell**;仅在真实阶段的 cell 内生成卡片节点,并按阶段索引映射到对应槽位
|
||||
3. **泳道底色**:为了增强层级感同时保持界面整洁,**强烈建议所有泳道容器统一使用极浅灰色背景**(如 `fillColor: "#F8F9FA"` 或 `"#FCFCFC"`)。边框使用浅灰色细虚线(`borderDash: "dashed"`, `borderWidth: 1`, `borderColor: "#DEE0E3"`)以明确边界。
|
||||
4. **步骤卡片**:使用 `rect`。为建立清晰的视觉层级,卡片**必须填充浅色背景**(参考 `elements/style.md` 中的浅色板,如极浅的主题色),边框使用对应的主题主色(`borderWidth: 1-2`),文字使用深色(如 `#1F2329`)以确保可读性。统一圆角;宽高以可读为先,避免过窄导致换行过多
|
||||
5. **间距**:只要存在 connector 连线,卡片之间的主轴间距必须满足 `gap >= 40`
|
||||
|
||||
### 子节点对齐
|
||||
|
||||
- **同一阶段必须严格对齐**:所有泳道复用同一套 stage slots;不允许靠卡片自身宽度或肉眼估算来对齐
|
||||
- **卡片宽度一致**:同一泳道中的步骤卡片应保持统一宽度;推荐使用统一固定宽度,或严格复用同一槽位宽度
|
||||
- **统一使用 stack 容器**:有内容的阶段统一使用 `layout: "vertical"` 的 stack frame(纵向堆叠 1-3 张卡片);空阶段不生成 stack/卡片,但保留透明 cell 保证对齐
|
||||
- **垂直居中但不影响对齐**:stage cell 默认 `alignItems: "stretch"`,可用 `justifyContent: "center"` 让卡片在 cell 内居中,以确保左右边界严格对齐
|
||||
- **不靠底色区分行/列**:阶段网格默认不需要背景色;如需“轻微”的行/列边界提示,优先给 stage cell 加 1px 细边框(`fillColor: "transparent"` 仍保持视觉透明)
|
||||
|
||||
### Flex 栅格模式(默认)
|
||||
|
||||
- lane body 使用 Flex 布局:水平泳道用 `layout: "horizontal"`,垂直泳道用 `layout: "vertical"`
|
||||
- 为每个阶段生成一个 **stage cell**(占位单元格);空阶段的 cell 透明但保留;cell 内用 `layout: "vertical"` 的 stack 承载 1-3 张卡片
|
||||
- 统一参数:`slotWidth: 180-220`(水平泳道 cell 宽度)、`slotHeight: 64-104`(垂直泳道 cell 高度建议档)、`gap: 40-56`(有连线时必须 ≥40)、`stackGap: 8`、`lanePadding: 16`
|
||||
- 对齐规则:所有泳道复用同一组 `slotWidth/slotHeight/gap`;同一阶段在各泳道上使用相同的 cell 索引保证严格对齐
|
||||
- 尺寸语义:lane body `width/height` 用 `"fit-content"`(Yoga 自适应);卡片 `height: "fit-content"`;Flex 容器内不写子节点 `x/y`
|
||||
- 内容密度:卡片文字 1-2 行;同阶段堆叠上限 2-3;超过上限优先拆分到相邻阶段或缩短文本
|
||||
|
||||
### 跨泳道间距(lanesGap)
|
||||
|
||||
- 根容器承载所有泳道:水平泳道用 `layout: "vertical"`,垂直泳道用 `layout: "horizontal"`
|
||||
- 缩减跨泳道主轴间距 `lanesGap`(建议 `16-24`),以保持整体图表的紧凑性。避免 `lanesGap` 设置为 `0` 导致边框重叠变粗,也避免间距过大导致视觉涣散。
|
||||
- 每条泳道作为根容器的子 frame,内部再使用上述 Flex 栅格的 stage cell 布局
|
||||
- `lanesGap` 与 `lanePadding/stackGap` 独立;lane 内容增减不应影响跨泳道间距
|
||||
- 4px 基线对齐:`lanesGap`、`lanePadding`、cell 尺寸建议按 4 的倍数对齐
|
||||
|
||||
### 水平泳道(lanes=行,stages=列)
|
||||
|
||||
- 根容器:`layout: "vertical"`,`gap: lanesGap` 固定;`alignItems: "stretch"`,标题在最上方
|
||||
- 每条泳道:一个可见 frame(分组容器),内部用 `layout: "horizontal"` 分成两块:
|
||||
- 左侧 lane label:固定宽度 text(如 100-140),垂直居中;左对齐(`textAlign: "left"`);title 需要比步骤卡片更醒目,优先通过 `fontSize: 18-20` + `fontWeight: "bold"` + 与泳道边框一致的 `textColor` 实现
|
||||
- 右侧 lane body:`layout: "horizontal"`,包含完整的阶段 **stage cell** 数组;cell 宽度固定为 `slotWidth`,相邻 cell 间 `gap` 统一;空阶段 cell 透明但保留
|
||||
- 步骤卡片:推荐统一卡片宽度(如 160-220),并在所有泳道复用同一组 `slotWidth / gap`,保证跨泳道阶段严格 x 对齐
|
||||
|
||||
### 垂直泳道(lanes=列,stages=行)
|
||||
|
||||
- 根容器:`layout: "horizontal"`,`gap: lanesGap` 固定;`alignItems: "stretch"`,标题在最上方
|
||||
- 每条泳道:一个可见 frame(分组容器),内部 `layout: "vertical"`:
|
||||
- 顶部 lane label:必须放在单独的 `lane label frame` 中,label frame 使用 `width: "fill-container"`、`alignItems: "center"`、`justifyContent: "center"`,并通过 `paddingTop` 留出与泳道上边的 gap(推荐 `12-16`,按 4px 基线取值,如 `padding: [12, 8, 8, 8]`);内部 text 使用 `width: "fill-container"` + `textAlign: "center"`,确保 title 在整条泳道顶部**水平居中**
|
||||
- lane body:`layout: "vertical"`,包含完整的阶段 **stage cell** 数组;cell 高度固定为 `slotHeight`,相邻 cell 间 `gap` 统一;空阶段 cell 透明但保留
|
||||
- 内容居中对齐:stage cell 建议 `alignItems: "center"` + `justifyContent: "center"`,让卡片在每个 cell 内水平/垂直居中;卡片宽度不超过 `slotWidth`(或固定宽度),避免被 `"fill-container"` 拉伸导致“看起来不居中”
|
||||
- 步骤卡片:推荐统一卡片高度或统一 `slotHeight / gap`,保证跨泳道阶段严格 y 对齐
|
||||
- 泳道外层容器必须显式写 `fillColor: "#F8F9FA"`(极浅灰)、`borderDash: "dashed"`、`borderWidth: 1`、`borderColor: "#DEE0E3"`(统一浅灰色),否则会被编译为虚拟 frame 导致不渲染
|
||||
- 统一高度(Flex 自适应,可选):根容器使用 `alignItems: "stretch"`,每个泳道外层 frame 使用 `height: "fill-container"`;泳道内部仍保持 lane label + lane body 的结构
|
||||
|
||||
示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 2,
|
||||
"nodes": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "lanes-root",
|
||||
"x": 40, "y": 40,
|
||||
"layout": "horizontal",
|
||||
"gap": 16,
|
||||
"alignItems": "stretch",
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "lane-left",
|
||||
"layout": "vertical",
|
||||
"width": "fit-content",
|
||||
"height": "fill-container",
|
||||
"fillColor": "#F8F9FA",
|
||||
"borderDash": "dashed",
|
||||
"borderWidth": 1,
|
||||
"borderColor": "#DEE0E3",
|
||||
"children": [
|
||||
{ "type": "frame", "id": "lane-left-label-wrap", "layout": "vertical", "width": "fill-container", "height": "fit-content",
|
||||
"alignItems": "center", "justifyContent": "center", "padding": [12, 8, 8, 8], "children": [
|
||||
{ "type": "text", "id": "lane-left-label", "text": "Lane Left", "width": "fill-container", "height": "fit-content",
|
||||
"textAlign": "center", "verticalAlign": "middle", "fontSize": 18, "fontWeight": "bold", "textColor": "#5178C6" }
|
||||
] },
|
||||
{ "type": "frame", "id": "lane-left-body", "layout": "vertical",
|
||||
"gap": 40, "padding": 16,
|
||||
"children": [
|
||||
{ "type": "frame", "id": "stage-1-cell-left", "layout": "vertical", "width": 220, "height": 80, "alignItems": "center", "justifyContent": "center",
|
||||
"children": [{ "type": "rect", "id": "c-s1", "width": 200, "height": "fit-content", "fillColor": "#E1EAFA", "borderColor": "#5178C6", "borderWidth": 2, "borderRadius": 8 }] },
|
||||
{ "type": "frame", "id": "stage-2-cell-left", "layout": "vertical", "width": 220, "height": 80, "alignItems": "center", "justifyContent": "center", "children": [] }
|
||||
] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "lane-right",
|
||||
"layout": "vertical",
|
||||
"width": "fit-content",
|
||||
"height": "fill-container",
|
||||
"fillColor": "#F8F9FA",
|
||||
"borderDash": "dashed",
|
||||
"borderWidth": 1,
|
||||
"borderColor": "#DEE0E3",
|
||||
"children": [
|
||||
{ "type": "frame", "id": "lane-right-label-wrap", "layout": "vertical", "width": "fill-container", "height": "fit-content",
|
||||
"alignItems": "center", "justifyContent": "center", "padding": [12, 8, 8, 8], "children": [
|
||||
{ "type": "text", "id": "lane-right-label", "text": "Lane Right", "width": "fill-container", "height": "fit-content",
|
||||
"textAlign": "center", "verticalAlign": "middle", "fontSize": 18, "fontWeight": "bold", "textColor": "#8569CB" }
|
||||
] },
|
||||
{ "type": "frame", "id": "lane-right-body", "layout": "vertical",
|
||||
"gap": 40, "padding": 16,
|
||||
"children": [
|
||||
{ "type": "frame", "id": "stage-1-cell-right", "layout": "vertical", "width": 220, "height": 80, "alignItems": "center", "justifyContent": "center", "children": [] },
|
||||
{ "type": "frame", "id": "stage-2-cell-right", "layout": "vertical", "width": 220, "height": 80, "alignItems": "center", "justifyContent": "center",
|
||||
"children": [{ "type": "rect", "id": "d-s2", "width": 200, "height": "fit-content", "fillColor": "#EAE6F3", "borderColor": "#8569CB", "borderWidth": 2, "borderRadius": 8 }] }
|
||||
] }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{ "type": "connector", "connector": { "from": "c-s1", "to": "d-s2",
|
||||
"lineShape": "polyline", "lineColor": "#BBBFC4", "lineWidth": 2, "endArrow": "arrow" } }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 泳道配色(默认色板)
|
||||
|
||||
- **泳道背景**:所有泳道容器统一使用极浅灰色(如 `fillColor: "#F8F9FA"` 或 `"#FCFCFC"`),以增强物理容器的层级感,并突出内部的彩色卡片。
|
||||
- **泳道边框**:所有泳道外层容器统一使用浅灰色细虚线(`borderColor: "#DEE0E3"`, `borderWidth: 1`, `borderDash: "dashed"`)。
|
||||
- **泳道标题**:按 `elements/style.md` 经典色板为每条泳道分配不同的主题色,泳道 title 的 `textColor` 使用该主题色。
|
||||
- **内容节点(rect)**:采用“浅色底 + 主题色边框”策略。`fillColor` 使用与该泳道主题色对应的极浅色(如浅蓝、浅紫等),`borderColor` 使用对应的主题色,文字 `textColor` 统一使用深色 `#1F2329`。
|
||||
- **连线(connector)**:连线颜色固定为灰色 `#BBBFC4`,不随泳道颜色变化。当连线带有文字(`label`)时,为防止文字压在边框上难以阅读,必须为连线文字设置纯白背景(`labelFillColor: "#FFFFFF"`)遮挡底纹。
|
||||
|
||||
提醒:避免创建“虚拟 frame”(见 `elements/schema.md` 的说明)。lane 外层必须具有可见属性以避免在编译时被跳过。
|
||||
|
||||
|
||||
## 连线规则(强制参考 connectors.md)
|
||||
|
||||
泳道图中所有连线的选择与写法必须严格遵循 `elements/connectors.md`,尤其是:
|
||||
- `connector` 必须放在 `WBDocument.nodes` 顶层,不能嵌套在 `children`
|
||||
- 默认优先使用自动绕线:`lineShape: "polyline"` / `"rightAngle"`,且不写 `waypoints`
|
||||
- 未指定 `lineShape` 时默认使用 `"rightAngle"`
|
||||
- 只有在必要时才强制锚点方向;锚点选择必须与节点相对位置一致
|
||||
- 有连线时卡片间距必须满足 `gap >= 40`;如果连线包含文字(`label`),主轴间距必须 `gap >= 64`
|
||||
- 带文字的连线必须设置 `labelFillColor: "#FFFFFF"` 遮挡底纹
|
||||
|
||||
泳道图语境下的落地约束:
|
||||
- **默认不写锚点**,交给引擎自动推断;只有需要强制“左→右推进 / 上→下推进”时才写
|
||||
- 需要表达“异步/事件流/推送”(如 SSE/Chunk)时:使用 `lineStyle: "dashed"` 并配合 `label` 说明语义;其他参数仍按 connectors.md
|
||||
- 避免连接“仅用于布局且可能被优化掉的虚拟 frame”,尽量连接具体步骤卡片的节点 id(参考 `elements/schema.md` 的虚拟 frame 陷阱)
|
||||
|
||||
## 骨架示例
|
||||
|
||||
> 示例展示布局的结构与对齐方法;实际节点的样式满足当前布局规则的前提下参考 `elements/style.md`
|
||||
|
||||
- 水平泳道示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 2,
|
||||
"nodes": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "lanes-root",
|
||||
"x": 40,
|
||||
"y": 40,
|
||||
"layout": "vertical",
|
||||
"gap": 16,
|
||||
"alignItems": "stretch",
|
||||
"padding": 0,
|
||||
"width": "fit-content",
|
||||
"height": "fit-content",
|
||||
"children": [
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "lane-a",
|
||||
"layout": "horizontal",
|
||||
"gap": 40,
|
||||
"padding": 16,
|
||||
"width": "fit-content",
|
||||
"height": "fill-container",
|
||||
"fillColor": "#F8F9FA",
|
||||
"borderDash": "dashed",
|
||||
"borderWidth": 1,
|
||||
"borderColor": "#DEE0E3",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "lane-a-label",
|
||||
"text": "Lane A",
|
||||
"width": 120,
|
||||
"height": "fit-content",
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "middle",
|
||||
"fontSize": 18,
|
||||
"fontWeight": "bold",
|
||||
"textColor": "#5178C6"
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "stage-1-cell-a",
|
||||
"layout": "vertical",
|
||||
"gap": 8,
|
||||
"padding": 0,
|
||||
"width": 200,
|
||||
"height": "fit-content",
|
||||
"fillColor": "transparent",
|
||||
"alignItems": "stretch",
|
||||
"justifyContent": "center",
|
||||
"children": [
|
||||
{
|
||||
"type": "rect",
|
||||
"id": "a-s1",
|
||||
"width": "fill-container",
|
||||
"height": "fit-content",
|
||||
"fillColor": "#E1EAFA",
|
||||
"borderColor": "#5178C6",
|
||||
"borderWidth": 2,
|
||||
"borderRadius": 8,
|
||||
"text": "[阶段 1 节点]",
|
||||
"fontSize": 14,
|
||||
"textColor": "#1F2329",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "stage-2-cell-a",
|
||||
"layout": "vertical",
|
||||
"gap": 8,
|
||||
"padding": 0,
|
||||
"width": 200,
|
||||
"height": "fit-content",
|
||||
"fillColor": "transparent",
|
||||
"alignItems": "stretch",
|
||||
"justifyContent": "center",
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "lane-b",
|
||||
"layout": "horizontal",
|
||||
"gap": 40,
|
||||
"padding": 16,
|
||||
"width": "fit-content",
|
||||
"height": "fill-container",
|
||||
"fillColor": "#F8F9FA",
|
||||
"borderDash": "dashed",
|
||||
"borderWidth": 1,
|
||||
"borderColor": "#DEE0E3",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"id": "lane-b-label",
|
||||
"text": "Lane B",
|
||||
"width": 120,
|
||||
"height": "fit-content",
|
||||
"textAlign": "left",
|
||||
"verticalAlign": "middle",
|
||||
"fontSize": 18,
|
||||
"fontWeight": "bold",
|
||||
"textColor": "#8569CB"
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "stage-1-cell-b",
|
||||
"layout": "vertical",
|
||||
"gap": 8,
|
||||
"padding": 0,
|
||||
"width": 200,
|
||||
"height": "fit-content",
|
||||
"fillColor": "transparent",
|
||||
"alignItems": "stretch",
|
||||
"justifyContent": "center",
|
||||
"children": []
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"id": "stage-2-cell-b",
|
||||
"layout": "vertical",
|
||||
"gap": 8,
|
||||
"padding": 0,
|
||||
"width": 200,
|
||||
"height": "fit-content",
|
||||
"fillColor": "transparent",
|
||||
"alignItems": "stretch",
|
||||
"justifyContent": "center",
|
||||
"children": [
|
||||
{
|
||||
"type": "rect",
|
||||
"id": "b-s2",
|
||||
"width": "fill-container",
|
||||
"height": "fit-content",
|
||||
"fillColor": "#EAE6F3",
|
||||
"borderColor": "#8569CB",
|
||||
"borderWidth": 2,
|
||||
"borderRadius": 8,
|
||||
"text": "[阶段 2 节点]",
|
||||
"fontSize": 14,
|
||||
"textColor": "#1F2329",
|
||||
"textAlign": "center",
|
||||
"verticalAlign": "middle"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "connector",
|
||||
"connector": {
|
||||
"from": "a-s1",
|
||||
"to": "b-s2",
|
||||
"lineShape": "polyline",
|
||||
"lineColor": "#BBBFC4",
|
||||
"lineWidth": 2,
|
||||
"endArrow": "arrow",
|
||||
"label": "[跨泳道交互]",
|
||||
"labelFillColor": "#FFFFFF"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- 垂直泳道示例:见上文“垂直泳道”
|
||||
|
||||
- 全泳道统一 `slotWidth/slotHeight/gap`,并为每个阶段生成占位 **stage cell**(空阶段 cell 透明但保留)
|
||||
- Flex 容器内不写子节点 `x/y`;对齐通过 cell 索引与统一尺寸实现
|
||||
- 只有真实阶段才在对应 cell 内生成卡片;空阶段不生成卡片但保留 cell 保证网格完整
|
||||
- 连线必须放在 `nodes` 顶层,并连接具体步骤卡片 id,不要连接 `lane-*-body` 这类布局容器
|
||||
- **水平泳道**:根容器用 `layout: "vertical"` 固定 `lanesGap`;lane body 用 `layout: "horizontal"`;cell 固定宽度 `slotWidth`;主轴 `gap` 统一
|
||||
- **垂直泳道**:根容器用 `layout: "horizontal"` 固定 `lanesGap`;lane body 用 `layout: "vertical"`;cell 固定高度 `slotHeight`;主轴 `gap` 统一
|
||||
- **泳道 title**:title 比步骤卡片更醒目,但仍只用字号、字重、文字色强调;不要给泳道 title 额外加背景条
|
||||
|
||||
## 陷阱
|
||||
|
||||
- **各泳道复用的 stage slots 不一致**:会导致同阶段错位;`slotWidth / slotHeight / gap` 必须全泳道统一
|
||||
- **把 connector 放进 children**:会导致 schema 报错或无法连线(见 connectors.md)
|
||||
- **把辅助容器画成可见元素**:lane body 或其他支撑 frame 必须保持 `fillColor: "transparent"`,除泳道分组容器外不要额外加边框
|
||||
- **手写 waypoints 过早**:先让引擎自动绕线;只有在必要时才通过 waypoints 接管
|
||||
- **连线过多**:按 connectors.md 的连线数量策略降采样,否则跨泳道线会互相遮挡导致不可读
|
||||
216
.claude/skills/lark-whiteboard/scenes/treemap.md
Normal file
216
.claude/skills/lark-whiteboard/scenes/treemap.md
Normal file
@ -0,0 +1,216 @@
|
||||
# 矩形树图 (Treemap)
|
||||
|
||||
## Content 约束
|
||||
|
||||
- 分类 3-5 个,每个分类下子项 2-4 个
|
||||
- 总面积比例需预先计算:每个矩形面积 = 父矩形面积 * (本项数值 / 同级总数值)
|
||||
- 每个叶子节点标签必须包含数值(如 "{{LABEL}} ({{VALUE}})")
|
||||
|
||||
## Layout 选型
|
||||
|
||||
- **脚本生成坐标**(推荐):Treemap 需要精确的面积比例计算,用 .cjs 脚本递归切分矩形,脚本输出 JSON 文件后调用 `npx -y @larksuite/whiteboard-cli@^0.2.13` 渲染
|
||||
- 不适合手动心算坐标
|
||||
|
||||
## Layout 规则
|
||||
|
||||
- 使用交替切分法(Slice-and-Dice):奇数层水平切分 width,偶数层垂直切分 height
|
||||
- 父矩形内必须为标题预留 30-40px 顶部空间,子矩形从 y + 35 开始放置
|
||||
- 子节点必须完全落在父矩形范围内
|
||||
- 水平切分时:子 width = 父 width * (子数值 / 父总数值),子 x 依次累加
|
||||
- 垂直切分时:子 height = (父 height - 35) * (子数值 / 父总数值),子 y 依次累加(注意扣除父标签预留的 35px)
|
||||
|
||||
### 面积比例计算规则
|
||||
|
||||
1. **面积与数值严格成正比**:任何层级的节点,其矩形面积 `width * height` 必须与数值成比例
|
||||
2. **奇数层水平切分**(如第一层分类):
|
||||
- 父矩形的 `height` 和 `y` 坐标传给所有子节点(扣除标签预留空间后)
|
||||
- 按子节点数值占父节点的比例切分父矩形的 `width`:`子width = 父width * (子数值 / 父总数值)`
|
||||
- 子节点的 `x` 坐标依次向右累加
|
||||
3. **偶数层垂直切分**(如第二层子项):
|
||||
- 父矩形的 `width` 和 `x` 坐标传给所有子节点
|
||||
- 按子节点数值占父节点的比例切分父矩形的 `height`:`子height = 父height * (子数值 / 父总数值)`
|
||||
- 子节点的 `y` 坐标依次向下累加
|
||||
4. **层层递归**:不断交替水平和垂直切分方向,直到所有叶子节点都被分配了精确的坐标和宽高
|
||||
|
||||
### 父标签预留空间
|
||||
|
||||
每个非叶子节点的矩形,顶部必须预留 30-40px 放置分类标签。子矩形从父矩形的 `y + 35` 开始放置,可用高度为 `父height - 35`。
|
||||
|
||||
示例:父矩形 `{ x: 40, y: 40, height: 700 }`,则:
|
||||
- 父标签放在 `y: 46`(留 6px 上边距)
|
||||
- 子矩形从 `y: 75` 开始放置(40 + 35)
|
||||
- 子矩形可用高度为 `700 - 35 = 665`
|
||||
|
||||
## 骨架示例
|
||||
|
||||
2 层 treemap:3 个分类(硬件 40、软件 35、服务 25),各含 2 个子项。
|
||||
|
||||
根矩形 1100x700,第一层水平切分 width,第二层垂直切分 height。
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 2,
|
||||
"nodes": [
|
||||
{
|
||||
"type": "rect",
|
||||
"id": "root",
|
||||
"x": 40, "y": 40,
|
||||
"width": 1100, "height": 700,
|
||||
"borderWidth": 2, "borderRadius": 6
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"x": 48, "y": 46,
|
||||
"width": 1084, "height": 24,
|
||||
"text": "{{ROOT_TITLE}}",
|
||||
"fontSize": 14
|
||||
},
|
||||
|
||||
{
|
||||
"type": "rect",
|
||||
"id": "cat-A",
|
||||
"x": 40, "y": 75,
|
||||
"width": 440, "height": 665,
|
||||
"borderWidth": 2, "borderRadius": 6
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"x": 48, "y": 81,
|
||||
"width": 424, "height": 24,
|
||||
"text": "{{CAT_A}}",
|
||||
"fontSize": 14
|
||||
},
|
||||
{
|
||||
"type": "rect",
|
||||
"id": "cat-A-item-1",
|
||||
"x": 40, "y": 110,
|
||||
"width": 440, "height": 380,
|
||||
"borderRadius": 4
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"x": 48, "y": 116,
|
||||
"width": 424, "height": 24,
|
||||
"text": "{{ITEM_A1}} (24)",
|
||||
"fontSize": 14
|
||||
},
|
||||
{
|
||||
"type": "rect",
|
||||
"id": "cat-A-item-2",
|
||||
"x": 40, "y": 490,
|
||||
"width": 440, "height": 250,
|
||||
"borderRadius": 4
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"x": 48, "y": 496,
|
||||
"width": 424, "height": 24,
|
||||
"text": "{{ITEM_A2}} (16)",
|
||||
"fontSize": 14
|
||||
},
|
||||
|
||||
{
|
||||
"type": "rect",
|
||||
"id": "cat-B",
|
||||
"x": 480, "y": 75,
|
||||
"width": 385, "height": 665,
|
||||
"borderWidth": 2, "borderRadius": 6
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"x": 488, "y": 81,
|
||||
"width": 369, "height": 24,
|
||||
"text": "{{CAT_B}}",
|
||||
"fontSize": 14
|
||||
},
|
||||
{
|
||||
"type": "rect",
|
||||
"id": "cat-B-item-1",
|
||||
"x": 480, "y": 110,
|
||||
"width": 385, "height": 380,
|
||||
"borderRadius": 4
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"x": 488, "y": 116,
|
||||
"width": 369, "height": 24,
|
||||
"text": "{{ITEM_B1}} (20)",
|
||||
"fontSize": 14
|
||||
},
|
||||
{
|
||||
"type": "rect",
|
||||
"id": "cat-B-item-2",
|
||||
"x": 480, "y": 490,
|
||||
"width": 385, "height": 285,
|
||||
"borderRadius": 4
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"x": 488, "y": 496,
|
||||
"width": 369, "height": 24,
|
||||
"text": "{{ITEM_B2}} (15)",
|
||||
"fontSize": 14
|
||||
},
|
||||
|
||||
{
|
||||
"type": "rect",
|
||||
"id": "cat-C",
|
||||
"x": 865, "y": 75,
|
||||
"width": 275, "height": 665,
|
||||
"borderWidth": 2, "borderRadius": 6
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"x": 873, "y": 81,
|
||||
"width": 259, "height": 24,
|
||||
"text": "{{CAT_C}}",
|
||||
"fontSize": 14
|
||||
},
|
||||
{
|
||||
"type": "rect",
|
||||
"id": "cat-C-item-1",
|
||||
"x": 865, "y": 110,
|
||||
"width": 275, "height": 399,
|
||||
"borderRadius": 4
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"x": 873, "y": 116,
|
||||
"width": 259, "height": 24,
|
||||
"text": "{{ITEM_C1}} (15)",
|
||||
"fontSize": 14
|
||||
},
|
||||
{
|
||||
"type": "rect",
|
||||
"id": "cat-C-item-2",
|
||||
"x": 865, "y": 509,
|
||||
"width": 275, "height": 231,
|
||||
"borderRadius": 4
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"x": 873, "y": 515,
|
||||
"width": 259, "height": 24,
|
||||
"text": "{{ITEM_C2}} (10)",
|
||||
"fontSize": 14
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
面积比例验证(第一层水平切分 width):
|
||||
- 硬件 40/100 * 1100 = 440,软件 35/100 * 1100 = 385,服务 25/100 * 1100 = 275
|
||||
- 子矩形从 y=75 开始,可用高度 665
|
||||
|
||||
## 陷阱
|
||||
|
||||
- **父标签被子矩形遮挡**(最严重):子矩形必须从 y + 35(相对父矩形顶部)开始放置,为父分类标签留出空间
|
||||
- **分类标签不可见**:分类标签 text 节点必须在其子矩形 rect 节点之前添加(z-index 靠后的节点在上层)
|
||||
- **面积比例不正确**:必须用脚本预先计算比例,不要心算
|
||||
- **缺少配色区分**:不同顶层分类必须用不同背景色(从色板选取),所有子节点继承对应色系
|
||||
|
||||
此场景必须用 .cjs 脚本生成。Agent 使用时只需修改 `data` 树,其余坐标与矩形面积自动递归计算。
|
||||
|
||||
```javascript
|
||||
const { writeFileSync } = require('fs');
|
||||
```
|
||||
Reference in New Issue
Block a user