|
| 1 | +# v0.13.9 设计规格:笔记编辑修复 + 问题树层级 + 画布链接与接线 |
| 2 | + |
| 3 | +> 状态:设计已批准(2026-08-25,用户裁决"开始实施");代码交付后登记 v0.13.md |
| 4 | +> 范围:纯前端 + 单测修复批——**无 schema 变更、无新命令** |
| 5 | +> 关联:v0.13.8 画布([规格](../archive/2026-08-24/2026-08-24-v0.13.8-knowledge-canvas-design.md))· ADR-024 知识体系层 |
| 6 | +
|
| 7 | +## 一、背景与问题清单 |
| 8 | + |
| 9 | +真机/日常使用反馈 4 个问题(2026-08-25 探讨确认): |
| 10 | + |
| 11 | +| # | 问题 | 根因(代码证据) | |
| 12 | +|---|------|------------------| |
| 13 | +| 1 | 笔记编辑态添加 H1 自动跳顶 | `NoteEditView.toolbarAction("h1")` 用 `wrapSelection(v, s, e, "# ", "")`——多行选区只在开头插一次(全选=选区起点 0→跳顶);滚动查看未聚焦 textarea 时选区滞留旧位置(插入跑到文档开头→跳顶) | |
| 14 | +| 2 | 问题树大纲无层级区分 | `KnowledgeTreeView.renderNode` 递归渲染无任何缩进,所有层级视觉平齐(对比 NoteReadingView 大纲有 `paddingLeft: (level-1)*12`) | |
| 15 | +| 3 | 画布领域名称未与子节点链接 | ① 向导创建领域入口 `add_knowledge_node` 未传 parentId→根节点;根节点与圆心核心问题卡之间**无任何边**(边只连 parent_id,`canvasElements.ts`);② 领域体系(coreQuestion 为 null)圆心**无体系名卡**;③ `CanvasNodeQuestion` 所有树节点统一 ❓ 样式,不区分问题/场景/领域入口 | |
| 16 | +| 4 | 画布接线混乱 + 接线位置固定 | ① 辐射布局每环 +200px,深度≥3 层边长达 600px+ 横穿画布;② Handle 写死 target=Top/source=Bottom,移动节点后接线方向不随相对方位变化 | |
| 17 | + |
| 18 | +## 二、设计 |
| 19 | + |
| 20 | +### ① 笔记 H1 跳顶(markdownEdit.ts / NoteEditView.tsx) |
| 21 | + |
| 22 | +- 新增纯函数 `headingLines(content, selStart, selEnd, level): EditResult`: |
| 23 | + - 多行选区:选区内每行行首加 `# `×level;**已是标题(`/^#{1,6}\s/`)的行跳过不叠加** |
| 24 | + - 选区尾恰在行首(`content[end-1] === "\n"`)且非空选区 → 不含下一行 |
| 25 | + - 光标统一落在**末行行首 + 该行及之前已插入的标记总长**(多行=末行标记后;单行=原行标记后)→ 全选添加时视图停在末尾行,不再跳顶 |
| 26 | +- `toolbarAction` 读选区前新增定位逻辑:`document.activeElement !== ta && ta.scrollTop > 0` 时,按 `scrollTop / (scrollHeight - clientHeight)` 比例估算可视区偏移并**向左校正到行首**作为插入目标(所见即所得) |
| 27 | +- h1/h2/h3 改调 `headingLines`;bold/italic 等保留 `wrapSelection` |
| 28 | + |
| 29 | +### ② 问题树层级 + 引导线(KnowledgeTreeView.tsx) |
| 30 | + |
| 31 | +- `renderNode(n, depth)`:节点行 `paddingLeft = 8 + depth * 16` |
| 32 | +- 子树容器(`open && kids` 外层):`marginLeft: 15 + borderLeft: 1px solid #e5e7eb + paddingLeft: 11` |
| 33 | +- 每个子节点行 `position: relative` + 顶部水平短线(`position:absolute; left:-11; top:12; width:11; height:1`)连接竖线 |
| 34 | +- 折叠时整棵子树不渲染(现有逻辑),引导线自然收起 |
| 35 | + |
| 36 | +### ③ 画布体系根卡 + 根节点连线 + 类型徽标(canvasElements.ts / CanvasNodeQuestion.tsx / KnowledgeCanvasView.tsx / KnowledgePage.tsx) |
| 37 | + |
| 38 | +- `CanvasElementsInput` 增加 `rootCard: { title: string; subtitle: string } | null` |
| 39 | +- `buildCanvasElements`:`rootCard` 存在时给所有 `parentId == null` 节点追加**虚线灰边** `core → 根节点`(`strokeDasharray: "5 4"`,展示层虚拟边不落库;语义:根节点的父是体系本身——注释声明不违反"连线只反映既有关系") |
| 40 | +- `CanvasNodeData` 增加 `nodeType: KnowledgeNodeType | null`(问题节点填充 `n.type`,概念/模型 null) |
| 41 | +- `CanvasNodeQuestion` 渲染类型 chip:📂领域入口紫 `#7c3aed` / 🎯场景橙 `#d97706` / ❓问题青 `#0f766e`(与树视图 `typeColor` 同口径) |
| 42 | +- `KnowledgeCanvasView` 新增 `systemName` prop;圆心根卡逻辑:`coreQuestion` 存在 → 核心问题卡(副标"核心问题");否则 `systemName` 存在 → 体系名卡(副标"领域体系");`CanvasCoreNode` 改读 `data.subtitle` 做标签 |
| 43 | +- **架构演进登记**(v0.13.md + Foresight):「根节点真实化(type=root,体系名/核心问题迁移为节点)」为 v0.14 候选——本次不动 schema |
| 44 | + |
| 45 | +### ④ 画布接线动态化 + 布局优化(canvasElements.ts / KnowledgeCanvasView.tsx / CanvasNodeQuestion.tsx / layoutRadial.ts) |
| 46 | + |
| 47 | +- 新增纯函数 `resolveEdgeHandles(sourceCenter, targetCenter): { sourceHandle, targetHandle }`: |
| 48 | + - `|dx| > |dy|` → 水平接入(source 用 right/left,target 用 left/right) |
| 49 | + - 否则垂直接入(source 用 bottom/top,target 用 top/bottom) |
| 50 | +- `CanvasNodeQuestion` / `CanvasCoreNode` 四边各声明 Handle(`source-{top|bottom|left|right}` / `target-{...}`,opacity 0) |
| 51 | +- `CanvasFlow` 内 `useMemo`:基于 **rfNodes 当前拖拽后 position** 重算每条边(含虚拟边)的 sourceHandle/targetHandle → 移动节点后接线方向随之改变 |
| 52 | +- `layoutRadial`:`RING_STEP` 200 → 160(长边缩短);测试全部走常量断言,自动跟随 |
| 53 | + |
| 54 | +## 三、测试计划 |
| 55 | + |
| 56 | +| 文件 | 新增/更新 | |
| 57 | +|------|-----------| |
| 58 | +| `markdownEdit.test.ts` | 新增 `headingLines`:多行逐行转换、已标题行跳过、选区尾行首边界、无选区行首转换、光标落位、空内容 | |
| 59 | +| `canvasElements.test.ts` | 更新:rootCard 存在时根节点多一条虚线边;rootCard 缺失不追加;nodeType 填充断言 | |
| 60 | +| `CanvasNodes.test.tsx` | 新增:类型 chip(问题/场景/领域入口)渲染;四 Handle 存在 | |
| 61 | +| `KnowledgeCanvasView.test.tsx` | 更新:领域体系(systemName 非空、coreQuestion null)渲染体系名根卡;接线 handle 随相对方位选择 | |
| 62 | +| `KnowledgeTreeView.test.tsx` | 新增:子节点行 paddingLeft 随 depth 递增(style 断言) | |
| 63 | +| `layoutRadial.test.ts` | 无需改(常量断言自动跟随) | |
| 64 | + |
| 65 | +手工验收:① 全选 5 行加 H1 → 全变标题且视图不跳顶;滚动后未点编辑区加 H1 → 插入在可视区行;② 问题树层级缩进 + 引导线,折叠收起;③ 领域体系画布圆心出现体系名卡 + 根节点虚线连入 + 节点类型 chip;④ 拖拽节点后接线方向随相对位置变化。 |
| 66 | + |
| 67 | +## 四、风险与边界 |
| 68 | + |
| 69 | +- **headingLines 光标语义变更**:多行转换后光标不再保持选区(落在末行标记后)——符合"整块转标题"预期,测试锁定契约 |
| 70 | +- **虚拟边与规格**:虚线边是体系-根节点关系的可视化(体系是父),不落库、不可拖拽连线——注释 + v0.13.md 明确声明 |
| 71 | +- **Handle 增加**:四 Handle 透明度 0,不改变视觉;`nodesConnectable={false}` 已有,无手动画线风险 |
| 72 | +- **scrollTop 比例估算**:textarea 默认 soft wrap 下为近似(长行折行误差),校正到行首后插入位置落在可视区行首,可接受 |
0 commit comments