- Update architecture impact map, data, feature checklist, gap audit - Add audit reports for dashboard, exam-homework, grades-diagnostic, settings-profile, textbooks - Update bug reports (admin, teacher, lesson-preparation, others, shared) - Update coding standards, DR plan, design docs, and README
12 KiB
备课模块(lesson-preparation)审查报告 v3
审查日期:2026-06-22 审查范围:
src/modules/lesson-preparation/全部 34 个文件 + 3 个路由页面 审查方式:代码审查 + Playwright 运行时测试 前置状态:v2 已完成节点图编辑器重构(React Flow)+ P1 问题修复
一、审查结论
| 维度 | 状态 | 说明 |
|---|---|---|
| 编辑器可用性 | ✅ | 节点图渲染、选中、添加、编辑、保存均正常 |
| 功能完整性 | ⚠️ | 存在 5 个 P1 功能缺陷 + 2 个 P2 规范问题 |
| 代码质量 | ⚠️ | 存在 6 个 P2 代码规范违规 |
| 用户体验 | ⚠️ | 存在 4 个 P3 改进项 |
| 架构合规 | ✅ | 三层架构正确,权限校验完整 |
| 运行时稳定性 | ✅ | Playwright 测试无控制台错误 |
二、运行时测试结果(Playwright)
| 测试项 | 结果 | 说明 |
|---|---|---|
| 登录 | ✅ | 正常跳转 dashboard |
| 新建课案 | ✅ | 模板选择 → 创建 → 跳转编辑页 |
| 节点渲染 | ✅ | 8 节点 + 7 边正确渲染 |
| 节点选中 | ✅ | 点击节点 → 侧边面板显示 |
| 标题编辑 | ✅ | 侧边面板输入框可编辑 |
| 添加节点 | ✅ | 8 → 9 节点 |
| 连线 Handle | ✅ | 18 个 handle(9 节点 × 2) |
| 版本抽屉 | ✅ | 打开/关闭正常,显示"暂无版本" |
| 保存版本 | ✅ | 点击后无错误 |
| 控制台错误 | ✅ | 无 error/warning |
三、P1 功能缺陷
[P1-1] 节点拖拽位置不持久化(position 变化未触发自动保存)
现象:拖拽节点改变位置后,3 秒自动保存未触发,刷新页面位置丢失。
原因:onNodesChange 中 updateNodePosition 调用了 set({ isDirty: true }),但 lesson-plan-editor.tsx:71 的自动保存 effect 依赖 [editor.isDirty, editor.doc, planId]。editor.doc 是 zustand 的订阅值,但 updateNodePosition 每次都创建新的 doc 对象,导致 effect 频繁触发。然而拖拽过程中会触发多次 position 变化,debounce 3s 应该能生效。
实际根因:React Flow 拖拽时 change.position 可能是中间状态(dragging: true),最终位置在 dragging: false 时才确定。当前代码未区分 dragging 状态,每次都写入 store,但最终位置是正确的。问题在于 editor.doc 引用变化太快,debounce timer 不断重置,如果用户持续拖拽超过 3s 仍未保存。
修复建议:在 onNodesChange 中检查 change.dragging === false 才写入最终位置,避免中间状态污染。
[P1-2] 侧边面板关闭后无法重新打开
文件:lesson-plan-editor.tsx:65-68
现象:用户点击节点选中 → 侧边面板打开 → 点击面板关闭按钮 → 再次点击同一节点,面板不会重新打开。
原因:
useEffect(() => {
if (editor.selectedNodeId) setPanelOpen(true);
}, [editor.selectedNodeId]);
点击关闭按钮调用 selectNode(null),selectedNodeId 变为 null,panelOpen 仍为 true。再次点击同一节点时,selectedNodeId 从 null 变为该节点 id,effect 触发 setPanelOpen(true),但 panelOpen 已经是 true,React 不会重新渲染。
实际问题是:关闭按钮只调用 selectNode(null) 但没有 setPanelOpen(false),导致面板在 selectedNodeId 为 null 时仍然显示(因为 panelOpen && selectedNodeId 条件中 panelOpen 为 true 但 selectedNodeId 为 null,条件为 false,面板隐藏)。再次点击节点时 selectedNodeId 变化,effect 触发 setPanelOpen(true),但已经是 true。
实际根因:关闭面板后 panelOpen 仍为 true,但 selectedNodeId 为 null,条件 panelOpen && selectedNodeId 为 false。再次点击节点时 selectedNodeId 变化,effect 触发 setPanelOpen(true)(已是 true),面板应该显示。但 NodeEditPanel 内部 node 查找依赖 selectedNodeId,如果找到了节点应该显示。
验证:需要实际测试确认。如果确实无法重新打开,可能是 panelOpen 状态管理问题。
修复建议:移除 panelOpen 状态,直接用 selectedNodeId !== null 控制面板显示。
[P1-3] inline-question-editor 知识点标注缺失(v2 遗留)
文件:inline-question-editor.tsx:22
现象:课案内新建题目无法关联知识点。
原因:kpIds 被硬编码为常量空数组:
const kpIds: string[] = [];
修复建议:添加知识点选择器 UI,或复用 KnowledgePointPicker。
[P1-4] exercise-block 用 index 作为 key(v2 遗留)
{data.items.map((item, idx) => (
<div key={idx} ...>
问题:删除/排序时可能导致 React 状态错乱。
修复建议:用 item.questionId 作为 key。
[P1-5] lesson-plan-card 用 window.location.reload()(v2 遗留)
问题:不符合 SPA 模式,导致整个页面重新加载。
修复建议:用 useRouter().refresh()。
四、P2 代码规范问题
[P2-1] node-editor 用 as unknown as Record<string, unknown> 类型断言
data: n as unknown as Record<string, unknown>,
问题:双重断言绕过类型检查,违反"禁止 as 断言"规范。
建议:React Flow 的 Node 类型要求 data 为 Record<string, unknown>,可以构造一个符合类型的对象。
[P2-2] node-editor 隐藏 span 传递 props(hack)
<span className="hidden" data-textbook={textbookId} data-chapter={chapterId} data-classes={classes?.length} />
问题:用隐藏 DOM 元素避免 unused 警告,是 hack 做法。
建议:textbookId/chapterId/classes 是 NodeEditor 的 props 但未使用(实际由 NodeEditPanel 使用)。应移除这些 props,或让 NodeEditor 不接收它们。
[P2-3] publish-service 用 JSON.parse(JSON.stringify()) 深拷贝(v2 遗留)
问题:性能差,且不支持 Date 等特殊类型。
建议:用 structuredClone()。
[P2-4] exercise-block 用 as never 类型断言(v2 遗留)
update({ purpose: e.target.value as never })
建议:用 as ExercisePurpose 并添加类型守卫。
[P2-5] 多个组件用 alert()/confirm()(v2 遗留)
文件:
- version-history-drawer.tsx:46
- lesson-plan-card.tsx:48
- inline-question-editor.tsx:26
- text-study-block.tsx:42
问题:阻塞主线程,不符合现代 Web UI 规范。
建议:使用 AlertDialog 组件或 sonner toast。
[P2-6] text-study-block 选区计算错误
问题:range.startOffset/range.endOffset 是相对于当前 DOM 节点的偏移,不是相对于 sourceText 的字符偏移。如果 textarea 内有换行或子节点,偏移会不正确。
建议:用 textarea.selectionStart/textarea.selectionEnd 获取相对于文本的偏移。
五、P3 用户体验改进
[P3-1] 节点画布无空状态提示
问题:空白课案(无节点)时画布只显示网格,无引导提示。
建议:当 doc.nodes.length === 0 时显示"点击左下角添加节点开始"提示。
[P3-2] 版本抽屉无预览功能(v2 遗留)
问题:版本列表只显示版本号和标签,无法预览版本内容差异。
建议:点击版本时展开内容预览。
[P3-3] 编辑器无 loading 骨架屏(v2 遗留)
问题:编辑器初始化时无加载状态。
建议:添加 Suspense fallback。
[P3-4] 列表页英文标题与中文 UI 不一致
<h1>My Lesson Plans</h1>
<p>Manage your lesson preparation and teaching plans.</p>
问题:项目其他页面用中文,此处用英文。
建议:改为"我的备课"和"管理备课和教学计划"。
六、架构合规性检查
| 检查项 | 状态 | 说明 |
|---|---|---|
| 三层架构(app→modules→shared) | ✅ | 路由层只调用 actions 和 data-access |
| 模块间通过 data-access 通信 | ✅ | publish-service 通过 questions/exams/homework 的 data-access |
| Server Action 权限校验 | ✅ | 所有 action 调用 requirePermission |
| Zod 校验 | ✅ | actions 使用 schema 校验输入 |
| ActionState 返回类型 | ✅ | 统一使用 ActionState |
| "server-only" 标注 | ✅ | 所有 data-access 文件有 "server-only" |
| "use client" 标注 | ✅ | 所有客户端组件有 "use client" |
| revalidatePath 精确刷新 | ✅ | 创建/删除/回退后调用 revalidatePath |
| 架构图同步 | ✅ | 004/005 已同步 v2 节点图结构 |
| 数据结构向后兼容 | ✅ | normalizeDocument 自动迁移 v1→v2 |
七、修复优先级
| 优先级 | 问题编号 | 描述 | 影响 |
|---|---|---|---|
| P1 | P1-2 | 侧边面板关闭后无法重新打开 | UX 阻塞 |
| P1 | P1-1 | 节点拖拽位置可能不持久化 | 数据丢失风险 |
| P1 | P1-4 | exercise-block 用 index 作为 key | 列表状态错乱 |
| P1 | P1-5 | lesson-plan-card 用 window.location.reload | SPA 体验差 |
| P1 | P1-3 | inline 题目无知识点标注 | 功能缺失 |
| P2 | P2-2 | node-editor 隐藏 span hack | 代码质量 |
| P2 | P2-1 | node-editor 类型断言 | 代码规范 |
| P2 | P2-6 | text-study-block 选区计算错误 | 功能错误 |
| P2 | P2-3 | publish-service 深拷贝方式 | 性能 |
| P2 | P2-4 | exercise-block as never 断言 | 代码规范 |
| P2 | P2-5 | alert/confirm 使用 | UX 规范 |
| P3 | P3-4 | 列表页英文标题 | i18n 一致性 |
| P3 | P3-1 | 画布空状态提示 | UX 引导 |
| P3 | P3-2 | 版本预览 | UX 增强 |
| P3 | P3-3 | 编辑器骨架屏 | UX 优化 |
八、验证记录
| 验证项 | 命令 | 结果 |
|---|---|---|
| TypeScript | npx tsc --noEmit |
✅ exit 0 |
| ESLint | npm run lint |
✅ 备课模块零错误 |
| Playwright 节点渲染 | 8 节点 + 7 边 | ✅ |
| Playwright 节点选中 | 侧边面板显示 | ✅ |
| Playwright 添加节点 | 8 → 9 节点 | ✅ |
| Playwright 版本抽屉 | 打开/关闭 | ✅ |
| Playwright 保存版本 | 无错误 | ✅ |
| 控制台错误 | 无 error/warning | ✅ |
九、附录:测试截图
bugs/v3_01_initial.png- 初始编辑页bugs/v3_02_selected.png- 节点选中状态bugs/v3_03_versions.png- 版本抽屉bugs/v3_04_final.png- 最终状态