Files
NextEdu/bugs/lesson_preparation_bug_v3.md
SpecialX 27db170c0a docs: update architecture docs, audit reports, and bug tracking
- 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
2026-06-23 17:36:18 +08:00

12 KiB
Raw Blame History

备课模块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 个 handle9 节点 × 2
版本抽屉 打开/关闭正常,显示"暂无版本"
保存版本 点击后无错误
控制台错误 无 error/warning

三、P1 功能缺陷

[P1-1] 节点拖拽位置不持久化position 变化未触发自动保存)

文件node-editor.tsx:59-74

现象拖拽节点改变位置后3 秒自动保存未触发,刷新页面位置丢失。

原因onNodesChangeupdateNodePosition 调用了 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 变为 nullpanelOpen 仍为 true。再次点击同一节点时selectedNodeId 从 null 变为该节点 ideffect 触发 setPanelOpen(true),但 panelOpen 已经是 trueReact 不会重新渲染。

实际问题是:关闭按钮只调用 selectNode(null) 但没有 setPanelOpen(false),导致面板在 selectedNodeId 为 null 时仍然显示(因为 panelOpen && selectedNodeId 条件中 panelOpen 为 true 但 selectedNodeId 为 null条件为 false面板隐藏。再次点击节点时 selectedNodeId 变化effect 触发 setPanelOpen(true),但已经是 true。

实际根因:关闭面板后 panelOpen 仍为 trueselectedNodeId 为 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 作为 keyv2 遗留)

文件exercise-block.tsx:67

{data.items.map((item, idx) => (
  <div key={idx} ...>

问题:删除/排序时可能导致 React 状态错乱。

修复建议:用 item.questionId 作为 key。


[P1-5] lesson-plan-card 用 window.location.reload()v2 遗留)

文件lesson-plan-card.tsx:39,50

问题:不符合 SPA 模式,导致整个页面重新加载。

修复建议:用 useRouter().refresh()


四、P2 代码规范问题

[P2-1] node-editor 用 as unknown as Record<string, unknown> 类型断言

文件node-editor.tsx:42

data: n as unknown as Record<string, unknown>,

问题:双重断言绕过类型检查,违反"禁止 as 断言"规范。

建议React Flow 的 Node 类型要求 dataRecord<string, unknown>,可以构造一个符合类型的对象。


[P2-2] node-editor 隐藏 span 传递 propshack

文件node-editor.tsx:152

<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 遗留)

文件publish-service.ts:83-85

问题:性能差,且不支持 Date 等特殊类型。

建议:用 structuredClone()


[P2-4] exercise-block 用 as never 类型断言v2 遗留)

文件exercise-block.tsx:52

update({ purpose: e.target.value as never })

建议:用 as ExercisePurpose 并添加类型守卫。


[P2-5] 多个组件用 alert()/confirm()v2 遗留)

文件

问题:阻塞主线程,不符合现代 Web UI 规范。

建议:使用 AlertDialog 组件或 sonner toast。


[P2-6] text-study-block 选区计算错误

文件text-study-block.tsx:29-37

问题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 不一致

文件page.tsx:24-25

<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 - 最终状态