# 备课编辑器 · 无边记纸感重构设计

**日期**: 2026-07-04
**主题**: 备课模块正文节点改为真实富文本编辑器 + 三栏布局重构 + 节点展开到正文 + 师生交互节点
**状态**: 已视觉确认，待规格审核

---

## 1. 背景与问题

### 1.1 当前问题

备课模块编辑器（`src/modules/lesson-preparation/components/lesson-plan-editor.tsx`）存在以下核心问题：

1. **正文节点是"假"的富文本编辑器**
   - `textbook-content-node.tsx`（画布中央节点）只是把 Markdown 字符串作为纯文本用 `whitespace-pre-wrap` 显示
   - 没有真实排版（加粗/标题/列表/图片都看不到）、没有光标、不可直接编辑
   - 与 `rich-text-block.tsx`（侧栏教学节点已用 Tiptap）形成不一致体验

2. **画布交互不适合长文档编辑**
   - React Flow 画布上 520px 小框装不下教材正文
   - 频繁缩放/拖动画布打断写作流
   - 移动端几乎不可用

3. **锚点系统脆弱**
   - `lib/anchor-injector.ts` 用字符串偏移（`plainToMd` Map）定位
   - 任何编辑都引起偏移漂移，要靠 `relocateAnchors` 修补
   - Markdown → 纯文本的偏移映射在复杂格式下不可靠

4. **配色不统一**
   - `globals.css` 的 `--lesson-node-*` 用了 13 个 Material Design 鲜艳原色（#4caf50、#f44336、#9c27b0 等）
   - 与 shadcn/ui 设计语言冲突

### 1.2 用户期望

- "像在无边记（Freeform）里做教案"——一张安静的白纸居中，便签轻贴其侧
- 正文节点必须是真实富文本编辑器，可编辑（教材正文副本，不影响原课文）
- 节点能展开到正文流里，用字体差异区分（不框住）
- 师生交互环节设计（师问生答）
- 右键菜单含 AI 协助
- 不用奇怪颜色和图标，符合系统设计语言

---

## 2. 视觉设计

### 2.1 视觉主张

一张安静的白纸居中，节点内容轻贴其侧——像在无边记里把教案摊开写，而不是在画布上摆积木。

### 2.2 字体双轨

| 用途 | 字体 | 字号 | 颜色 |
|------|------|------|------|
| 正文（纸上教材） | Fraunces 衬线 | 16px / 1.75 行高 | #1a1a1a |
| UI / 节点文本 / 详情 | Inter 无衬线 | 13px / 1.6 行高 | #44403c |
| 锚点标签 / 元数据 | JetBrains Mono | 9-11px | #a8a29e |

### 2.3 配色（移除鲜艳 Material 色）

- 全中性调：白/灰/近黑
- 节点类型只用 6px 小色点区分：
  - objective `#4b5563` / new_teaching `#1c1917` / interaction `#6366f1`（克制靛蓝）
  - exercise `#6b7280` / summary `#525252` / textbook `#44403c`
- 锚点：区间高亮 `rgba(28,25,23,0.08)` + 底部 1.5px 黑线；点锚点黑色小圆圈

### 2.4 禁用元素

- 所有 emoji（📖🎯📚 等）
- 所有装饰性图标
- 鲜艳色块

---

## 3. 布局架构

### 3.1 三栏布局（替换 React Flow 画布）

```
┌──────────────────────────────────────────────────────────────┐
│ 顶部工具栏（标题 / 保存状态 / 撤销重做 / 发布 / AI）             │
├──────────┬────────────────────────────┬────────────────────┤
│          │                            │                    │
│  左栏     │       中栏（纸）             │   右栏（详情面板）   │
│  260px   │       1fr                  │   380px             │
│          │                            │                    │
│  结构树   │   教材正文富文本编辑器        │   选中节点的详情     │
│  (折叠)  │   居中 max-w-720px          │   (普通卡片样式)    │
│          │   白底 shadow               │                    │
└──────────┴────────────────────────────┴────────────────────┘
```

### 3.2 左栏：结构树

- 所有节点（含正文）按 `order` 排序，纯列表
- 每行：折叠箭头 + 6px 色点 + 标题 + 类型标签 + 「纸上」标记（如已展开）
- 师生交互节点可展开子节点（对话轮次）
- 底部"+ 添加节点"

### 3.3 中栏：纸

- 教材正文的 Tiptap 富文本编辑器
- `max-width: 720px`、`padding: 64px 72px`、`box-shadow`、白底
- 聚焦时顶部出现毛玻璃浮动工具条（B / I / H1 / H2 / • / 1. / " / —）
- 锚点标记内嵌在文本流里
- 展开的节点作为 `inline-node` 嵌入段落之间

### 3.4 右栏：详情面板

- **不再是便签样式**，改为普通详情卡片
- 头部：色点 + 类型标签 + 标题输入框 + 展开切换按钮 + 更多按钮
- 属性条：教学阶段 / 差异化 / 时长 / 锚点（可点击切换）
- 主体：节点类型的专属编辑器（复用现有 `BlockRenderer`）
- 底部 AI 区：4 个 AI 协助按钮（靛蓝 `#6366f1`）
- 元信息条：标签 + 更新时间

---

## 4. 节点展开/收起机制

### 4.1 统一展开（所有节点类型）

所有教学节点（objective / new_teaching / exercise / interaction 等）都支持展开到正文流。

**展开态（inline-node 嵌入纸面）**：
- 左侧 2px 细线（`#d6d3d1`，hover 变 `#1c1917`）
- Inter 字体 13.5px / 暖灰 `#44403c`
- 头部：色点 + 类型标签 + 锚点引用 + 「收起 ▴」按钮
- 标题：Inter 14px 加粗 `#1a1a1a`
- 主体：节点类型的渲染内容
- 视觉上"从正文里长出来"，不框住

**插入位置**：按节点 `order` 字段顺序，在教材正文的段落之间插入 inline-node。锚点（区间高亮/点圆圈）仍留在正文文本内，仅作为视觉标记；inline-node 的位置由 order 决定，不由锚点位置决定。这样保证展开顺序稳定，编辑正文不会引起 inline-node 位置漂移。

**收起态**：
- inline-node 从纸上消失
- 节点数据仍在右栏详情和左栏结构树可见

### 4.2 三个展开入口

1. **左栏结构树**：点击「纸上」标记 → 切换展开/收起
2. **右栏详情头部**：展开切换按钮 ▾/▴
3. **右键菜单**：纸上右键节点 → "展开到正文" / "从正文收起"

### 4.3 右键菜单（节点级）

**触发对象**：纸上的 inline-node（已展开的节点）。右键教材正文文本触发的是"锚定"菜单（见 §6.3），不是节点菜单。右键左栏结构树节点不触发菜单（左栏用按钮操作）。

分组结构：
- **节点操作**：展开到正文 / 从正文收起 / 上移 / 下移
- **AI 协助**：生成本节点内容 / 优化表达 / 差异化建议 / 生成分层提问
- **其他**：复制节点 / 删除节点

---

## 5. 师生交互节点（新节点类型）

### 5.1 类型定义

新增 `interaction` BlockType：

```typescript
export type BlockType =
  | "objective" | "key_point" | "import"
  | "new_teaching" | "consolidation" | "summary"
  | "homework" | "blackboard" | "text_study"
  | "exercise" | "rich_text" | "reflection"
  | "interaction";  // 新增
```

### 5.2 数据结构

```typescript
export interface QATurn {
  id: string;
  role: "teacher" | "student";
  content: string;
  /** 教师提问的预期答案 / 引导策略（可选）*/
  expectedAnswer?: string;
  /** 这一轮的顺序 */
  order: number;
}

export interface InteractionBlockData {
  /** 设计意图 */
  designIntent: string;
  /** 对话轮次 */
  turns: QATurn[];
  /** 关联知识点 */
  knowledgePointIds: string[];
}
```

### 5.3 渲染样式

**纸上展开态（QA 对话体）**：
- 每轮对话：`师`/`生` 角色标签（JetBrains Mono 9px，黑/灰区分）+ 内容
- 教师提问下方用 italic 灰色小字标注 `[预期：...]`
- 对话轮次之间用 dashed 线分隔

**右栏详情编辑器**：
- 每轮对话独立卡片：角色选择 + 内容文本框 + 预期答案文本框
- 上下移 / 删除按钮
- 底部"+ 添加一轮对话"

### 5.4 左栏子节点

师生交互节点在左栏可展开显示对话轮次：
- 第 1 轮 · 提问（师）
- 第 2 轮 · 回答（生）
- 第 3 轮 · 追问（师）

---

## 6. 锚点系统重构

### 6.1 从字符串偏移改为 Tiptap Mark

**当前**：`NodeAnchor.start/end` 是基于 `markdownToPlainText` 的纯文本偏移，靠 `injectPlaceholders` 注入 `[[anchor:id]]` 标记，靠 `relocateAnchors` 修补漂移。

**改为**：Tiptap 自定义 Mark `AnchorMark`，存储 `anchorId` 和 `nodeId`。

```typescript
// Tiptap Mark 定义
const AnchorMark = Mark.create({
  name: "anchor",
  addAttributes() {
    return {
      anchorId: { default: null },
      nodeId: { default: null },
      type: { default: "range" },  // "range" | "point"
    };
  },
  parseHTML() { return [{ tag: "span[data-anchor-id]" }]; },
  renderHTML({ HTMLAttributes }) {
    return ["span", mergeAttributes(HTMLAttributes, {
      "data-anchor-id": HTMLAttributes.anchorId,
      "data-node-id": HTMLAttributes.nodeId,
      "data-anchor-type": HTMLAttributes.type,
    })];
  },
});
```

### 6.2 锚点数据结构变更

```typescript
export interface NodeAnchor {
  id: string;
  nodeId: string;
  type: AnchorType;
  // 删除 start/end/textPreview/invalid（改为 Mark 内嵌）
  // 保留 id/nodeId/type 用于关联
}
```

### 6.3 锚点交互

- **区间锚定**：在纸上选中文本 → 右键 → "锚定到节点" → 选择节点 → Tiptap `toggleMark` 包裹选中文本
- **点锚定**：在纸上点击位置 → 右键 → "插入锚点" → 选择节点 → 插入 PointMark 节点（带圈数字）
- **编辑时自动跟随**：因为是 Mark，文本增删时 Mark 自动随文本移动，不再需要 `relocateAnchors`
- **删除锚点**：右栏详情面板的锚点列表 → 删除按钮 → Tiptap `unsetMark`

### 6.4 锚点视觉

- 区间锚点：浅灰高亮 `rgba(28,25,23,0.08)` + 底部 1.5px 黑线 + 行内小标签 `A` `B` `C`（黑底白字 9px）
- 点锚点：16px 黑色圆圈带数字 ①②③
- hover/active：高亮加深，圆点放大

---

## 7. 数据模型变更

### 7.1 文档版本升级到 v4

```typescript
export interface LessonPlanDocumentV4 {
  version: 4;
  textbookContentNodeId: string;
  nodes: AnyLessonPlanNode[];
  edges: AnyLessonPlanEdge[];  // 保留但不再用于画布连线
  anchors: NodeAnchor[];       // 简化：只存 id 关联
  /** V4 新增：节点展开状态 */
  expandedNodeIds: string[];
}
```

### 7.2 迁移

- `lib/document-migration.ts` 新增 `migrateV3ToV4`
- v3 的 `anchor.start/end/textPreview/invalid` 字段忽略（旧锚点在 v4 中失效，需重新锚定）
- v3 的 `position {x,y}` 字段保留但忽略（不再用于画布）
- v3 的 `edges` 保留但忽略（不再画连线）

### 7.3 向后兼容

- 读取 v3 文档时自动迁移到 v4
- v4 文档保存时 `version: 4`
- 旧版本无法读取 v4（v4 是新创建分支，不回写 v3）

---

## 8. 组件架构

### 8.1 移除的组件

- `components/node-editor.tsx`（React Flow 画布）
- `components/nodes/lesson-node.tsx`
- `components/nodes/textbook-content-node.tsx`
- `components/nodes/textbook-segments.tsx`
- `components/nodes/anchor-node-selector.tsx`
- `lib/anchor-injector.ts`（字符串偏移系统）
- `lib/rf-mappers.ts`（React Flow 映射）
- `lib/auto-layout.ts`

### 8.2 新增组件

```
src/modules/lesson-preparation/components/
├─ paper-editor/
│  ├─ paper-editor.tsx          # 中栏：纸区容器
│  ├─ textbook-tiptap-editor.tsx # 正文 Tiptap 编辑器（含锚点 Mark）
│  ├─ inline-node.tsx           # 展开节点的 inline 渲染
│  ├─ inline-qa-dialog.tsx      # 师生交互的对话体渲染
│  ├─ anchor-mark.ts            # Tiptap AnchorMark 定义
│  ├─ paper-toolbar.tsx         # 浮动工具条
│  └─ paper-context-menu.tsx    # 右键菜单
├─ structure-tree/
│  ├─ structure-tree.tsx        # 左栏结构树
│  └─ tree-node-row.tsx         # 树节点行
├─ detail-panel/
│  ├─ detail-panel.tsx          # 右栏详情面板容器
│  ├─ detail-head.tsx           # 头部（类型 + 标题 + 操作）
│  ├─ detail-props.tsx          # 属性条
│  └─ qa-editor.tsx             # 师生交互编辑器
└─ blocks/
   └─ interaction-block.tsx     # 师生交互 block（详情编辑）
```

### 8.3 改造的组件

- `lesson-plan-editor.tsx`：移除 NodeEditor + NodeEditPanel，改为三栏布局（StructureTree + PaperEditor + DetailPanel）
- `node-edit-panel.tsx`：拆分到 `detail-panel/` 下，逻辑保留
- `config/block-registry.tsx`：新增 `interaction` 类型注册

### 8.4 保留的组件

- `blocks/rich-text-block.tsx`（仍用于非正文节点的富文本编辑）
- `blocks/objective-block.tsx` 等其他 block 编辑器
- `version-history-drawer.tsx` / `print-view.tsx` / `consistency-check-dialog.tsx` / `ai-feedback-dialog.tsx` / `ai-differentiation-dialog.tsx`
- 所有 `actions-*.ts` / `data-access-*.ts`

---

## 9. 状态管理

### 9.1 新增 slice：`expanded-slice.ts`

```typescript
interface ExpandedState {
  expandedNodeIds: string[];
  toggleExpand: (nodeId: string) => void;
  setExpanded: (nodeIds: string[]) => void;
  isExpanded: (nodeId: string) => boolean;
}
```

### 9.2 现有 slice 调整

- `editor-slice.ts`：移除 `selectedNodeId` 改为 `activeNodeId`（详情面板显示的节点），新增 `anchorNodeForSelection`（待锚定的节点 ID，用于纸上选中文本时锚定）
- `selection-slice.ts`：移除画布选择相关，保留节点选择
- `history-slice.ts`：保持不变（撤销/重做覆盖 v4 文档）

---

## 10. 关键交互流

### 10.1 编辑教材正文

1. 中栏纸区聚焦 → Tiptap 编辑器激活 → 浮动工具条出现
2. 输入 → `onUpdate` → `editor-slice.updateNode(textbookNodeId, { data: { content: html } })`
3. debounce 3s 自动保存（现有逻辑保留）

### 10.2 锚定选中文本到节点

1. 在纸上选中文本 → Tiptap selection change
2. 右键 → 上下文菜单 → "锚定到节点"
3. 选择节点 → `toggleMark("anchor", { anchorId, nodeId, type: "range" })`
4. `expandedNodeIds` 不变，但右栏详情面板的"锚点"属性更新

### 10.3 展开节点到正文

1. 右栏详情头部点击 ▾ → `expanded-slice.toggleExpand(nodeId)`
2. `paper-editor` 监听 `expandedNodeIds` → 在教材正文对应锚点位置插入 `<InlineNode>`
3. InlineNode 渲染节点内容（用 Inter 字体区分正文 Fraunces）

### 10.4 师生交互对话编辑

1. 左栏添加 `interaction` 节点 → 默认 3 轮空对话
2. 右栏详情显示 `qa-editor` → 编辑每轮角色/内容/预期
3. 展开到正文 → `inline-qa-dialog` 用对话体渲染
4. 编辑右栏 → 纸上展开态实时更新

---

## 11. 移除的功能

| 功能 | 原因 |
|------|------|
| React Flow 画布 | 不适合长文档编辑，纸感设计取代 |
| 节点拖拽 / 连线 | 树结构 + 展开流取代空间布局 |
| Minimap / Controls | 画布移除 |
| 字符串偏移锚点 | Tiptap Mark 取代 |
| `relocateAnchors` | Mark 自动跟随 |
| 13 个鲜艳 Material 色 | 统一到中性令牌 |
| `position {x,y}` 字段使用 | 保留字段但忽略（向后兼容） |

---

## 12. i18n

新增翻译键（zh-CN / en）：

- `lessonPreparation.interaction.*`：师生交互节点相关
- `lessonPreparation.paper.*`：纸区相关
- `lessonPreparation.contextMenu.*`：右键菜单
- `lessonPreparation.detail.*`：详情面板
- `lessonPreparation.tree.*`：结构树

---

## 13. 架构图同步

完成后更新：
- `docs/architecture/004_architecture_impact_map.md`：lesson-preparation 模块章节
- `docs/architecture/005_architecture_data.json`：
  - 新增 `interaction` 到 BlockType
  - 新增 `QATurn` / `InteractionBlockData` 类型
  - 新增组件 exports
  - 移除的组件
  - 数据结构版本 v4

---

## 14. 已知风险

1. **v3 锚点丢失**：v3 文档的字符串偏移锚点在迁移到 v4 后失效，需重新锚定。迁移后在编辑器顶部显示一条不可关闭的黄色 banner（"此课案使用旧版锚点格式，部分锚点已失效，请重新锚定"），点击"知道了"后消失（记录到 localStorage 不再提示该 planId）。
2. **Tiptap Mark 与 React 状态同步**：Tiptap 的 Mark 状态在编辑时是内部的，需要 `onSelectionUpdate` 同步到 React 状态以驱动右栏锚点列表。
3. **展开节点的性能**：大量节点展开时，纸上 DOM 数量增加。需 `onlyRenderVisibleElements` 或虚拟化（暂不实现，超过 20 个展开节点时再说）。
4. **打印视图**：`print-view.tsx` 需适配 v4 文档结构（移除画布依赖，按展开顺序打印）。

---

## 15. 实现范围

本设计涵盖：
- 三栏布局重构
- 正文 Tiptap 富文本编辑器
- 锚点 Mark 系统
- 节点展开/收起
- 师生交互节点
- 右键菜单 + AI 协助入口
- 配色统一
- v3 → v4 数据迁移
- i18n
- 架构图同步

**不涵盖**（YAGNI）：
- 协同编辑（V5-11 R3 单独立项）
- 移动端独立优化（先桌面端可用）
- 展开节点的虚拟化（性能问题出现再处理）
- 自定义字体加载（先引 Google Fonts CDN）
