Files
NextEdu/docs/architecture/audit/textbooks-audit-report.md
SpecialX 2548f70f40 docs(textbooks): 新增教材模块审计报告并同步架构图
- 新增 docs/architecture/audit/textbooks-audit-report.md,覆盖三层架构、权限、i18n、类型安全、错误边界、组件复用、a11y、可测试性、性能、安全等维度的审计,并给出 P0/P1/P2 改进优先级与重构方案要点

- 同步 004 架构影响地图 §2.5:修正 actions/data-access 行数与导出函数名(移除不存在的读 Action,补充 reorderChaptersAction),补充跨模块 UI 依赖、已知问题清单

- 同步 005 架构数据 JSON:补充 getKnowledgePointOptions 跨模块接口、uiDeps、knownIssues、auditReport 字段,修正 getTextbooks/getTextbookById 的 usedBy 以包含学生端页面
2026-06-22 15:38:26 +08:00

35 KiB
Raw Permalink Blame History

教材Textbooks模块审计报告

审计日期2026-06-22 审计范围:src/modules/textbooks/**src/app/(dashboard)/teacher/textbooks/**src/app/(dashboard)/student/learning/textbooks/** 参照规则:docs/architecture/004_architecture_impact_map.mddocs/architecture/005_architecture_data.json.trae/rules/project_rules.md


一、现有实现概要

1.1 文件分布

教材模块作为 K12 系统的"标杆模块"(架构图原文),文件分布如下:

文件 行数 职责
数据访问 data-access.ts 514 教材/章节/知识点 CRUD + 跨模块查询接口
Server Actions actions.ts 317 13 个 Server Action含权限校验
类型 types.ts 45 Textbook / Chapter / KnowledgePoint 类型
校验 schema.ts 64 Zod 校验 schema
Hook hooks/use-knowledge-point-actions.ts 121 知识点增删改状态机
Hook hooks/use-text-selection.ts 57 文本选区捕获
组件 components/textbook-reader.tsx 319 阅读器主壳Tabs目录/知识点/图谱)
组件 components/textbook-content-panel.tsx 170 Markdown 渲染 + 编辑切换
组件 components/chapter-sidebar-list.tsx 348 递归章节树 + 拖拽排序
组件 components/knowledge-point-list.tsx 107 知识点列表
组件 components/knowledge-graph.tsx 181 知识图谱 SVG 可视化
组件 components/knowledge-point-panel.tsx 157 知识点面板(旧版,与 list 重叠)
组件 components/knowledge-point-dialogs.tsx 148 创建/编辑知识点弹窗集合
组件 components/textbook-card.tsx 121 教材卡片
组件 components/textbook-filters.tsx 71 筛选栏
组件 components/textbook-form-dialog.tsx 134 新建教材弹窗
组件 components/textbook-settings-dialog.tsx 160 教材设置/删除弹窗
组件 components/create-chapter-dialog.tsx 95 新建章节弹窗
组件 components/create-knowledge-point-dialog.tsx 95 新建知识点弹窗(旧版)
页面 teacher/textbooks/page.tsx 68 教师端列表页RSC
页面 teacher/textbooks/[id]/page.tsx 65 教师端详情页RSC
页面 student/learning/textbooks/page.tsx 66 学生端列表页RSC
页面 student/learning/textbooks/[id]/page.tsx 64 学生端详情页RSC
骨架屏 4 个 loading.tsx 列表/详情骨架屏

1.2 数据流

page.tsx (RSC)
  └─ getTextbooks / getTextbookById / getChaptersByTextbookId / getKnowledgePointsByTextbookId  (data-access)
       └─ db (drizzle) → textbooks / chapters / knowledgePoints 表
  └─ <TextbookReader> (client)
       ├─ <ChapterSidebarList>  → deleteChapterAction / reorderChaptersAction
       ├─ <TextbookContentPanel> → updateChapterContentAction
       ├─ <KnowledgePointList>   → useKnowledgePointActions → create/update/deleteKnowledgePointAction
       └─ <KnowledgePointDialogs> → ⚠️ 直接 import @/modules/questions/components/create-question-dialog

1.3 架构图记录完整性

经核对 004_architecture_impact_map.md §2.5 与 005_architecture_data.json,架构图对教材模块的记录存在以下偏差(详见第五节):

  • 行数统计过期:图记 actions.ts 276 行 / data-access.ts 428 行,实际为 317 / 514
  • 导出函数名错误:图记 getTextbooksAction / getTextbookByIdAction / getChaptersAction / getKnowledgePointsAction 等"读 Action",实际不存在——读操作直接走 data-accessRSC未包装成 Action。
  • 组件文件数:图记"12 文件",实际 11 个组件文件。
  • 未记录跨模块 UI 依赖:knowledge-point-dialogs.tsx 直接 import questions 模块的 CreateQuestionDialog,图未标注。

二、现存问题与原因分析

2.1 架构解耦

问题 2.1.1 跨模块直接 import 业务组件P0

  • 位置knowledge-point-dialogs.tsx#L16
  • 现象import { CreateQuestionDialog } from "@/modules/questions/components/create-question-dialog"
  • 违反规则:项目规则"该模块必须作为独立功能单元……模块内部组件绝不直接 import 其他业务模块的 actions 或 data-access只能通过注入的接口调用"以及"模块间只能通过对方 data-access 通信"。
  • 原因:教材知识点页希望"一键创建相关题目",直接耦合了 questions 模块的弹窗组件,而非通过接口注入或事件回调。
  • 后果questions 模块任何对 CreateQuestionDialog props/位置的变更都会破坏教材模块编译;无法独立测试、独立部署教材模块;新增 admin/parent 角色时无法替换该弹窗实现。

问题 2.1.2 前端权限硬编码 canEditP0

  • 位置
  • 违反规则:项目规则"前端权限判断统一使用 usePermission().hasPermission(),严禁出现 role === "xxx" 硬编码"。此处虽未出现 role ===,但用"路由前缀"teacher/student隐式决定编辑权本质等价于角色硬编码。
  • 原因:图省事直接按路由写死布尔值,未接入权限上下文。
  • 后果:一旦 admin 也需编辑教材、或 teacher 在某些场景被回收 TEXTBOOK_UPDATE,前端仍会展示编辑按钮,造成"按钮可见但点击 403"的体验;权限策略变更需改多处代码。

问题 2.1.3 data-access 缺少数据范围过滤P1

  • 位置data-access.ts#L75 getTextbooks#L125 getTextbookById
  • 现象:查询未结合当前用户身份(年级、班级、学科权限)做过滤,任何能进入路由的用户都能读到全量教材。
  • 违反规则:项目规则"所有敏感数据查询必须在 data-access 层结合当前用户权限过滤"。
  • 原因:学生端页面虽调用 getCurrentStudentUser(),但拿到的 student 信息并未用于过滤教材(如按学生年级筛选)。
  • 后果:跨年级学生可看到非本年级教材;多租户场景下数据越权。

2.2 国际化i18n

问题 2.2.1 全模块零 i18n 覆盖P0

  • 位置:模块全部 19 个源文件
  • 现象:项目已接入 next-intli18n/request.ts),但教材模块没有任何一处使用 useTranslations / getTranslations,所有文案硬编码,且中英文混杂:
  • 违反规则:项目规则"所有用户可见文本必须适配 i18n使用 next-intl提取翻译键"。
  • 原因:模块开发时未跟进 i18n 改造,文案随写随定。
  • 后果:无法切换语言;同一界面中英混杂,专业度差;后续做国际化需返工全部组件。

2.3 类型安全

问题 2.3.1 非空断言与 as 断言P1

  • 位置
  • 违反规则:项目规则"禁止 as 断言(除非从 unknown 转换)"、"可选链后禁止跟非空断言 !"。
  • 后果:运行时若数据不一致(如 parentId 指向已删除节点),直接抛错而非优雅降级。

问题 2.3.2 data-access.ts 使用 select() 无类型投影P2

  • 位置data-access.ts#L413db.select().from(chapters)
  • 现象select() 不传参数返回整行,类型推断为全表 schema与模块对外 Chapter 类型不完全一致(如 content 可空性)。
  • 后果:类型边界模糊,后续 schema 变更可能静默破坏调用方。

2.4 错误与边界处理

问题 2.4.1 缺少 React Error BoundaryP1

  • 位置src/app/(dashboard)/teacher/textbooks/**src/app/(dashboard)/student/learning/textbooks/** 均无 error.tsx
  • 现象:详情页 getTextbookById 返回 undefined 时走 notFound(),但章节/知识点查询失败、Server Action 抛错时整页崩溃,无降级 UI。
  • 违反规则:项目规则"每个独立的数据区块必须用 React Error Boundary 包裹"。
  • 后果:一次 DB 抖动导致整个阅读器白屏,无法隔离故障域。

问题 2.4.2 删除确认交互不一致P2

  • 位置textbook-settings-dialog.tsx#L52if (!confirm("Are you sure...")) 使用浏览器原生 confirm
  • 现象:模块内其他删除(章节、知识点)均用 AlertDialog,唯独教材删除用 confirm()
  • 违反规则:项目规则"组合优先"与 UI 一致性;confirm() 阻塞主线程且不可定制样式。
  • 后果:交互体验割裂;移动端 confirm 表现不一。

问题 2.4.3 空状态文案与组件不统一P2

2.5 组件复用与组合

问题 2.5.1 知识点列表/面板存在重复实现P1

  • 位置
  • 现象:两个组件职责几乎相同(展示章节知识点 + 删除),KnowledgePointPanel 还自带 router.refresh(),但实际无调用方。
  • 违反规则:项目规则"最大化复用"。
  • 后果:死代码增加认知负担;修改知识点展示逻辑需同步两处。

问题 2.5.2 创建知识点弹窗存在两套实现P1

  • 位置
  • 现象:两套创建知识点弹窗,文案一中一英,字段一致但实现独立。
  • 后果:同上,双份维护。

问题 2.5.3 学科/年级选项硬编码三处P1

  • 位置
  • 现象:学科、年级枚举在 4 个文件里各写一份,且彼此不一致settings 弹窗的学科列表少了 Biology 和 Geography
  • 违反规则:项目规则"最大化复用……抽象为泛型组件和 hooks"、"配置驱动设计"。
  • 后果:新增学科需改 4 处;当前已出现数据不一致——用户在 form 里能选 Biology但 settings 里看不到,编辑时学科被覆盖。

2.6 可访问性a11y

问题 2.6.1 知识图谱 SVG 缺少无障碍属性P1

  • 位置knowledge-graph.tsx#L142-L158
  • 现象<svg>role="img"、无 aria-label、无 <title>;节点用 <button> 但无 aria-label 描述跳转目标。
  • 违反规则:项目规则"可访问性a11y语义化标签、ARIA 属性、键盘导航"。
  • 后果:屏幕阅读器用户无法理解图谱内容。

问题 2.6.2 图谱节点不支持键盘导航P2

  • 位置knowledge-graph.tsx#L159
  • 现象:节点用绝对定位 <button>,但无 tabIndex 管理、无方向键导航Tab 顺序混乱。
  • 后果:键盘用户难以在图谱中移动焦点。

2.7 可测试性

问题 2.7.1 纯逻辑未导出无法单测P1

  • 位置
  • 现象:这些纯函数(树构建、图布局、索引构建)是核心逻辑,但未导出,无法写单测;模块目录下无任何 __tests__*.test.ts
  • 违反规则:项目规则"数据获取、计算、格式化等纯逻辑全部放入纯函数或 hooks与 UI 分离;导出清晰的接口类型以便 mock"。
  • 后果:章节树构建、图谱布局这类容易出 bug 的算法无回归保护。

问题 2.7.2 零测试覆盖P1

  • 位置:整个模块
  • 现象:无单元测试、无集成测试、无 e2e 测试。
  • 后果:重构高风险。

2.8 性能

问题 2.8.1 知识点高亮用正则全局替换存在性能与正确性风险P2

  • 位置textbook-reader.tsx#L153-L165
  • 现象processedContent 对每个知识点名做 new RegExp(..., "gi") 全局替换O(n×m) 复杂度;且未处理知识点名互为子串的情况(已按长度降序缓解,但仍可能误伤)。
  • 后果:章节内容长、知识点多时主线程卡顿;高亮可能跨标签边界破坏 Markdown。

问题 2.8.2 getKnowledgePointsByTextbookId 一次性拉全量P2

  • 位置data-access.ts#L357
  • 现象:详情页一次性加载整本教材所有章节的知识点,无分页/懒加载。
  • 后果:大体量教材首屏慢。

2.9 安全性

问题 2.9.1 Server Action 未校验资源归属P1

  • 位置actions.ts 全部 Action
  • 现象updateChapterContentAction(chapterId, content, textbookId) 仅校验 TEXTBOOK_UPDATE 权限,未校验 chapterId 是否属于当前用户有权访问的教材。
  • 违反规则:项目规则"Server Action 二次校验"。
  • 后果:教师 A 可通过改 chapterId 篡改教师 B 的章节内容(越权写)。

问题 2.9.2 Markdown 渲染虽用 sanitize但编辑端无 XSS 过滤P2


三、行业差距对比

对标国内外主流 K12 教育平台如人教数字教材、ClassIn、Seewo、Khan Academy、好未来"学而思"教材体系)在教材模块的设计,本模块存在以下差距:

3.1 内容呈现层

行业优秀实践 本模块现状 影响
支持富媒体嵌入(图片/音频/视频/公式/交互式 3D 模型) 仅 Markdown 文本 + RichTextEditor 理科教材无法呈现实验视频、几何图形、化学方程式K12 教学场景严重受限
公式编辑LaTeX / MathML 数学/物理教材无法正确呈现公式
页面翻阅式阅读(带页码、书签、进度记忆) 仅滚动 + URL chapterId 学生阅读进度无持久化,无法"续读"
朗读 / TTS 朗读 低年级学生、视障学生体验差
笔记/划线/高亮/书签 仅有"选区创建知识点" 学生无法在教材上做个人笔记,教师无法布置"精读"任务

3.2 知识体系层

行业优秀实践 本模块现状 影响
知识图谱支持缩放/拖拽/力导向布局/关联题目预览 静态 SVG 树状布局,无交互(无缩放、无拖拽、无关联题目) 图谱仅"能看",不能"用",无法支撑知识图谱驱动的个性化学习
知识点与题目/作业/考试双向关联,支持"知识点掌握度"雷达 仅单向"知识点→创建题目"入口 无法做学情诊断、薄弱知识点推送
知识点支持多级层级、跨章节关联、前置/后置依赖 parentId 树 + chapterId 归属 无法表达"学习路径",无法做前置知识校验

3.3 多角色协作层

行业优秀实践 本模块现状 影响
admin统一教材库 + 多教师协作编辑 + 版本历史 仅 teacher 单人编辑,无版本管理 多教师同改一本教材会互相覆盖,无回滚能力
parent查看孩子教材进度、笔记 完全缺失 parent 角色 parent 无法了解孩子学习内容
student教材 + 笔记 + 作业联动 仅只读阅读 学生无法在教材上做标记、无法跳转到对应作业
教研组:教材模板复用、章节共享 无模板/共享机制 同学科同年级教材重复建设

3.4 交互体验层

行业优秀实践 本模块现状 影响
章节拖拽支持跨级移动 reorderChapters 仅支持同级排序,跨级需先删后建 教材结构调整效率低
全文搜索(章节标题 + 正文 + 知识点) 仅列表页按 title/subject/grade/publisher 模糊搜索 学生无法"在教材里搜概念"
离线下载 / 移动端适配 阅读器布局在窄屏下三栏堆叠,未做移动端阅读优化 移动端体验差K12 学生主要用平板/手机
阅读进度条 / 章节完成度 无法量化学习进度

3.5 数据分析层

行业优秀实践 本模块现状 影响
教材使用统计(阅读时长、热门章节、知识点停留) 无埋点 无法为教研提供数据支撑
知识点难度标注 / 教师标注重点 仅有 level 字段但无 UI 录入 无法做分层教学

四、改进优先级建议

P0紧急阻塞多角色上线

  1. 解耦跨模块 UI 依赖:将 KnowledgePointDialogs 中对 CreateQuestionDialog 的直接 import 改为通过 props 注入render prop 或 children由页面层决定渲染哪个题目创建组件或定义 QuestionCreator 接口,由 questions 模块实现并通过 Context 注入。
  2. 接入前端权限 Hook:删除 canEdit={true} 硬编码,在 TextbookReader 内部调用 usePermission().hasPermission(Permissions.TEXTBOOK_UPDATE) 决定编辑按钮可见性;列表页"新增教材"按钮同理用 TEXTBOOK_CREATE 控制。
  3. 全模块 i18n 改造:新增 shared/i18n/messages/{en,zh-CN}/textbooks.json 命名空间提取所有硬编码文案Server Component 用 getTranslationsClient Component 用 useTranslations;统一中英文混杂问题。
  4. Server Action 资源归属校验:在 updateChapterContentAction / deleteChapterAction / createKnowledgePointAction 等 Action 内,先校验 chapterId 所属 textbookId 与传入 textbookId 一致,并结合当前用户身份做二次校验。

P1重要影响正确性与可维护性

  1. data-access 加数据范围过滤getTextbooks 接受 scope 参数(年级/班级/学科),学生端按学生年级过滤;getTextbookById 校验访问权。
  2. 补齐 Error Boundary:在 teacher/textbooks/[id]student/learning/textbooks/[id] 下新增 error.tsxTextbookReader 内对章节区、知识点区、图谱区分别用 Error Boundary 包裹。
  3. 消除重复组件:删除未使用的 knowledge-point-panel.tsxcreate-knowledge-point-dialog.tsx;统一知识点列表与创建弹窗为单一实现。
  4. 抽取学科/年级配置:新建 src/modules/textbooks/constants.ts,集中导出 SUBJECTSGRADESSUBJECT_COLORS,供 filters/form/settings/card 复用,消除不一致。
  5. 导出纯函数并补单测:导出 buildChapterTree / sortChapters / computeGraphLayout / buildChapterIndex,补 Vitest 单测覆盖空数组、单节点、深层嵌套、循环引用等边界。
  6. 修复类型断言:用类型守卫替换 !as,例如 chapter.children! 改为 hasChildren ? <RecursiveSortableList items={chapter.children} /> : null
  7. 图谱 a11ysvg 加 role="img" + aria-label;节点加 aria-label={node.name};支持方向键导航。
  8. 统一删除确认textbook-settings-dialog.tsxconfirm() 改为 AlertDialog,与模块其他删除一致。

P2优化提升体验与专业度

  1. 统一空状态:内联空状态全部改用 EmptyState 组件,补 a11y。
  2. 知识点高亮性能优化:改用一次 AST 遍历(基于 remark 插件)替换正则全局替换,避免跨标签误伤。
  3. 知识点懒加载:详情页仅加载当前章节知识点,切换章节时按需加载。
  4. 移动端阅读优化:窄屏下三栏改为抽屉式(章节侧栏可滑出)。
  5. 补全架构图同步(见第五节)。
  6. 埋点接口预留:在 data-accessactions 中预留 onTextbookView / onChapterRead 钩子,供后续接入监控。

五、架构图同步说明

本次审计发现 004_architecture_impact_map.md §2.5 与 005_architecture_data.json 中教材模块节点存在以下偏差,需同步修正:

5.1 行数统计过期

文件 图记行数 实际行数
actions.ts 276 317
data-access.ts 428 514
types.ts 79 45
hooks/use-knowledge-point-actions.ts 121 121一致
组件文件数 12 11

5.2 导出函数名错误

架构图 §2.5 记录的 Actions 列表含 getTextbooksAction / getTextbookByIdAction / getChaptersAction / getKnowledgePointsAction实际不存在。读操作直接由 RSC 页面调用 data-accessgetTextbooks / getTextbookById / getChaptersByTextbookId / getKnowledgePointsByTextbookId / getKnowledgePointsByChapterId),未包装成 Server Action。实际 Actions 为:

createTextbookAction / updateTextbookAction / deleteTextbookAction
createChapterAction / updateChapterContentAction / deleteChapterAction / reorderChaptersAction
createKnowledgePointAction / updateKnowledgePointAction / deleteKnowledgePointAction

5.3 未记录的跨模块 UI 依赖

架构图标注教材为"标杆模块(无跨模块 DB 访问)",这一结论对 data-access 层成立,但组件层存在跨模块 UI 依赖未记录:

  • textbooks/components/knowledge-point-dialogs.tsxquestions/components/create-question-dialog

应在 004 的依赖关系图与 005 的 dependencyMatrix 中补充该 UI 层依赖,并标注为"待解耦P0"。

5.4 未记录的跨模块 data-access 调用方

getKnowledgePointOptionsdata-access 导出)被 questions 模块调用架构图已记录§2.4 questions 依赖 textbooks data-access但 005 JSON 中 textbooks 节点的 exports 字段未列出该函数。建议补充。

5.5 建议的 JSON 节点更新

005_architecture_data.jsonmodules.textbooks 节点建议补充/修正:

{
  "textbooks": {
    "exports": {
      "actions": [
        "createTextbookAction", "updateTextbookAction", "deleteTextbookAction",
        "createChapterAction", "updateChapterContentAction", "deleteChapterAction",
        "reorderChaptersAction",
        "createKnowledgePointAction", "updateKnowledgePointAction", "deleteKnowledgePointAction"
      ],
      "dataAccess": [
        "getTextbooks", "getTextbookById", "getChaptersByTextbookId",
        "getKnowledgePointsByChapterId", "getKnowledgePointsByTextbookId",
        "createTextbook", "updateTextbook", "deleteTextbook",
        "createChapter", "updateChapterContent", "deleteChapter",
        "createKnowledgePoint", "updateKnowledgePoint", "deleteKnowledgePoint",
        "reorderChapters", "getTextbooksDashboardStats",
        "getKnowledgePointOptions"  // 跨模块接口,供 questions 使用
      ]
    },
    "uiDeps": [
      "questions/components/create-question-dialog  // P0 待解耦"
    ],
    "files": {
      "actions.ts": 317,
      "data-access.ts": 514,
      "types.ts": 45,
      "schema.ts": 64,
      "components": 11
    },
    "knownIssues": [
      "跨模块 UI 依赖 CreateQuestionDialogP0",
      "前端权限硬编码 canEditP0",
      "全模块零 i18nP0",
      "Server Action 未校验资源归属P1",
      "data-access 缺数据范围过滤P1",
      "缺 Error BoundaryP1",
      "知识点列表/弹窗重复实现P1",
      "学科/年级选项硬编码且不一致P1",
      "纯逻辑未导出零单测P1"
    ]
  }
}

附:重构方案设计要点(不写实现代码)

为满足"完全解耦 / 组合优先 / 国际化就绪 / 最大化复用 / 错误与边界处理 / 可测试性 / 可扩展性 / 企业级补充"八项原则,建议按以下方向重构(详细实现留待后续任务):

A. 数据服务接口抽象

// textbooks/services/types.ts
export interface TextbookDataService {
  listTextbooks(query?: TextbookQuery): Promise<Textbook[]>
  getTextbook(id: string): Promise<Textbook | null>
  listChapters(textbookId: string): Promise<Chapter[]>
  listKnowledgePoints(textbookId: string): Promise<KnowledgePoint[]>
}

export interface TextbookMutationService {
  createTextbook(input: CreateTextbookInput): Promise<ActionState>
  updateTextbook(id: string, input: UpdateTextbookInput): Promise<ActionState>
  deleteTextbook(id: string): Promise<ActionState>
  // ...chapter / knowledgePoint mutations
}

通过 TextbookDataProviderReact Context注入不同角色实现teacher 实现 = 全量 + 可写student 实现 = 按年级过滤 + 只读admin 实现 = 全量 + 可写 + 可分配。

B. 配置驱动角色渲染

// textbooks/config/role-config.ts
export const TEXTBOOK_ROLE_CONFIG: Record<Role, TextbookRoleConfig> = {
  teacher: { canEdit: true, showStats: true, widgets: ['chapters','knowledge','graph','settings'] },
  student: { canEdit: false, showProgress: true, widgets: ['chapters','knowledge','graph','notes'] },
  admin:   { canEdit: true, showStats: true, showAudit: true, widgets: ['chapters','knowledge','graph','settings','audit'] },
  parent:  { canEdit: false, showChildProgress: true, widgets: ['chapters','progress'] },
}

TextbookReader 根据 useRoleConfig() 决定渲染哪些 Widget新增角色只改配置。

C. 组合式 UI

  • TextbookReader 改为 children-based 组合:<TextbookReader><ChapterSidebar /><ContentPanel /><KnowledgePanel /></TextbookReader>
  • 跨模块的"创建题目"入口改为 render prop<KnowledgePointList onCreateQuestion={renderQuestionCreator} />,由页面层注入 questions 模块组件,模块内部不 import questions。

D. i18n 翻译文件结构示例

shared/i18n/messages/
├─ en/textbooks.json
└─ zh-CN/textbooks.json
// zh-CN/textbooks.json
{
  "list": {
    "title": "教材",
    "subtitle": "管理数字课程资源与章节",
    "add": "新建教材",
    "empty": { "withFilters": "没有匹配的教材", "withoutFilters": "暂无教材" }
  },
  "reader": {
    "tabs": { "chapters": "章节目录", "knowledge": "知识点", "graph": "图谱" },
    "selectChapter": "请选择一个章节开始阅读",
    "emptyKnowledge": "该章节暂无知识点"
  },
  "dialog": {
    "create": { "title": "新建教材", "submit": "保存" },
    "settings": { "title": "教材设置", "delete": "删除教材" },
    "knowledge": { "create": "添加知识点", "edit": "编辑知识点" }
  },
  "field": {
    "title": "标题", "subject": "学科", "grade": "年级", "publisher": "出版社"
  },
  "subject": { "Mathematics": "数学", "Physics": "物理", /* ... */ },
  "grade": { "Grade 7": "七年级", /* ... */ }
}

E. 错误边界与骨架屏

  • 每个独立数据区块(章节树、内容区、知识点区、图谱区)用 <ErrorBoundary fallback={<ErrorState />}> 包裹
  • 异步加载用 <Suspense fallback={<TextbookReaderSkeleton />}>
  • 空状态、无权限、网络异常统一用 EmptyState / ForbiddenState / ErrorState 三套标准组件

F. 可测试性

  • 纯逻辑(buildChapterTree / computeGraphLayout / sortChapters / buildChapterIndex / processedContent 生成器)抽到 textbooks/utils/ 并导出
  • 数据服务接口便于 mock组件测试时注入 stub service
  • 补 Vitest 单测 + Playwright e2e列表筛选、章节拖拽、知识点创建三条核心路径

G. 监控埋点接口

export interface TextbookAnalytics {
  onTextbookOpen(textbookId: string): void
  onChapterRead(textbookId: string, chapterId: string, durationMs: number): void
  onKnowledgePointClick(kpId: string): void
}

通过 Context 注入,默认 no-op后续接入真实监控 SDK。