Files
NextEdu/docs/superpowers/specs/2026-07-04-lesson-preparation-paper-redesign-design.md
SpecialX a1c283e10d docs(lesson-preparation): 备课编辑器无边记纸感重构设计
新增设计文档,定义正文节点改造为真实 Tiptap 富文本编辑器、三栏布局(结构树 + 纸 + 详情面板)、节点展开到正文、师生交互节点、Tiptap Mark 锚点系统、v3→v4 数据迁移。
2026-07-04 10:58:55 +08:00

473 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 备课编辑器 · 无边记纸感重构设计
**日期**: 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`
- 旧版本无法读取 v4v4 是新创建分支,不回写 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