Skip to content

Commit 33135ed

Browse files
committed
docs(v0.13.8): 知识体系画布设计规格(React Flow 节点画布)
1 parent 5e0bbfb commit 33135ed

1 file changed

Lines changed: 282 additions & 0 deletions

File tree

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
# v0.13.8 设计规格:知识体系画布(React Flow 节点式无限画布)
2+
3+
> 状态:**已批准**(2026-08-24,用户裁决方案2——React Flow 标注画布)
4+
> 关联:[v0.13 系列文档](../../versions/v0.13.md) · [ADR-024](../../adr/ADR-024-knowledge-system-layer.md) · [v0.13.7 上手路径](../../archive/2026-08-24/2026-08-24-v0.13.7-knowledge-system-onboarding-design.md)([ ] 已归档)
5+
> 范围:v0.13.8——知识体系画布视图。**不含** v0.13.4 审计 UI、v0.13.5 书籍、REQ-029 力导向图。
6+
7+
## 一、问题定义
8+
9+
知识体系层当前只提供**树视图**一种结构展示方式(KnowledgeTreeView),节点以递归缩进列表排列。随着体系节点增多(> 10 个问题 + 概念/模型引用),树视图的局限性开始显现:
10+
11+
1. **空间限制**:一级节点超过 5 个后纵向滚动沉重;子节点折叠在一层里看不见。
12+
2. **概念/模型游离**:概念和模型不在树中——它们在独立标签页里,用户无法在"看结构"的同时看到哪些节点关联了哪些概念。
13+
3. **整体感缺失**:树视图只展示问题轴,概念/模型/决策日志各占独立标签页——用户无法一眼看到体系全貌。
14+
4. **无法自由组织**:用户想按自己的认知习惯排布节点(重要的放中间、相关的放近处),树视图不允许——父子关系决定位置。
15+
16+
**设计目标**:给用户一张可以自由排布知识节点的无限画布——不取代树视图,而是提供互补的"鸟瞰"视角。
17+
18+
## 二、设计约束(核心纪律)
19+
20+
本功能受且仅受以下纪律约束:
21+
22+
1. **不是图可视化(REQ-029 P3 维持)**:画布不是力导向图。节点位置由用户拖拽决定,首次打开时以辐射布局计算初始位置——算法只在首次生效,用户拖走位置就不再改变。
23+
2. **不取代树视图**:画布与树视图共存。用户通过标签页栏的「画布」项或树视图内的浮钮切换。
24+
3. **零破坏既有数据**:canvas_x/y 列无值不影响树视图和既有命令。
25+
4. **连线只反映既有关系**:画布连线源于 knowledge_nodes.parent_id 和 knowledge_links。用户不能画线即建引用。
26+
5. **不做全屏白板模式**:画布在体系页中栏渲染,不是独立页面。
27+
28+
## 三、技术选型
29+
30+
**React Flow v12**(@xyflow/react)
31+
32+
| 维度 | React Flow | 理由 |
33+
|------|-----------|------|
34+
| 无限画布 | pan/zoom 开箱即用 | 零自研 |
35+
| 自定义节点 | React 组件 | 可复用现有知识卡片 UI |
36+
| 连线 | smoothstep/bezier | parent_id 映射为边 |
37+
| 性能 | 虚拟化渲染(视口外不渲染) | 百节点级流畅 |
38+
| 框选/拖拽 | 内置 | 用户排布体验 |
39+
| minimap | 内置 MiniMap 组件 | 全局导航 |
40+
| 控制面板 | 内置 Controls 组件 | 缩放/适配/锁定 |
41+
| 包体积 | ~95KB gzip | 可接受 |
42+
43+
对比方案:
44+
- 自研 DOM 画布:无虚拟化,~50 节点后抖。弃用。
45+
- 自研 Canvas:千节点级但 React 组件桥接复杂。弃用(成本高且收益过剩——知识体系节点数上限预计 < 200)。
46+
47+
## 四、架构设计
48+
49+
### 4.1 页面结构
50+
51+
```
52+
KnowledgePage
53+
├── 左栏:体系列表(不变)
54+
├── 中栏:
55+
│ ├── [树视图] 标签(不变)—— KnowledgeTreeView
56+
│ │ └── 树视图顶部新增浮动「画布」按钮(切换 middleView)
57+
│ ├── [概念] 标签(不变)—— 概念列表
58+
│ ├── [模型] 标签(不变)—— 模型列表
59+
│ ├── [决策] 标签(不变)—— 决策日志
60+
│ ├── [画布] 标签(新增)—— KnowledgeCanvasView
61+
│ │ └── ReactFlow(无限画布)
62+
│ │ ├── CanvasNodeQuestion(自定义节点)
63+
│ │ ├── CanvasNodeConcept(自定义节点)
64+
│ │ ├── CanvasNodeModel(自定义节点)
65+
│ │ ├── edges(parent_id 映射)
66+
│ │ ├── MiniMap
67+
│ │ └── Controls
68+
│ └── 浮动工具栏(添加节点/适配视图/刷新布局)
69+
└── 右栏:详情面板(不变)
70+
```
71+
72+
**切换交互**:用户选择了树视图内嵌切换 + 标签页栏并存。
73+
- 树视图顶部新增小号「画布」图标按钮,点击切到 middleView="canvas"
74+
- 画布视图顶部保留「树视图」返回按钮,点击切回 middleView="tree"
75+
- 同时标签页栏(概念/模型/决策日志/画布)保持可见,语义一致
76+
- 切换回树视图时折叠/展开状态和滚动位置保持(组件不退场)
77+
78+
### 4.2 组件设计
79+
80+
#### KnowledgeCanvasView(新增,~200 行)
81+
82+
```
83+
Props:
84+
systemId: number
85+
nodes: KnowledgeNode[]
86+
concepts: KnowledgeConcept[]
87+
models: KnowledgeModel[]
88+
links: KnowledgeLink[]
89+
selectedNodeId: number | null
90+
onSelectNode: (id: number) => void
91+
onChanged: () => void
92+
93+
职责:
94+
1. nodes/concepts/models → React Flow 节点(Node[])
95+
2. parent_id → React Flow 边(Edge[])
96+
3. 首次切换时计算辐射布局初始位置
97+
4. 拖拽结束 → invoke("update_node_canvas_position")
98+
5. 节点点击 → onSelectNode(与树视图选中联动)
99+
6. 切换回树视图时选中态保持一致
100+
```
101+
102+
#### CanvasNodeQuestion(~60 行)
103+
104+
React Flow 自定义节点。渲染单个知识问题。
105+
106+
```
107+
┌────────────────────┐
108+
│ ❓ 照片为什么发灰 │
109+
│ 曝光三角 · 安全快门│ ← 关联概念徽标(若有)
110+
│ ◇ 黄金时刻法则 │ ← 关联模型徽标(若有)
111+
│ 📋 2 条笔记 │ ← 引用计数
112+
└────────────────────┘
113+
```
114+
115+
- 宽度固定 220px,高度自适应
116+
- 标题区:类型图标 + 文本(省略号,最大 2 行)
117+
- 底部概念/模型/引用徽标行(复用 SystemBadge 风格)
118+
119+
#### CanvasNodeConcept(~50 行)
120+
121+
```
122+
┌──────────────────────┐
123+
│ 🧬 曝光三角 │
124+
│ 本质:光圈/快门/ISO │
125+
│ ● 活跃 │
126+
└──────────────────────┘
127+
```
128+
129+
- 宽度固定 180px
130+
- 概念名 + 本质摘要(1 行)+ 状态指示符
131+
132+
#### CanvasNodeModel(~45 行)
133+
134+
```
135+
┌──────────────────────┐
136+
│ ⚙ 黄金时刻法则 │
137+
│ 日出日落前后 1 小时 │
138+
│ 🏷 摄影 │
139+
└──────────────────────┘
140+
```
141+
142+
- 宽度固定 180px
143+
- 模型名 + 主张摘录(1 行)+ 学科标签
144+
145+
### 4.3 数据流
146+
147+
```
148+
父级 (KnowledgePage)
149+
│
150+
├─ 加载 systems → nodes/concepts/models/links(复用 loadSystemDetail)
151+
│
152+
└─ middleView === "canvas"
153+
└─ KnowledgeCanvasView
154+
│
155+
├─ React Flow 初始化
156+
│ ├─ nodes: 辐射布局(首次)or 已存 canvas_x/y
157+
│ ├─ edges: parent_id → Edge[]
158+
│ └─ 渲染自定义节点
159+
│
160+
├─ onNodeDragStop → debounce → invoke("update_node_canvas_position")
161+
│ └─ { nodeId, canvasX, canvasY }
162+
│
163+
├─ onNodeClick → onSelectNode(nodeId) → 右栏详情面板更新
164+
│
165+
└─ onFitView → React Flow fitView()
166+
```
167+
168+
### 4.4 辐射布局算法
169+
170+
用户选择了辐射布局——核心问题在圆心,子节点向外辐射。
171+
172+
```
173+
算法:BFS 辐射布局(首次切到画布时触发 + 用户点"自动排列"时重新计算)
174+
175+
输入:nodes(按 parent_id 组织)
176+
输出:Map<nodeId, {x, y}>
177+
178+
术语:
179+
- 层级(ring):圆心为 ring 0,向外每层 +1
180+
- BBox:问题节点 220x80px、概念节点 180x70px、模型节点 180x70px
181+
- 环半径:圆心到该环中心线的距离,ring 1 = 220px,每环 +200px
182+
183+
步骤:
184+
1. 找出 parentId === null 的根节点组
185+
2. ring 0(圆心):无明确核心问题时第一个根在 (0, 0);
186+
有 coreQuestion 且有关联节点时,coreQuestion 虚拟节点在圆心
187+
3. ring 1:根节点的直接子节点,均匀分布在环半径上
188+
角度步长 = 360° / ring 1 节点数
189+
4. ring 2+:子节点均匀分布在父节点方向扇区内
190+
规则:子节点角度 = 父节点角度 ± spread
191+
spread = 60° / (子节点数 + 1)
192+
5. parentId 找不到父节点 → 最外环随机角度(兜底)
193+
6. 碰撞检测:放置前检查 BBox 重叠 → 沿角度外推 +50px,最多 2 次后到下一环
194+
```
195+
196+
> **纪律**:只计算一次。用户拖拽后位置不再参与算法。
197+
> 用户想重置布局可点"自动排列"按钮(触发全部重新计算,覆盖已存位置)。
198+
199+
### 4.5 切换交互
200+
201+
用户在头脑风暴中选择了"树视图内嵌切换"。具体交互设计:
202+
203+
- KnowledgeTreeView 顶部新增一个图标按钮「🎨 画布」(小号,无色边框,hover 时变色)
204+
- 点击后 middleView 从 "tree" 切到 "canvas"
205+
- KnowledgeCanvasView 顶部新增返回按钮「← 树视图」
206+
- 标签页栏(概念/模型/决策日志/画布)中的「画布」项保持可用——两种入口互通
207+
- 切换回树视图时折叠展开状态保持(该状态由 KnowledgeTreeView 内部管理,组件不卸载)
208+
- 切换回画布时视口位置恢复(canvas_x/y + canvas_state 中的 viewport 信息)
209+
210+
### 4.6 DB 变更
211+
212+
```
213+
-- 节点画布位置(幂等 ensure_column)
214+
ALTER TABLE knowledge_nodes ADD COLUMN canvas_x REAL;
215+
ALTER TABLE knowledge_nodes ADD COLUMN canvas_y REAL;
216+
217+
-- 体系画布状态表(视口位置恢复)
218+
CREATE TABLE IF NOT EXISTS knowledge_canvas_states (
219+
system_id INTEGER PRIMARY KEY REFERENCES knowledge_systems(id),
220+
viewport_x REAL DEFAULT 0,
221+
viewport_y REAL DEFAULT 0,
222+
zoom REAL DEFAULT 1.0
223+
);
224+
```
225+
226+
**命令增量**(3 条新命令):
227+
228+
| 命令 | 入参 | 返回值 | 说明 |
229+
|------|------|--------|------|
230+
| update_node_canvas_position | nodeId, canvasX, canvasY | bool | 保存节点拖拽位置(防抖后调用) |
231+
| batch_initialize_canvas_positions | systemId, positions: [{nodeId, x, y}] | bool | 批量写入初始辐射位置 |
232+
| save_canvas_viewport | systemId, viewportX, viewportY, zoom | bool | 保存画布视口(切回时恢复) |
233+
234+
## 五、与既有模式的冲突与兼容
235+
236+
| 既有事实 | 画布如何兼容 |
237+
|---------|-------------|
238+
| knowledge_nodes 无 canvas_x/y | 默认为 null → 首次打开触发 batch_initialize |
239+
| 树视图选中节点高亮 | 画布选中同一节点:高亮画布节点 + 右栏不变(shared selectedNodeId) |
240+
| 概念/模型在独立标签页 | 画布上的概念/模型节点 = 浮动参照,新建仍需走概念/模型标签页 |
241+
| 树视图不支持概念/模型混排 | 画布同时显示问题+概念+模型(按类型区分颜色/图标) |
242+
| testid 驱动的测试模式 | 辐射布局算法纯函数可测;React Flow 渲染跳过 jsdom(见 §六) |
243+
244+
## 六、测试策略
245+
246+
- **KnowledgeTreeView 既有测试不变**——画布不影响树视图。
247+
- **辐射布局算法**:纯函数 layoutRadial(nodes) → 单测断言中心节点在 (0,0)、子节点均匀分布。零 React 依赖。
248+
- **KnowledgeCanvasView**:仅测数据转换(nodes → React Flow nodes/edges)。React Flow 渲染用 e2e 或跳过。
249+
- **命令单元测试**:Rust 侧 commands_knowledge 新增 canvas_x/y 写入与读取测试。
250+
251+
## 七、UI 缩放体验
252+
253+
- 画布缩放与节点文本挂钩:zoom > 0.7 显示完整内容,0.4~0.7 仅标题,< 0.4 缩略卡片(仅图标+节点名缩写)
254+
- 右键菜单(远期):节点右键 → 编辑/删除/添加子节点
255+
256+
## 八、性能预算
257+
258+
| 场景 | 节点数 | 帧率目标 | 首开时间 |
259+
|------|--------|---------|---------|
260+
| 小体系 | < 20 | 60fps | < 500ms |
261+
| 中型体系 | 20-50 | 30-60fps | < 1s |
262+
| 大型体系 | 50-100 | 30fps | < 1.5s |
263+
| 超大型 | > 100 | >= 24fps | < 2s |
264+
265+
React Flow 虚拟化在此数据量级下应无瓶颈。
266+
267+
## 九、不做清单(本版)
268+
269+
- 画布上新建/编辑节点(只做展示+拖拽)
270+
- 手动连线(用户不能画新边)
271+
- 节点颜色/标签自定义
272+
- 画布导出为图片
273+
- 框选批量移动(React Flow 内置但本版暂不暴露)
274+
- 移动端触控适配
275+
- AI 建议布局
276+
277+
## 十、参考
278+
279+
- ADR-024 知识体系层(方案 B)——有界体系通过、自由双链/图谱仍出局
280+
- v0.13.1 知识体系基建规格([ ] 已归档)
281+
- 知识体系设计理念(§四 为什么不算"知识图谱回归")
282+
- React Flow 官方文档:https://reactflow.dev/

0 commit comments

Comments
 (0)