# 备课模块(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 变化未触发自动保存) **文件**:[node-editor.tsx:59-74](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/node-editor.tsx#L59-L74) **现象**:拖拽节点改变位置后,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](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/lesson-plan-editor.tsx#L65-L68) **现象**:用户点击节点选中 → 侧边面板打开 → 点击面板关闭按钮 → 再次点击同一节点,面板不会重新打开。 **原因**: ```tsx 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](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/inline-question-editor.tsx#L22) **现象**:课案内新建题目无法关联知识点。 **原因**:`kpIds` 被硬编码为常量空数组: ```tsx const kpIds: string[] = []; ``` **修复建议**:添加知识点选择器 UI,或复用 `KnowledgePointPicker`。 --- ### [P1-4] exercise-block 用 index 作为 key(v2 遗留) **文件**:[exercise-block.tsx:67](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/blocks/exercise-block.tsx#L67) ```tsx {data.items.map((item, idx) => (
``` **问题**:删除/排序时可能导致 React 状态错乱。 **修复建议**:用 `item.questionId` 作为 key。 --- ### [P1-5] lesson-plan-card 用 window.location.reload()(v2 遗留) **文件**:[lesson-plan-card.tsx:39,50](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/lesson-plan-card.tsx#L39) **问题**:不符合 SPA 模式,导致整个页面重新加载。 **修复建议**:用 `useRouter().refresh()`。 --- ## 四、P2 代码规范问题 ### [P2-1] node-editor 用 `as unknown as Record` 类型断言 **文件**:[node-editor.tsx:42](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/node-editor.tsx#L42) ```tsx data: n as unknown as Record, ``` **问题**:双重断言绕过类型检查,违反"禁止 as 断言"规范。 **建议**:React Flow 的 `Node` 类型要求 `data` 为 `Record`,可以构造一个符合类型的对象。 --- ### [P2-2] node-editor 隐藏 span 传递 props(hack) **文件**:[node-editor.tsx:152](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/node-editor.tsx#L152) ```tsx ``` **问题**:用隐藏 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](file:///e:/Desktop/CICD/src/modules/lesson-preparation/publish-service.ts#L83-L85) **问题**:性能差,且不支持 Date 等特殊类型。 **建议**:用 `structuredClone()`。 --- ### [P2-4] exercise-block 用 `as never` 类型断言(v2 遗留) **文件**:[exercise-block.tsx:52](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/blocks/exercise-block.tsx#L52) ```tsx update({ purpose: e.target.value as never }) ``` **建议**:用 `as ExercisePurpose` 并添加类型守卫。 --- ### [P2-5] 多个组件用 alert()/confirm()(v2 遗留) **文件**: - [version-history-drawer.tsx:46](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/version-history-drawer.tsx#L46) - [lesson-plan-card.tsx:48](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/lesson-plan-card.tsx#L48) - [inline-question-editor.tsx:26](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/inline-question-editor.tsx#L26) - [text-study-block.tsx:42](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/blocks/text-study-block.tsx#L42) **问题**:阻塞主线程,不符合现代 Web UI 规范。 **建议**:使用 `AlertDialog` 组件或 `sonner` toast。 --- ### [P2-6] text-study-block 选区计算错误 **文件**:[text-study-block.tsx:29-37](file:///e:/Desktop/CICD/src/modules/lesson-preparation/components/blocks/text-study-block.tsx#L29-L37) **问题**:`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](file:///e:/Desktop/CICD/src/app/(dashboard)/teacher/lesson-plans/page.tsx#L24-L25) ```tsx

My Lesson Plans

Manage your lesson preparation and teaching plans.

``` **问题**:项目其他页面用中文,此处用英文。 **建议**:改为"我的备课"和"管理备课和教学计划"。 --- ## 六、架构合规性检查 | 检查项 | 状态 | 说明 | |--------|------|------| | 三层架构(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` - 最终状态