| name | excalidraw-diagram |
| description | 使用 excalidraw-cli 和 @moona3k/excalidraw-export 绘制手绘风格示意图并导出为 SVG。当用户要求绘制线框图、界面流转图、架构图、流程图,或提到 excalidraw、手绘风格图、wireframe、示意图时,主动使用此 skill。也适用于需要将已有 .excalidraw 文件转为 SVG 的场景。 |
Excalidraw 手绘风示意图
通过 excalidraw-cli 生成 .excalidraw 文件,再用 @moona3k/excalidraw-export 导出为内嵌 Virgil 手写字体的手绘风 SVG。全程 npx,零全局安装。
意图识别与路由
收到任务后,先判断属于哪条路径,再跳转到对应章节执行。
四条路径互斥,每次只走一条。如果意图不清晰,直接问用户要做 A/B/C/D 哪件事,不要猜。
同时需要 D + C 时:先完成 D(步骤标注),再做 C(模块规则标注)。这样路径 C 的序号列表可以直接追加在步骤标注之后,无需移动已有内容。
工具链
两步走,都用 npx:
步骤1: npx excalidraw-cli create <input.json> -o <output.excalidraw> --no-banner --no-checkpoint
步骤2: npx @moona3k/excalidraw-export <output.excalidraw> --svg -o <output.svg>
两种工作流程
场景 A:修改已有 .excalidraw 文件(最小改动)
优先场景:用户要求修改、更新或保持一致性时。
第一步:读取源文件,不读 SVG
当需要了解图的结构时,先检查同名 .excalidraw 文件是否存在。.excalidraw 是干净的 JSON,可以直接读取;SVG 文件通常 5万–10万 token,根本读不完。
output.svg ← 不要读这个
output.excalidraw ← 读这个,找 "id" 字段定位元素
分段读取 .excalidraw(每次 limit=200 行),找到目标元素的 id 和坐标后就够了,不需要读完全文。
第二步:精确定位要改的元素
只改用户要求修改的元素,以及因此受影响的关联元素(如绑定的 label、红点标注)。其余元素一律不动。
判断"受影响"的依据:
- 移动了形状 → 同步移动绑定的 label 文字、指向该位置的标注红点和虚线
- 改了颜色 → 只改该元素的颜色字段
- 删除了元素 → 删除整个 JSON 对象块,不影响其他元素
第三步:用 Edit 工具做字符串替换
直接对 .excalidraw 文件做 Edit,精确替换目标元素的字段值。不需要写临时 JSON,不需要重新生成整个文件。
# 只改一个元素的颜色:直接 Edit
"backgroundColor": "#a5d8ff" → "backgroundColor": "#ffc9c9"
# 删除一个完整元素块:Edit 替换为空字符串
如果改动超过 5 个元素,或者逻辑复杂,可以改用 PowerShell 脚本做批量替换(见下方注意事项)。
第四步:重新导出 SVG
改完 .excalidraw 后,走正常的两步导出:
npx excalidraw-cli create "<path>/file.excalidraw" --no-banner --no-checkpoint -o "<path>/file-tmp.excalidraw"
npx @moona3k/excalidraw-export "<path>/file-tmp.excalidraw" --svg -o "<path>/file.svg"
Remove-Item "<path>/file-tmp.excalidraw" -Force
导出后如需在 Markdown 中嵌入,记得使用相对于 md 文件的相对路径(见场景 B 第 5 步)。
场景 B:从头创建新图
1. 准备 JSON 元素文件
将 Excalidraw 元素写成 JSON 数组,保存到临时文件(如 _tmp.json)。JSON 内容过长会导致命令行溢出,所以始终用文件输入而不是 --json 内联。
2. 生成 .excalidraw
npx excalidraw-cli create "<path>/_tmp.json" --no-banner --no-checkpoint -o "<path>/output.excalidraw"
3. 导出 SVG
npx @moona3k/excalidraw-export "<path>/output.excalidraw" --svg -o "<path>/output.svg"
4. 清理临时文件
Remove-Item "<path>/_tmp.json" -Force
5. 嵌入 Markdown 文档
如果任务包含将 SVG 嵌入到 Markdown(Obsidian)文档,链接路径必须是相对于 md 文件的相对路径,不能只写文件名。原因:仅写文件名时 Obsidian 会全局搜索(通常能渲染),但 iwiki-push 等工具无法定位文件,导致上传失败。
# SVG 在 md 同级的 ref/ 目录下 → 正确写法
![[ref/uj01-flow.svg|1000]]
# 只有文件名 → 避免
![[uj01-flow.svg|1000]]
计算方法: 相对路径 = SVG 路径相对于 md 所在目录的路径。
- md 在
dir/设计案.md,SVG 在 dir/ref/flow.svg → 写 ![[ref/flow.svg|1000]]
- md 在
dir/设计案.md,SVG 在 dir/flow.svg(同级)→ 写 ![[flow.svg|1000]]
6. 内嵌线框备注(特例)
文本线框图中有时会自带箭头备注(如标注某个控件的交互说明、状态说明)。建图时可以用与模块标注相同的视觉格式(红色文字 + 红色虚线)将这些备注渲染进图中,但豁免来源标注要求——它们是线框图本身携带的描述,不属于某个功能模块,无需附 (模块X)。
为了让后续路径 C 能识别这类标注,其文字内容末尾不得出现 (模块X) 格式的括号(这是区分依据)。id 建议使用 wf- 前缀(如 wf-anno1)以便日后 Grep 定位,但不强制。
Excalidraw 元素格式参考
必填字段
所有元素都需要:type、id(唯一字符串)、x、y、width、height
默认值(可省略)
strokeColor: "#1e1e1e"
backgroundColor: "transparent"
fillStyle: "solid"
strokeWidth: 2
roughness: 2(手绘粗糙度,越高越像手绘)
opacity: 100
- 形状默认带
roundness: {type: 3}(圆角)
- 文字默认
fontFamily: 1(Virgil 手写体)
元素类型
矩形
{"type":"rectangle","id":"r1","x":100,"y":100,"width":200,"height":100}
填充色:"backgroundColor":"#a5d8ff","fillStyle":"solid"
椭圆
{"type":"ellipse","id":"e1","x":100,"y":100,"width":150,"height":150}
菱形
{"type":"diamond","id":"d1","x":100,"y":100,"width":150,"height":150}
带标签的形状(推荐)
在矩形/椭圆/菱形/箭头上加 label 字段,CLI 会自动展开为居中文本:
{"type":"rectangle","id":"r1","x":100,"y":100,"width":200,"height":80,
"label":{"text":"Hello","fontSize":20}}
label 属性:text(必填)、fontSize(默认20)、fontFamily、strokeColor
独立文本(用于标题、标注)
{"type":"text","id":"t1","x":150,"y":138,"text":"Hello","fontSize":20}
- x 是文本左边缘
- y 是文本顶边,文字垂直中心在
y + fontSize/2
- 居中时需手动计算:
x = cx - estimatedWidth / 2
- 宽度估算见下方「Virgil 字体宽度计算」
Virgil 字体宽度计算
SVG 导出后使用 Virgil 手写字体,字符宽度和常规字体差异很大。放置标注红点、对齐元素时,必须用以下经验公式估算文字渲染宽度,否则会严重偏移。
单字符宽度(经实测校准):
- CJK 字符(汉字、日文,以及全角标点
:,。!?、【】「」《》""'' 等):fontSize × 0.9
判断方法:Unicode 码点 ≥ U+2E80 的字符均按 CJK 计算。全角标点不是 ASCII 标点,切勿按 0.35 计算。
- ASCII 字母/数字(a–z、A–Z、0–9):
fontSize × 0.45
- ASCII 空格:
fontSize × 0.25
- ASCII 标点(
/、-、(、)、[、]、.、,、|、:、!、? 等半角标点):fontSize × 0.35
⚠️ 易错点:步骤标注常用的格式 [复用|全屏] 大厅主界面:... 中,[、|、] 是 ASCII 标点(×0.35),而 : 是全角标点(×0.9,CJK 宽度)。混淆后宽度会被严重低估。
文本总宽度 = 逐字符累加各字符宽度
计算示例:
以 fontSize: 14 为例,标注格式 [复用|全屏] 大厅主界面:游戏主导航界面,各功能模块入口聚合于此:
| 字符 | 类型 | 数量 | 单宽 | 小计 |
|---|
[、|、] | ASCII 标点 | 3 | 4.9 | 14.7 |
(空格) | ASCII 空格 | 1 | 3.5 | 3.5 |
| 复用、全屏、大厅主界面、游戏主导航界面、各功能模块入口聚合于此 | CJK 汉字 | 24 | 12.6 | 302.4 |
:、, | CJK 全角标点 | 2 | 12.6 | 25.2 |
| 合计 | | | | ≈ 346px |
以 fontSize: 12 为例:
| 文本 | CJK数 | ASCII字母数字数 | 空格数 | 标点数 | 总宽度 |
|---|
人机/快速 - 街头获胜1场 0/1 [?] | 6 | 4 | 5 | 7 | 6×10.8 + 4×5.4 + 5×3.0 + 7×4.2 = 130px |
排位/巅峰 - 街头获胜1场 0/1 | 7 | 2 | 4 | 4 | 7×10.8 + 2×5.4 + 4×3.0 + 4×4.2 = 115px |
文字起始于 x:50,则末尾分别在 x≈180 和 x≈165。
使用场景:
- 连线标注:在文字末尾放标注红点时,红点 x = 文字起始x + 总宽度 + 5px间距
- 红点垂直居中对齐文字行:红点 y = 文字y + (fontSize - 红点高度) / 2
- 虚线箭头起点 = 红点右侧:箭头 x = 红点x + 红点宽度
- 序号气泡:气泡圆心 = 目标文字末尾位置(同样用宽度公式算出);圆圈 x = 文字起始x + 总宽度,圆圈 y = 文字y + (fontSize - 16) / 2
箭头
{"type":"arrow","id":"a1","x":300,"y":150,"width":200,"height":0,
"points":[[0,0],[200,0]],"endArrowhead":"arrow"}
points: 相对于 (x,y) 的偏移量数组
endArrowhead: null | "arrow" | "bar" | "dot" | "triangle"
- 带标签:
"label":{"text":"连接文字"}
箭头绑定
"startBinding":{"elementId":"r1","fixedPoint":[1,0.5]},
"endBinding":{"elementId":"r2","fixedPoint":[0,0.5]}
fixedPoint 位置:顶部=[0.5,0]、底部=[0.5,1]、左=[0,0.5]、右=[1,0.5]
视口控制(伪元素,不绘制,控制 Excalidraw 打开时的初始视图)
{"type":"cameraUpdate","width":800,"height":600,"x":0,"y":0}
必须用 4:3 比例:400x300、600x450、800x600、1200x900、1600x1200
删除元素(伪元素)
{"type":"delete","ids":"b2,a1,t3"}
绘制顺序
数组顺序 = z 轴顺序(先画的在后面)。推荐渐进式:
- 好:背景 → 形状1 → 文字1 → 箭头1 → 形状2 → 文字2 → 所有标注元素(最后)
- 差:全部矩形 → 全部文字 → 全部箭头
标注元素(红点、红色虚线、红色标注文字)必须在 JSON 数组最末尾,确保层级最高、不被任何其他元素遮挡。即使标注指向的是早期元素,标注三件套也必须放在数组最后。
颜色规范
填充色(浅色,形状背景)
| 用途 | 色号 |
|---|
| 输入/主要节点 | #a5d8ff(浅蓝) |
| 成功/输出 | #b2f2bb(浅绿) |
| 警告/待处理 | #ffd8a8(浅橙) |
| 处理中/特殊 | #d0bfff(浅紫) |
| 错误/关键 | #ffc9c9(浅红) |
| 笔记/决策 | #fff3bf(浅黄) |
| 存储/数据 | #c3fae8(浅青) |
主色(边框/文字)
| 用途 | 色号 |
|---|
| 蓝色 主操作 | #4a9eed |
| 琥珀 警告 | #f59e0b |
| 绿色 成功 | #22c55e |
| 红色 错误 | #ef4444 |
| 紫色 强调 | #8b5cf6 |
| 灰色 次要文字 | #757575 |
区域背景色(opacity:30 用于分层)
| 用途 | 色号 |
|---|
| UI/前端层 | #dbe4ff |
| 逻辑/代理层 | #e5dbff |
| 数据/工具层 | #d3f9d8 |
字号规范
- 标题:>= 20px
- 正文/标签:>= 16px
- 次要标注:>= 14px(少用)
- 绝不使用 < 14px
尺寸规范
- 带标签矩形最小:120x60
- 元素间距最小:20-30px
- 优先用少量大元素而非大量小元素
重要注意事项
SVG 边界问题
SVG 导出按元素实际边界自动裁剪。任何时候只要图里有标注文字(步骤标注、模块规则标注、连线文字等),都必须确保 bound 矩形完整覆盖所有标注内容,否则文字会被裁掉。
添加标注后必须重新计算并更新 bound
每次添加完所有标注后,按以下步骤检查并更新 bound 矩形。
第一步:算出所有标注行中最长一行的渲染宽度
使用「Virgil 字体宽度计算」公式(详见上方同名章节),对每条标注文字的每一行逐字符累加:
| 字符类型 | 单字符宽度 |
|---|
| CJK(中文、全角) | fontSize × 0.9 |
| ASCII 字母/数字 | fontSize × 0.45 |
| ASCII 空格 | fontSize × 0.25 |
ASCII 标点(/-.()[],) | fontSize × 0.35 |
找出所有行中渲染宽度最大的那一行,记为 W_max。
示例(fontSize=14,标注从 x=950 开始):
| 标注行文字 | 字符构成 | 渲染宽度 |
|---|
任务界面:任务系统主界面,三类任务进度和奖励领取均在此完成 | 27 CJK + 2 CJK全角标点 | (27+2)×12.6 = 365px |
[复用|全屏] 大厅主界面:游戏主导航界面,各功能模块入口聚合于此 | 24 CJK + 2 CJK全角标点 + 3 ASCII标点 + 1 ASCII空格 | 26×12.6 + 3×4.9 + 1×3.5 = 344px |
① 固定顺序: 每日->每周->挑战 (模块一) | 10 CJK + 14 ASCII字母数字 + 5 空格 + 6 标点 | 10×12.6 + 14×6.3 + 5×3.5 + 6×4.9 = 244px |
此例 W_max = 365px。
第二步:计算 bound 所需右边界
右边界 = 标注文字起始x + W_max + 80px(冗余,用于覆盖 Virgil 字体的渲染误差)
bound.width = 右边界 - bound.x
冗余设为 80px(而非更小的 40px)。Virgil 手写字体有固有渲染误差,经验公式本身也只是估算,80px 是安全下限——宽一点不影响显示,窄了就会裁字。
代入示例:右边界 = 950 + 346 + 80 = 1376,bound.x = -20,→ bound.width = 1376 - (-20) = **1396**,向上取整到 1400。
第三步:计算 bound 所需下边界(高度)
找出最靠下的标注元素(y 最大的那个),加上该文字块的高度和 40px 冗余:
文字块高度 = 行数 × (fontSize × 1.4) ← Virgil 行高经验值
下边界 = 最低标注元素y + 文字块高度 + 40px
bound.height = 下边界 - bound.y
第四步:用 Edit 工具更新 bound
检查现有 bound 的 x + width 和 y + height 是否分别覆盖右边界和下边界。若不够,直接 Edit 更新对应字段:
{"type":"rectangle","id":"bound","x":-20,"y":-80,"width":1380,"height":2820,
"strokeWidth":1,"strokeColor":"#adb5bd","strokeStyle":"dashed","opacity":30}
标注(红色虚线 + 红点标记)
标注由三个元素组成:红点(标注起点)→ 红色虚线(连接线)→ 红色文字(说明)。红点让读者一眼定位到被标注的目标元素。
放置标注时需要用「Virgil 字体宽度计算」公式精确算出目标文字末尾位置,再在末尾放红点。不要凭感觉估计位置,Virgil 手写字体比常规字体窄很多,不算会严重偏移。
完整标注三件套:
{"type":"ellipse","id":"an-dot","x":<文字末尾x>,"y":<文字y + (fontSize-8)/2>,
"width":8,"height":8,
"backgroundColor":"#ef4444","fillStyle":"solid","strokeColor":"#ef4444","strokeWidth":1},
{"type":"arrow","id":"an-line","x":<红点x+8>,"y":<红点y+4>,
"width":<到标注文字的距离>,"height":0,
"points":[[0,0],[<到标注文字的距离>,0]],
"endArrowhead":null,
"strokeColor":"#ef4444","strokeWidth":1,"strokeStyle":"dashed"},
{"type":"text","id":"an-text","x":<标注区起始x>,"y":<虚线y - 7>,
"text":"标注说明","fontSize":14,"strokeColor":"#ef4444"}
红点定位计算步骤:
- 用 Virgil 字体宽度公式算出目标文字的渲染宽度 W
- 红点 x = 文字起始x + W + 5(5px 间距)
- 红点 y = 文字y + (fontSize - 8) / 2(垂直居中于文字行)
- 虚线起点 x = 红点x + 8(红点右侧)
- 虚线 y = 红点y + 4(红点垂直中心)
- 标注文字 y = 虚线y - 7(fontSize 14 时垂直居中于虚线)
确保标注文字完全在边界框内,不与界面元素重叠。
标注层级规则: 所有标注元素(红点、红色虚线、红色标注文字)必须放在 JSON 数组最末尾,确保 z 轴层级最高。标注需要始终可见,不能被形状、箭头或其他元素遮挡。即使标注指向的是图中较早出现的元素,标注三件套也统一放在数组最后。
说明性标签(如「复用原有界面」)
流转图中有时需要对整个界面加文字标签说明其性质,例如「复用原有界面」。这类标签与标注三件套的用色规范一致,同时用虚线框而非实线框,以避免与界面内的功能按钮混淆:
{
"type": "rectangle",
"id": "badge-reuse",
"x": <界面右上角 x>,
"y": <界面顶边 y - 26>,
"width": 180,
"height": 22,
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeColor": "#ef4444",
"strokeWidth": 1,
"strokeStyle": "dashed",
"boundElements": [{"id": "badge-reuse_label", "type": "text"}],
"roundness": {"type": 3},
"roughness": 1
},
{
"type": "text",
"id": "badge-reuse_label",
"x": <同上 x + 10>,
"y": <同上 y + 4>,
"width": 160,
"height": 14,
"text": "复用原有界面",
"fontSize": 14,
"fontFamily": 1,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "badge-reuse",
"strokeColor": "#ef4444"
}
- 标签贴在对应界面框的外侧顶边(y = 界面 y - 26),不遮挡界面内容
- 透明背景 + 红色虚线框 + 红色文字,与标注三件套颜色统一(
#ef4444)
- 虚线(
strokeStyle: "dashed")与界面内实线按钮明确区分
- 同样放在 JSON 数组末尾,确保层级最高
不要使用 emoji
Excalidraw 的 Virgil 字体不支持 emoji 渲染,用文字描述代替。例如用 [放大镜] 而不是放大镜 emoji。
PowerShell 写入文件必须用无 BOM 的 UTF-8
用 PowerShell 脚本批量修改 .excalidraw 文件时,必须用 UTF8Encoding($false) 写出,否则文件开头会带 BOM(),导致 excalidraw-cli 报 JSON parse 错误。
# 正确:无 BOM
$utf8NoBom = New-Object System.Text.UTF8Encoding $false
[System.IO.File]::WriteAllText($path, $content, $utf8NoBom)
# 错误:默认的 Set-Content / Out-File 会带 BOM
如果已经写入了带 BOM 的文件,可以这样修复:
$content = [System.IO.File]::ReadAllText($path, [System.Text.Encoding]::UTF8)
if ($content.StartsWith([char]0xFEFF)) { $content = $content.Substring(1) }
[System.IO.File]::WriteAllText($path, $content, (New-Object System.Text.UTF8Encoding $false))
PowerShell here-string 里坐标值末尾不要加多余引号
在 here-string(@'...'@)里写 JSON 时,数字字段后面容易误加一个额外的 " 变成 "y":696",导致 CLI 报 Expected ',' or '}' after property value。写完后用正则快速自检:
# 检测是否存在 "数字字段名":数字" 的错误模式
if ($content -match '"[a-z]+":(\d+)"') { Write-Host "WARNING: stray quote after number detected" }
如果已出错,批量修复:
$content = [System.Text.RegularExpressions.Regex]::Replace($content, '"([xy])":(\d+)"', '"$1":$2')
长 JSON 必须用文件输入
JSON 超过几百字符就会导致命令行溢出。始终写到临时 JSON 文件再传给 CLI。
暗色模式
第一个元素放一个巨大的深色矩形作为背景:
{"type":"rectangle","id":"darkbg","x":-4000,"y":-3000,"width":10000,"height":7500,
"backgroundColor":"#1e1e2e","fillStyle":"solid","strokeColor":"transparent","strokeWidth":0}
暗色模式文字色:主要 #e5e5e5,次要 #a0a0a0
界面线框图最佳实践
基于 PC 端游界面设计的经验总结:
- 统一长宽比:同一项目所有界面用相同的外框尺寸(如 1100x660 或 900x540)
- 弹窗界面:保持外框不变,加半透明遮罩
opacity:50,弹窗在框内居中
- 非焦点区域模糊化:用低透明度灰色块
"backgroundColor":"#e9ecef","opacity":40 覆盖
- 焦点区域高亮:关键入口用蓝色边框 + 浅蓝填充突出
- 不在界面图上加标注:独立界面图保持纯净,标注放在流转图中
- 奖励/道具图标:用彩色小方块 + label 表示,不用纯文字
- 金币:
#ffd8a8(浅橙),碎片: #d0bfff(浅紫),礼包: #b2f2bb(浅绿)
流转图最佳实践
-
界面保持完整:流转图中每个步骤的界面和独立界面图一模一样,不缩小、不省略
-
复用已有界面布局(逐元素保真):绘制流转图前,先检查保存目录中是否已存在对应界面的独立示意图(如 01-俱乐部推荐页.excalidraw)。如果已有,流转图中该步骤的界面必须逐元素对照已有图复刻,而不是凭记忆"大概画一个类似的"。
核心原则:有差异的部分允许修改,其他部分必须一致。
所谓"有差异的部分"是指:不同视角/状态确实会导致变化的内容——比如按钮文字("申请"→"取消申请")、Tab 高亮切换、列表行数据填充不同、某些按钮在该视角下不可见等。这些改动是合理的。
但除了这些有明确理由的差异外,所有其他元素都必须严格保持和原图一致:
- 元素拆分方式一致:原图把"人数"和"招募状态"拆成两个 text 元素(分别用不同颜色),流转图就不能合并成一行灰色文字
- 文字内容格式一致:原图文字是
* 0 排名-- 就不能简化成 *0;原图有副标题"发展俱乐部"就不能省略
- 颜色和样式一致:每个元素的
strokeColor、backgroundColor、fontSize 都必须与原图一致(按缩放比例调整 fontSize)
- 元素数量一致:不能为了省事跳过或合并元素
做法:读取已有 .excalidraw 文件(分段 limit=200),逐一提取所有元素的 id、type、坐标、文字内容、颜色,建立完整元素清单。然后按流转图的缩放比例(通常 900/1200 = 0.75)等比缩放坐标和尺寸,fontSize 也按比例缩小,将清单中的每个元素都写入流转图 JSON。只修改有差异理由的部分,不要跳过任何元素,不要合并任何元素,不要改写任何文字格式
-
箭头连接:步骤之间用带标签的垂直箭头连接,标签说明触发动作
-
状态变化高亮:前后两个界面只有数据/状态不同时,变化部分用黄色背景高亮
-
游戏中/外部操作:用虚线框 strokeStyle:"dashed" 表示非本系统界面的操作
-
结果框:最终结果用绿色背景框总结
子功能:将功能模块规则标注到 SVG 流转图
当用户提供一份功能模块说明(如 PRD 中的模块一、模块二),希望将其中的规则要点以红点标注的形式标到对应 SVG 流转图中时,使用以下流程。
溯源原则(全局约束):所有标注内容必须能在原始文档中找到对应依据,禁止生造。不得凭理解推断、合理延伸或自行补充文档未明确说明的规则。标注文字可以压缩改写,但含义必须直接来自原文。每条标注都必须附 (模块X) 括号作为溯源凭据,让读者可以按编号回原文核实——这不是装饰,是验证入口,缺失视为不合格标注。
同时标注多个模块时:不要逐模块分别走流程——这会导致同一个文件被读写多次,每次写入都在上一次的基础上累积坐标偏移,最终难以排查。正确做法:先把所有模块的要点汇总到同一张表,再按目标文件分组,每个文件只读写一次。具体节拍:
- 读取所有模块文档,汇总候选要点(第一步)
- 按目标文件分组,建好对比表(第二至四步)
- 对每个目标文件:一次性读取最新状态 → 一次性写入当次所有新标注 → 导出 SVG
- 不同文件之间没有依赖,可以并行处理;同一个文件内的所有修改必须合批完成,不得分多次写入
第一步:理解模块内容,提炼标注要点
读取所有待标注的功能模块文档,将全部模块的要点汇总到同一张候选列表里,每条要点同时记录其来源模块。
提炼原则:
- 聚焦界面可见的行为规则(排序、显示格式、状态切换、数量限制等),略去纯后端逻辑
- 标注只打在界面元素上(任务条目、按钮、分组头、弹窗内容等),不要标注流程图中连接步骤用的说明性文本框(游戏中虚线框、结果绿色框、步骤箭头标签等)。前者是产品规则的载体,后者是流程叙述的辅助,混在一起会让标注失去焦点
- 每条标注文字尽量简洁(≤20字/行,最多3行)
- 每条要点必须在原始文档中有明确依据,不得推断、延伸或自行补充——如果文档没有写,就是没有,不要标
- 来源模块字段不可省略,格式为
(模块X),多模块合并时写 (模块X/Y)
第二步:确定标注位置(首次出现原则)
同一个界面(如任务列表界面)可能在多个 UJ 流转图中出现。规则只标注在该界面第一次出现的那个流转图里,后续重复出现的不加重复标注。
通过 Grep 搜索所有 .excalidraw 文件中该界面的关键 id(如 s\d+-hdr、s\d+-nav)来确认哪个文件是首次出现。
将候选要点按目标文件分组,后续按文件逐一处理。
第三步:读取目标 excalidraw,收集已有标注
读取目标 .excalidraw 文件(分段,每次 limit=200 行),专门提取所有现有标注文字("strokeColor": "#ef4444" 且 type: "text" 的元素)。
把已有标注文字汇成一张清单,并区分两类:
- 有来源标注:文字末尾带
(模块X) 括号,来自之前的路径 C 执行
- 线框原生标注:文字末尾不带
(模块X) 括号,来自建图时内嵌的线框备注(路径 B 特例)
例如:
已有标注(有来源):
- 进度达标 / 自动置顶到分组首位 (模块一)
已有标注(线框原生,无来源):
- 红点穿透显示 / 由"有完成未领取任务"驱动
- 单条件任务
- 多条件任务, OR关系
- [?] 可选, 悬停显示赛制说明
这个分类在第四步去重时有不同处理逻辑,必须在这一步记录清楚。
同时,还需要建立气泡落点表——即所有已有气泡(bub-*-ring)的落点坐标,以便第四步判断「同一界面元素是否已有气泡」。方法:Grep "id": "bub- 找出所有气泡元素,提取 x、y 值,建立如下映射:
气泡落点表(当前文件已有):
编号 元素id前缀 cx cy 覆盖元素
① bub-s2-1 428 759 s2-announce-edit(公告编辑按钮)
② bub-s2-2 293 826 s2-btn-exit(退出按钮)
③ bub-s2-3 23 753 s2-club-logo(俱乐部左上角区)
⑤ bub-s2-5 91 905 s2-rk1-badge(段位徽章区)
…
此表是第四步「目标元素是否已有气泡」的唯一判断依据,缺少它会导致新要点被误判为「新增」而非「追加行」。
第四步:与新标注对比去重,发现冲突时询问用户
对每条候选新标注,先做落点占用检查,再做内容去重:
落点占用检查(必须先于内容检查): 确认新标注的目标界面元素在气泡落点表中是否已有记录。判断方法是对比目标元素的 id 或坐标是否与某个已有气泡的覆盖范围重叠(气泡通常放在元素内或紧邻元素边缘,y 误差在 ±30px 以内即视为同一元素)。只要落点已被占用,无论内容是否重叠,都应归入「追加行」而非「新增」。
内容去重在落点检查之后,根据已有标注的类型(有来源 / 线框原生)区别处理:
- 完全覆盖 + 已有来源 + 来源相同:内容和来源均完全重合 → 真正跳过,不做任何操作
- 完全覆盖 + 已有来源 + 来源不同:内容重合但来源是另一个模块 → 不跳过,将已有标注末尾的
(模块X) 改为 (模块X/Y),原地回写;这样两个模块都能被溯源
- 完全覆盖 + 线框原生:新标注含义与某条线框原生标注完全重合 → 不跳过,在该线框原生标注文字末尾补充来源
(模块X),原地回写,使其升级为有来源标注;不新增独立标注条目
- 明确新增,但目标元素已有气泡(落点占用检查命中):新要点与已有标注内容不重叠,但指向同一个界面元素(即已有气泡的落点)→ 不新增气泡,直接在已有标注的
anno-* 文字末尾追加新行,格式为 \n续行内容 (模块Y);气泡 id 和编号不变,不额外新增椭圆和数字元素。追加完成后,必须执行第四点八步(布局重排检查),将后续所有 anno-* 的 y 坐标下移相应偏移量,否则后续条目与新增行重叠。
- 明确新增,目标元素尚无气泡:新增完整气泡 + 说明文字条目
- 语义相近/有重叠:新标注与已有标注部分重叠或措辞相似但不完全相同 → 这是冲突,需要用户介入
冲突时,使用 AskUserQuestion 工具让用户选择:
问题示例:
"新标注「奖励预览最多显示3个,超出可滚动」与已有标注「奖励预览: 图标+数量」
指向同一个奖励图标元素,内容有部分重叠。请选择处理方式:"
选项:
A. 合并为一条(整合两段文字内容,来源写 (模块X/Y) 包含双方)
B. 保留已有标注(在已有标注末尾追加新来源,变为 (模块X/Y)),跳过新标注
C. 两条都保留(已有标注末尾追加新来源,新标注独立添加带自身来源)
D. 用新标注替换旧标注(仅保留新来源)
溯源原则在冲突处理中同样适用:无论选择哪个选项,只要涉及"保留已有标注",就必须在其文字末尾追加新的来源模块,确保每一条曾经关联过的模块都被记录——不能因为选了 B 就让新模块的来源消失。
对每个冲突单独询问,不要批量跳过。
执行前先列出完整的对比表,再动手写入。 这样用户可以在修改前做最后确认。
对比表格式
必须包含「来源」列,确保每条标注可追溯到原始模块。状态列使用以下标识:
- ✅ 新增:待写入新标注条目(目标元素尚无气泡)
- ➕ 追加行:目标元素已有气泡,在已有
anno-* 文字末尾追加新行,不新增气泡元素
- ⬆ 升级:已有线框原生标注与新标注完全重合,原地补充第一个来源括号
- ⬆ 追加来源:已有有来源标注与新标注完全重合,但来源不同,原地在括号内追加新模块编号
- ⏭ 跳过:已有有来源标注已完全覆盖(内容 + 来源均相同),无需任何操作
- ⚠️ 冲突:与已有标注语义重叠,待用户确认
| 要点 | 状态 | 来源 | 说明 |
|------|------|------|------|
| 分组固定顺序每日→每周→挑战 | ✅ 新增 | 模块一 | 指向分隔头 |
| 单条件/多条件目标描述 | ⬆ 升级 | 模块二 | 原生标注「单条件任务」末尾补充 (模块二) |
| 进度达标自动置顶 | ⬆ 追加来源 | 模块三 | 已有「进度达标… (模块一)」,末尾改为 (模块一/三) |
| 奖励领取规则 | ⏭ 跳过 | 模块五 | 已有「奖励发放 (模块五)」,来源相同,无需操作 |
| 奖励预览最多3个 | ⚠️ 冲突 | 模块三 | 与已有原生标注「奖励预览: 图标+数量」重叠,待用户确认 |
| 发放失败Toast提示 | ✅ 新增 | 模块五 | 指向领取按钮 |
第四点五步:落点合规检查(写入前必做)
在动手写入坐标之前,对对比表中每一条「✅ 新增」要点做一次落点检查,确认目标元素的元素类型:
合法落点(可打标注):
- 界面框(
s\d+-frame)
- 界面内的交互控件:按钮、任务条目、分组头、弹窗内容区、奖励图标、状态文字等
- 界面内的固定导航/标题区
禁止落点(不可打标注):
- 流程说明性文本框:游戏中虚线框(
strokeStyle: "dashed" 的外部步骤框)、服务器后台操作框、结果绿色框(backgroundColor: "#b2f2bb" 类)
- 步骤箭头及其标签文字(
type: "arrow" 及 containerId 指向箭头的文字)
- 标题文字(
id: "title")
- 视口控制/边界框(
id: "bound")
检查方法:在 .excalidraw 文件中找到目标元素的 id,确认其 type 和 strokeStyle,以及它是否在某个 s\d+-frame 的 y 坐标范围内。若目标元素落在「禁止落点」类别,需将该标注改打到同一步骤中最近的真实界面元素上(如该步骤的任务界面框顶边、或界面内的分组头),或将该规则合并进同步骤其他已有标注的文字里。
只有通过落点检查的要点才进入第五步写入。
第四点八步:追加行后的布局重排检查(有追加行操作时必做)
凡是对比表中出现 ➕ 追加行 状态的条目,在追加文字后,必须立即执行以下重排检查,再继续写入其他标注或导出 SVG。
检查对象:同一步骤内(同一界面区间的 y 坐标范围内),位于被追加 anno-* 之后的所有 anno-* 文字元素。
计算偏移量:
追加了 N 行新文字 → 所有后续 anno-* 的 y 值各需 +N×20
操作步骤:
- 确认本次追加了几行(
\n 数量 = 新增行数 N)
- 在
.excalidraw 文件中找出同一步骤内该 anno-* 之后所有 anno-* 元素的 id 列表
- 用 Edit 逐条修改其
"y" 值,或用 PowerShell 批量替换:
# 示例:追加了 1 行,偏移 +20,目标步骤 y 范围 700~900
# 手动逐条 Edit 对应元素的 y 字段更安全,避免误改其他步骤
- 检查
bound.height 是否仍能覆盖最低标注元素(最低 anno-* y + 文字高度 + 40px 冗余)
注意:只需调整同一步骤区间内该条目后面的 anno-*,不同步骤之间相互独立,不受影响。
第五步:计算坐标,添加标注
默认始终使用序号气泡方案。连线标注在实际使用中容易与界面元素交叉、遮挡,且无论标注数量多少都存在这个问题。只有当用户明确说"使用连线标注"时,才切换到方案 A。
起始 y 坐标计算(防重叠):序号列表必须从步骤标注文字底边 + 12px 开始,不能直接用界面框顶边 y 加一个小值。计算方法:
- 找到该步骤对应的
step-sX-summary 元素的 y 值和行数
- 行数 × 20px = 步骤标注文字高度(fontSize=14,每行约 20px)
- desc 起始 y = step-sX-summary.y + 行数×20 + 12
- 例:步骤标注 y=2000,共2行 → desc 起始 y = 2000 + 40 + 12 = 2052
方案 A:连线标注(仅当用户明确指定时使用)
每条标注 = 红点(在元素原始位置)→ 红色虚线 → 右侧文字。
按「标注三件套」规范放置(见上方「标注(红色虚线 + 红点标记)」章节)。
标注文字格式:说明内容后追加来源模块,例如:
"text": "进度达标\n自动置顶到分组首位 (模块一)"
方案 B:序号气泡(默认方案,始终使用)
彻底无连线,视觉干净,不受标注数量和连接点密度影响。
编号作用域:每个界面独立从 1 开始。同一张流转图里有多个界面时,每个界面的气泡都从 1 重新编起——读者凭右侧说明列表的位置(y 坐标区间)就能判断属于哪个界面,全图共享序号反而会让单个界面出现 ⑪、⑫ 这样不直观的大号数字,也会让气泡文字更难放进 16px 圆圈。
元素 id 命名:为避免同一文件内 id 冲突,用界面前缀区分。例如第 2 个界面的气泡:bub-s2-1-ring、bub-s2-1-num;说明文字:anno-s2-1。第 1 个界面省略前缀,直接用 bub1-ring / desc1 即可。
气泡元素(放在目标元素旁,直径 16px,白底红边):
{"type":"ellipse","id":"bub-s2-1-ring","x":<cx-8>,"y":<cy-8>,"width":16,"height":16,
"backgroundColor":"#ffffff","fillStyle":"solid",
"strokeColor":"#ef4444","strokeWidth":2,"roughness":1},
{"type":"text","id":"bub-s2-1-num","x":<cx-4>,"y":<cy-7>,
"text":"1","fontSize":11,"strokeColor":"#ef4444","fontFamily":1}
右侧说明列表(x=950,从各界面 desc 起始 y 开始,行间距 18px,多行块之间间距 8px):
说明文字第一行以 ① 说明内容 (模块X) 格式书写,序号用 Unicode 圆圈数字(①②③…⑩),来源模块括号放在第一行末尾:
{"type":"text","id":"anno-s2-1","x":950,"y":<起始y>,
"text":"① 说明文字第一行 (模块X)","fontSize":14,"strokeColor":"#ef4444","fontFamily":1},
{"type":"text","id":"anno-s2-1b","x":950,"y":<起始y+18>,
"text":" 续行内容(2空格缩进)","fontSize":14,"strokeColor":"#ef4444","fontFamily":1}
坐标分配原则:
- 气泡圆心 cx,cy = 目标元素的原始连接点位置(不要移动)
- 说明列表 y 从上到下均匀分配,无需与气泡 y 对齐
- 每个界面的说明列表紧接在该界面步骤标注底边 + 12px 处开始(见「起始 y 坐标计算(防重叠)」)
所有标注元素统一追加到 .excalidraw 文件的 elements 数组末尾,不要插入中间。这有两个好处:一是确保 z 轴层级最高、不被其他元素遮挡;二是便于日后 Grep 定位——想找或修改标注时,直接跳到文件尾部,不必全文搜索。
超过 5 条标注时,用 PowerShell 脚本批量写入(注意无 BOM);少于 5 条直接用 Edit 工具追加到数组末尾。
PowerShell 批量注入的唯一安全写法:
注入点是 elements 数组的 ] 之前(即 ], 换行后接 "appState" 的位置)。有两个常见错误写法必须避开:
- ❌
$content -replace '(\s*\}\s*\]\s*,\s*"appState")', ... — 正则会把末尾元素的 } 吃掉
- ❌
$content.LastIndexOf('}') — 找到的是 "files": {} 里的 },插入后变成两个 JSON 根对象
正确做法:定位 ], 紧接 "appState" 的位置,在 ] 之前插入:
# 读取文件
$content = [System.IO.File]::ReadAllText($path, [System.Text.Encoding]::UTF8)
# 把新元素片段拼成字符串(以逗号开头,不含末尾 ])
$newElements = @'
,
{ ...第一个新元素... },
{ ...最后一个新元素... }
'@
# 安全注入:定位 elements 数组的 ],在其前插入新元素
# .excalidraw 文件结构固定:elements 数组之后紧接 ],\n "appState"
$insertMarker = "],`n `"appState`"" # 匹配 elements 数组结尾
$markerPos = $content.IndexOf($insertMarker) # 找到 ] 的位置
if ($markerPos -lt 0) { Write-Host "ERROR: marker not found"; exit }
# 在 ] 之前插入(markerPos 就是 ] 的位置)
$content = $content.Substring(0, $markerPos) + $newElements + $content.Substring($markerPos)
# 验证
if ($content -match '"id": "最后一个新元素的id"') { Write-Host "OK" } else { Write-Host "ERROR" }
if ($content -match '"[a-z]+":(\d+)"') { Write-Host "WARNING: stray quote" }
# 写出无 BOM
$utf8NoBom = New-Object System.Text.UTF8Encoding $false
[System.IO.File]::WriteAllText($path, $content, $utf8NoBom)
写入后:检查 bound 是否够宽。右侧说明列表全部写入后,用「SVG 边界问题」章节的公式算一遍最长标注行的渲染宽度,确认 bound.x + bound.width ≥ 950 + W_max + 40。不够宽就直接 Edit 更新 bound.width,否则标注文字会被 SVG 导出时裁掉。
子功能:添加步骤标注到流转图
当用户希望对流转图里的每个步骤加一句整体介绍(而非标注具体界面规则)时,使用本流程。
这类标注的作用是让读者扫一眼就能知道"这一步在做什么"——适合给其他同事看图时快速理解流程,或者在把图作为文档资产使用前先做一遍基础注释。
与路径 C 的协作顺序:若同时需要步骤标注(路径 D)和模块规则标注(路径 C),先做 D 再做 C。路径 C 的序号列表会追加在步骤标注之后,不需要移动已有内容。
第一步:读取设计文档,建立界面出现顺序表
读取用户提供的设计文档(如 PRD 的用户旅程章节),扫描所有流转图文件,按文档中出现顺序记录每种界面的首次出现位置,同时记录以下两个属性:
- 界面类型:
全屏(独占整个视口的主界面)或 弹窗(叠加在主界面上的覆盖层)
- 是否复用:该界面是否属于系统内多处共用的通用界面(如「通用恭喜你获得界面」在任务、活动、商城等多处均使用),而非本流程独有界面
建立如下映射表(在脑内或临时文件中维护,不需要输出给用户):
界面名称 类型 复用 首次出现于
主导航界面 全屏 否 uj01-flow(步骤 1)
核心功能列表界面 全屏 否 uj01-flow(步骤 2)
通用奖励确认界面 全屏 复用 uj01-flow(步骤 4)
详情预览弹窗 弹窗 否 uj02-flow(步骤 2)
…
以上为格式示例,实际内容从设计文档读取,不要直接使用这里的名称。
通过 Grep 搜索 .excalidraw 文件中界面框的 id(如 s1-frame、s2-frame 等)来判断首次出现文件,而不是靠记忆。
第二步:理解每个步骤的含义
逐一读取目标流转图的 .excalidraw 文件,识别其中的步骤分隔结构:
- 流转图通常以步骤编号命名界面框 id(如
s1-frame、s2-frame)
- 每个步骤之间有步骤箭头(带标签的垂直箭头,如「点击任务入口」「点击领取」)
- 步骤内可能有界面框、游戏中虚线框、结果绿色框等
结合设计文档的用户旅程描述,为每个步骤写一句步骤概述,遵循以下规则:
-
一句话,≤25字,说清楚这一步"玩家在做什么"或"系统在做什么"
-
用主语 + 动作格式,例如:「玩家从大厅入口进入任务界面」「系统展示可领取任务并高亮领取按钮」
-
若步骤包含"首次出现的界面",在概述后换行追加界面角色说明,格式为:
[类型标签] 界面名称:角色说明(≤20字)
类型标签由两个维度用 | 拼接,置于行首,用方括号标注,与后续内容之间加一个空格:
- 第一维度(是否复用):
复用 或省略(非复用时不写)
- 第二维度(界面形态):
全屏(独占视口的主界面)或 弹窗(叠加在主界面上的覆盖层)
- 拼接规则:非复用直接写形态,
[全屏] 或 [弹窗];复用则写 [复用|全屏] 或 [复用|弹窗]
示例:
[全屏] 任务界面:任务系统主入口,三类任务进度和领取操作均在此完成
[弹窗] 任务预览弹窗:链式任务专属,展示所有阶段目标与进度
[复用|全屏] 大厅主界面:游戏主导航界面,各功能模块入口聚合于此
[复用|弹窗] 通用恭喜你获得界面:全局奖励展示弹窗,所有奖励发放场景复用
不确定时必须询问:若无法从设计文档或图的结构中确定某界面的「全屏/弹窗」或「是否复用」,不要自行猜测,使用 AskUserQuestion 工具向用户提问后再继续。
-
不加数字/字母前缀,不加括号,纯文字,与后续的序号标注(① ② ③)在视觉上明显区分
第三步:确定每个步骤标注的放置位置
步骤标注放在每个步骤标注区域的最左上角——即该步骤界面框右侧的标注列最顶端,位于序号列表(如果有的话)的上方。
坐标规则:
- x 坐标:与该步骤序号列表的起始 x 保持一致(通常是
x=950,即界面框右侧留白区域)
- y 坐标:界面框顶边 y 值,或该步骤标注区已有内容上方,留 8px 间距
- 若该步骤暂时没有其他标注(路径 C 尚未执行),步骤标注直接放在标注区起始位置即可
第四步:写入标注元素
步骤标注与其他所有标注(红点、虚线、模块规则说明)使用相同的红色,遵循全局用色规范。
步骤标注元素格式:
{
"type": "text",
"id": "step-N-summary",
"x": 950,
"y": <界面框顶边 y>,
"text": "<步骤概述>\n<界面角色说明(仅首次出现时追加)>",
"fontSize": 14,
"strokeColor": "#ef4444",
"fontFamily": 1
}
strokeColor: "#ef4444" 与所有其他标注元素保持一致
- 若步骤只有概述(界面非首次出现),
text 字段只写一行,不加 \n
- 若步骤包含首次出现的界面,
text 写两行:第一行步骤概述,第二行界面角色说明
写入顺序:步骤标注同样追加到 .excalidraw 文件 elements 数组末尾。如果当前文件已有红色模块标注(路径 C 已执行过),步骤标注追加在红色标注之后,确保 z 轴层级最高。
第五步:重新导出 SVG
所有步骤标注写入完毕后,重新导出 SVG:
npx excalidraw-cli create "<path>/file.excalidraw" --no-banner --no-checkpoint -o "<path>/file-tmp.excalidraw"
npx @moona3k/excalidraw-export "<path>/file-tmp.excalidraw" --svg -o "<path>/file.svg"
Remove-Item "<path>/file-tmp.excalidraw" -Force
步骤标注示例
以下为格式示例(界面名称为虚构占位,实际内容从设计文档读取):
步骤 1(主导航界面,首次出现,全屏,非复用):
text = "玩家进入主界面,发现功能入口有未读提示\n[全屏] 主导航界面:游戏主入口,各功能模块入口聚合于此"
步骤 2(核心功能列表,首次出现,全屏,非复用):
text = "玩家打开功能列表,查看当前可执行的目标\n[全屏] 功能列表界面:本系统主界面,状态查看和操作均在此完成"
步骤 4(通用奖励确认,首次出现,全屏,复用):
text = "玩家点击领取,弹出奖励确认界面\n[复用|全屏] 通用奖励确认界面:全局奖励展示界面,所有奖励发放场景复用"
步骤(详情预览弹窗,首次出现,弹窗,非复用):
text = "玩家点击查看图标,弹出详情预览弹窗\n[弹窗] 详情预览弹窗:本功能专属,展示全部阶段信息"
步骤(大厅主界面,首次出现,全屏,复用):
text = "玩家登录大厅,发现任务入口有红点提示\n[复用|全屏] 大厅主界面:游戏主导航界面,各功能模块入口聚合于此"
步骤(核心功能列表,非首次出现,只写概述):
text = "玩家完成目标后返回,状态更新为可领取"
与路径 C 的排版关系
执行完步骤标注(路径 D)后,如果还需要追加模块规则标注(路径 C),不需要移动已有的步骤标注,直接在其下方继续追加序号气泡和说明列表即可。最终每个步骤标注区域的层次结构应如下:
[步骤概述(红色,无前缀)] ← 路径 D 写入
[[类型标签] 界面角色说明(仅首次)] ← 路径 D 写入(可选行)
① 规则要点一 (模块X) ← 路径 C 写入
② 规则要点二 (模块X) ← 路径 C 写入
…
路径 C 的序号列表 y 坐标需从步骤标注文字的底边 + 12px 开始,避免与灰色文字重叠。步骤标注文字高度估算:每行约 fontSize × 1.4(fontSize=14 时约 20px/行),多行文字总高度 = 行数 × 20px。