# Excalidraw Diagram

> 使用 excalidraw-cli 和 @moona3k/excalidraw-export 绘制手绘风格示意图并导出为 SVG。当用户要求绘制线框图、界面流转图、架构图、流程图，或提到 excalidraw、手绘风格图、wireframe、示意图时，主动使用此 skill。也适用于需要将已有 .excalidraw 文件转为 SVG 的场景。

- Skill: `nuonuo0518/excalidraw-diagram` (Agent Skill)
- Install (CLI): `npx skillmds@latest add nuonuo0518/excalidraw-diagram`
- Raw SKILL.md: https://api.skillmd.com/api/skills/nuonuo0518/excalidraw-diagram/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: nuonuo0518 (https://skillmd.com/u/nuonuo0518)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/nuonuo0518/excalidraw-diagram

---


# Excalidraw 手绘风示意图

通过 excalidraw-cli 生成 `.excalidraw` 文件，再用 @moona3k/excalidraw-export 导出为内嵌 Virgil 手写字体的手绘风 SVG。全程 npx，零全局安装。

---

## 意图识别与路由

**收到任务后，先判断属于哪条路径，再跳转到对应章节执行。**

| 路径 | 用户意图特征 | 跳转章节 |
|------|-------------|---------|
| **A. 修改已有图** | 提到改、更新、修复、保持一致、重绘某个已有 svg/excalidraw | → [场景 A：修改已有 .excalidraw 文件](#场景-a修改已有-excalidraw-文件最小改动) |
| **B. 从头创建新图** | 要画一张新图，无已有源文件可参考 | → [场景 B：从头创建新图](#场景-b从头创建新图) |
| **C. 将功能模块标注到图里** | 提到把某个模块/需求/规则"标注到"图里，或将文档内容映射到 svg 标注；包括同时标注多个模块的场景 | → [子功能：将功能模块规则标注到 SVG 流转图](#子功能将功能模块规则标注到-svg-流转图) |
| **D. 添加步骤标注** | 提到"步骤标注"、"整体标注"、"步骤概述"、"每个步骤加一句说明"、"给每步加介绍" | → [子功能：添加步骤标注到流转图](#子功能添加步骤标注到流转图) |

四条路径互斥，每次只走一条。如果意图不清晰，直接问用户要做 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` 后，走正常的两步导出：

```powershell
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

```powershell
npx excalidraw-cli create "<path>/_tmp.json" --no-banner --no-checkpoint -o "<path>/output.excalidraw"
```

#### 3. 导出 SVG

```powershell
npx @moona3k/excalidraw-export "<path>/output.excalidraw" --svg -o "<path>/output.svg"
```

#### 4. 清理临时文件

```powershell
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 手写体）

### 元素类型

**矩形**
```json
{"type":"rectangle","id":"r1","x":100,"y":100,"width":200,"height":100}
```
填充色：`"backgroundColor":"#a5d8ff","fillStyle":"solid"`

**椭圆**
```json
{"type":"ellipse","id":"e1","x":100,"y":100,"width":150,"height":150}
```

**菱形**
```json
{"type":"diamond","id":"d1","x":100,"y":100,"width":150,"height":150}
```

**带标签的形状（推荐）**

在矩形/椭圆/菱形/箭头上加 `label` 字段，CLI 会自动展开为居中文本：
```json
{"type":"rectangle","id":"r1","x":100,"y":100,"width":200,"height":80,
 "label":{"text":"Hello","fontSize":20}}
```
label 属性：`text`（必填）、`fontSize`（默认20）、`fontFamily`、`strokeColor`

**独立文本**（用于标题、标注）
```json
{"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

**箭头**
```json
{"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":"连接文字"}`

**箭头绑定**
```json
"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 打开时的初始视图）
```json
{"type":"cameraUpdate","width":800,"height":600,"x":0,"y":0}
```
必须用 4:3 比例：400x300、600x450、800x600、1200x900、1600x1200

**删除元素**（伪元素）
```json
{"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 更新对应字段：

```json
{"type":"rectangle","id":"bound","x":-20,"y":-80,"width":1380,"height":2820,
 "strokeWidth":1,"strokeColor":"#adb5bd","strokeStyle":"dashed","opacity":30}
```

### 标注（红色虚线 + 红点标记）

标注由三个元素组成：**红点**（标注起点）→ **红色虚线**（连接线）→ **红色文字**（说明）。红点让读者一眼定位到被标注的目标元素。

放置标注时需要用「Virgil 字体宽度计算」公式精确算出目标文字末尾位置，再在末尾放红点。不要凭感觉估计位置，Virgil 手写字体比常规字体窄很多，不算会严重偏移。

**完整标注三件套：**
```json
// 1. 红点（8x8 椭圆）- 放在目标文字末尾
{"type":"ellipse","id":"an-dot","x":<文字末尾x>,"y":<文字y + (fontSize-8)/2>,
 "width":8,"height":8,
 "backgroundColor":"#ef4444","fillStyle":"solid","strokeColor":"#ef4444","strokeWidth":1},

// 2. 红色虚线 - 从红点右侧拉到标注文字
{"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"},

// 3. 红色标注文字 - 放在虚线末端
{"type":"text","id":"an-text","x":<标注区起始x>,"y":<虚线y - 7>,
 "text":"标注说明","fontSize":14,"strokeColor":"#ef4444"}
```

**红点定位计算步骤：**
1. 用 Virgil 字体宽度公式算出目标文字的渲染宽度 W
2. 红点 x = 文字起始x + W + 5（5px 间距）
3. 红点 y = 文字y + (fontSize - 8) / 2（垂直居中于文字行）
4. 虚线起点 x = 红点x + 8（红点右侧）
5. 虚线 y = 红点y + 4（红点垂直中心）
6. 标注文字 y = 虚线y - 7（fontSize 14 时垂直居中于虚线）

确保标注文字完全在边界框内，不与界面元素重叠。

**标注层级规则：** 所有标注元素（红点、红色虚线、红色标注文字）必须放在 JSON 数组最末尾，确保 z 轴层级最高。标注需要始终可见，不能被形状、箭头或其他元素遮挡。即使标注指向的是图中较早出现的元素，标注三件套也统一放在数组最后。

### 说明性标签（如「复用原有界面」）

流转图中有时需要对整个界面加文字标签说明其性质，例如「复用原有界面」。这类标签与标注三件套的用色规范一致，同时用虚线框而非实线框，以避免与界面内的功能按钮混淆：

```json
{
  "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 错误。

```powershell
# 正确：无 BOM
$utf8NoBom = New-Object System.Text.UTF8Encoding $false
[System.IO.File]::WriteAllText($path, $content, $utf8NoBom)

# 错误：默认的 Set-Content / Out-File 会带 BOM
```

如果已经写入了带 BOM 的文件，可以这样修复：
```powershell
$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`。写完后用正则快速自检：

```powershell
# 检测是否存在 "数字字段名":数字" 的错误模式
if ($content -match '"[a-z]+":(\d+)"') { Write-Host "WARNING: stray quote after number detected" }
```

如果已出错，批量修复：
```powershell
$content = [System.Text.RegularExpressions.Regex]::Replace($content, '"([xy])":(\d+)"', '"$1":$2')
```

### 长 JSON 必须用文件输入

JSON 超过几百字符就会导致命令行溢出。始终写到临时 JSON 文件再传给 CLI。

### 暗色模式

第一个元素放一个巨大的深色矩形作为背景：
```json
{"type":"rectangle","id":"darkbg","x":-4000,"y":-3000,"width":10000,"height":7500,
 "backgroundColor":"#1e1e2e","fillStyle":"solid","strokeColor":"transparent","strokeWidth":0}
```
暗色模式文字色：主要 `#e5e5e5`，次要 `#a0a0a0`

## 界面线框图最佳实践

基于 PC 端游界面设计的经验总结：

1. **统一长宽比**：同一项目所有界面用相同的外框尺寸（如 1100x660 或 900x540）
2. **弹窗界面**：保持外框不变，加半透明遮罩 `opacity:50`，弹窗在框内居中
3. **非焦点区域模糊化**：用低透明度灰色块 `"backgroundColor":"#e9ecef","opacity":40` 覆盖
4. **焦点区域高亮**：关键入口用蓝色边框 + 浅蓝填充突出
5. **不在界面图上加标注**：独立界面图保持纯净，标注放在流转图中
6. **奖励/道具图标**：用彩色小方块 + label 表示，不用纯文字
   - 金币: `#ffd8a8`（浅橙），碎片: `#d0bfff`（浅紫），礼包: `#b2f2bb`（浅绿）

## 流转图最佳实践

1. **界面保持完整**：流转图中每个步骤的界面和独立界面图一模一样，不缩小、不省略
2. **复用已有界面布局（逐元素保真）**：绘制流转图前，先检查保存目录中是否已存在对应界面的独立示意图（如 `01-俱乐部推荐页.excalidraw`）。如果已有，流转图中该步骤的界面必须**逐元素对照**已有图复刻，而不是凭记忆"大概画一个类似的"。

   **核心原则：有差异的部分允许修改，其他部分必须一致。**

   所谓"有差异的部分"是指：不同视角/状态确实会导致变化的内容——比如按钮文字（"申请"→"取消申请"）、Tab 高亮切换、列表行数据填充不同、某些按钮在该视角下不可见等。这些改动是合理的。

   但除了这些有明确理由的差异外，所有其他元素都必须严格保持和原图一致：
   - **元素拆分方式一致**：原图把"人数"和"招募状态"拆成两个 text 元素（分别用不同颜色），流转图就不能合并成一行灰色文字
   - **文字内容格式一致**：原图文字是 `* 0  排名--` 就不能简化成 `*0`；原图有副标题"发展俱乐部"就不能省略
   - **颜色和样式一致**：每个元素的 `strokeColor`、`backgroundColor`、`fontSize` 都必须与原图一致（按缩放比例调整 fontSize）
   - **元素数量一致**：不能为了省事跳过或合并元素

   **做法**：读取已有 `.excalidraw` 文件（分段 limit=200），逐一提取所有元素的 id、type、坐标、文字内容、颜色，建立完整元素清单。然后按流转图的缩放比例（通常 900/1200 = 0.75）等比缩放坐标和尺寸，fontSize 也按比例缩小，将清单中的每个元素都写入流转图 JSON。只修改有差异理由的部分，不要跳过任何元素，不要合并任何元素，不要改写任何文字格式
3. **箭头连接**：步骤之间用带标签的垂直箭头连接，标签说明触发动作
4. **状态变化高亮**：前后两个界面只有数据/状态不同时，变化部分用黄色背景高亮
5. **游戏中/外部操作**：用虚线框 `strokeStyle:"dashed"` 表示非本系统界面的操作
6. **结果框**：最终结果用绿色背景框总结

---

## 子功能：将功能模块规则标注到 SVG 流转图

当用户提供一份功能模块说明（如 PRD 中的模块一、模块二），希望将其中的规则要点以红点标注的形式标到对应 SVG 流转图中时，使用以下流程。

> **溯源原则（全局约束）**：所有标注内容必须能在原始文档中找到对应依据，**禁止生造**。不得凭理解推断、合理延伸或自行补充文档未明确说明的规则。标注文字可以压缩改写，但含义必须直接来自原文。每条标注都必须附 `(模块X)` 括号作为溯源凭据，让读者可以按编号回原文核实——这不是装饰，是验证入口，缺失视为不合格标注。

> **同时标注多个模块时**：不要逐模块分别走流程——这会导致同一个文件被读写多次，每次写入都在上一次的基础上累积坐标偏移，最终难以排查。正确做法：**先把所有模块的要点汇总到同一张表，再按目标文件分组，每个文件只读写一次**。具体节拍：
> 1. 读取所有模块文档，汇总候选要点（第一步）
> 2. 按目标文件分组，建好对比表（第二至四步）
> 3. 对每个目标文件：一次性读取最新状态 → 一次性写入当次所有新标注 → 导出 SVG
> 4. 不同文件之间没有依赖，可以并行处理；同一个文件内的所有修改必须合批完成，不得分多次写入

### 第一步：理解模块内容，提炼标注要点

读取所有待标注的功能模块文档，将全部模块的要点汇总到同一张候选列表里，**每条要点同时记录其来源模块**。

提炼原则：
- 聚焦**界面可见的行为规则**（排序、显示格式、状态切换、数量限制等），略去纯后端逻辑
- **标注只打在界面元素上**（任务条目、按钮、分组头、弹窗内容等），不要标注流程图中连接步骤用的说明性文本框（游戏中虚线框、结果绿色框、步骤箭头标签等）。前者是产品规则的载体，后者是流程叙述的辅助，混在一起会让标注失去焦点
- 每条标注文字尽量简洁（≤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
```

**操作步骤**：

1. 确认本次追加了几行（`\n` 数量 = 新增行数 N）
2. 在 `.excalidraw` 文件中找出**同一步骤内**该 `anno-*` 之后所有 `anno-*` 元素的 id 列表
3. 用 Edit 逐条修改其 `"y"` 值，或用 PowerShell 批量替换：
   ```powershell
   # 示例：追加了 1 行，偏移 +20，目标步骤 y 范围 700~900
   # 手动逐条 Edit 对应元素的 y 字段更安全，避免误改其他步骤
   ```
4. 检查 `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，白底红边）：
```json
{"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 圆圈数字（①②③…⑩），来源模块括号放在**第一行末尾**：
```json
{"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"` 的位置，在 `]` 之前插入**：

```powershell
# 读取文件
$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 尚未执行），步骤标注直接放在标注区起始位置即可

---

### 第四步：写入标注元素

步骤标注与其他所有标注（红点、虚线、模块规则说明）使用**相同的红色**，遵循全局用色规范。

**步骤标注元素格式：**

```json
{
  "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：

```powershell
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。

