新增设计文档,定义正文节点改造为真实 Tiptap 富文本编辑器、三栏布局(结构树 + 纸 + 详情面板)、节点展开到正文、师生交互节点、Tiptap Mark 锚点系统、v3→v4 数据迁移。
17 KiB
备课编辑器 · 无边记纸感重构设计
日期: 2026-07-04 主题: 备课模块正文节点改为真实富文本编辑器 + 三栏布局重构 + 节点展开到正文 + 师生交互节点 状态: 已视觉确认,待规格审核
1. 背景与问题
1.1 当前问题
备课模块编辑器(src/modules/lesson-preparation/components/lesson-plan-editor.tsx)存在以下核心问题:
-
正文节点是"假"的富文本编辑器
textbook-content-node.tsx(画布中央节点)只是把 Markdown 字符串作为纯文本用whitespace-pre-wrap显示- 没有真实排版(加粗/标题/列表/图片都看不到)、没有光标、不可直接编辑
- 与
rich-text-block.tsx(侧栏教学节点已用 Tiptap)形成不一致体验
-
画布交互不适合长文档编辑
- React Flow 画布上 520px 小框装不下教材正文
- 频繁缩放/拖动画布打断写作流
- 移动端几乎不可用
-
锚点系统脆弱
lib/anchor-injector.ts用字符串偏移(plainToMdMap)定位- 任何编辑都引起偏移漂移,要靠
relocateAnchors修补 - Markdown → 纯文本的偏移映射在复杂格式下不可靠
-
配色不统一
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
- objective
- 锚点:区间高亮
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 三个展开入口
- 左栏结构树:点击「纸上」标记 → 切换展开/收起
- 右栏详情头部:展开切换按钮 ▾/▴
- 右键菜单:纸上右键节点 → "展开到正文" / "从正文收起"
4.3 右键菜单(节点级)
触发对象:纸上的 inline-node(已展开的节点)。右键教材正文文本触发的是"锚定"菜单(见 §6.3),不是节点菜单。右键左栏结构树节点不触发菜单(左栏用按钮操作)。
分组结构:
- 节点操作:展开到正文 / 从正文收起 / 上移 / 下移
- AI 协助:生成本节点内容 / 优化表达 / 差异化建议 / 生成分层提问
- 其他:复制节点 / 删除节点
5. 师生交互节点(新节点类型)
5.1 类型定义
新增 interaction BlockType:
export type BlockType =
| "objective" | "key_point" | "import"
| "new_teaching" | "consolidation" | "summary"
| "homework" | "blackboard" | "text_study"
| "exercise" | "rich_text" | "reflection"
| "interaction"; // 新增
5.2 数据结构
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。
// 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 锚点数据结构变更
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 黑线 + 行内小标签ABC(黑底白字 9px) - 点锚点:16px 黑色圆圈带数字 ①②③
- hover/active:高亮加深,圆点放大
7. 数据模型变更
7.1 文档版本升级到 v4
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.tsxcomponents/nodes/textbook-content-node.tsxcomponents/nodes/textbook-segments.tsxcomponents/nodes/anchor-node-selector.tsxlib/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
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 编辑教材正文
- 中栏纸区聚焦 → Tiptap 编辑器激活 → 浮动工具条出现
- 输入 →
onUpdate→editor-slice.updateNode(textbookNodeId, { data: { content: html } }) - debounce 3s 自动保存(现有逻辑保留)
10.2 锚定选中文本到节点
- 在纸上选中文本 → Tiptap selection change
- 右键 → 上下文菜单 → "锚定到节点"
- 选择节点 →
toggleMark("anchor", { anchorId, nodeId, type: "range" }) expandedNodeIds不变,但右栏详情面板的"锚点"属性更新
10.3 展开节点到正文
- 右栏详情头部点击 ▾ →
expanded-slice.toggleExpand(nodeId) paper-editor监听expandedNodeIds→ 在教材正文对应锚点位置插入<InlineNode>- InlineNode 渲染节点内容(用 Inter 字体区分正文 Fraunces)
10.4 师生交互对话编辑
- 左栏添加
interaction节点 → 默认 3 轮空对话 - 右栏详情显示
qa-editor→ 编辑每轮角色/内容/预期 - 展开到正文 →
inline-qa-dialog用对话体渲染 - 编辑右栏 → 纸上展开态实时更新
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. 已知风险
- v3 锚点丢失:v3 文档的字符串偏移锚点在迁移到 v4 后失效,需重新锚定。迁移后在编辑器顶部显示一条不可关闭的黄色 banner("此课案使用旧版锚点格式,部分锚点已失效,请重新锚定"),点击"知道了"后消失(记录到 localStorage 不再提示该 planId)。
- Tiptap Mark 与 React 状态同步:Tiptap 的 Mark 状态在编辑时是内部的,需要
onSelectionUpdate同步到 React 状态以驱动右栏锚点列表。 - 展开节点的性能:大量节点展开时,纸上 DOM 数量增加。需
onlyRenderVisibleElements或虚拟化(暂不实现,超过 20 个展开节点时再说)。 - 打印视图:
print-view.tsx需适配 v4 文档结构(移除画布依赖,按展开顺序打印)。
15. 实现范围
本设计涵盖:
- 三栏布局重构
- 正文 Tiptap 富文本编辑器
- 锚点 Mark 系统
- 节点展开/收起
- 师生交互节点
- 右键菜单 + AI 协助入口
- 配色统一
- v3 → v4 数据迁移
- i18n
- 架构图同步
不涵盖(YAGNI):
- 协同编辑(V5-11 R3 单独立项)
- 移动端独立优化(先桌面端可用)
- 展开节点的虚拟化(性能问题出现再处理)
- 自定义字体加载(先引 Google Fonts CDN)