- 新增 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 以包含学生端页面
35 KiB
教材(Textbooks)模块审计报告
审计日期:2026-06-22 审计范围:
src/modules/textbooks/**、src/app/(dashboard)/teacher/textbooks/**、src/app/(dashboard)/student/learning/textbooks/**参照规则:docs/architecture/004_architecture_impact_map.md、docs/architecture/005_architecture_data.json、.trae/rules/project_rules.md
一、现有实现概要
1.1 文件分布
教材模块作为 K12 系统的"标杆模块"(架构图原文),文件分布如下:
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-access(RSC),未包装成 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 模块任何对
CreateQuestionDialogprops/位置的变更都会破坏教材模块编译;无法独立测试、独立部署教材模块;新增 admin/parent 角色时无法替换该弹窗实现。
问题 2.1.2 | 前端权限硬编码 canEdit(P0)
- 位置:
- teacher/textbooks/[id]/page.tsx#L60:
canEdit={true} - student/learning/textbooks/[id]/page.tsx#L58:未传
canEdit(默认false)
- teacher/textbooks/[id]/page.tsx#L60:
- 违反规则:项目规则"前端权限判断统一使用
usePermission().hasPermission(),严禁出现role === "xxx"硬编码"。此处虽未出现role ===,但用"路由前缀"(teacher/student)隐式决定编辑权,本质等价于角色硬编码。 - 原因:图省事直接按路由写死布尔值,未接入权限上下文。
- 后果:一旦 admin 也需编辑教材、或 teacher 在某些场景被回收
TEXTBOOK_UPDATE,前端仍会展示编辑按钮,造成"按钮可见但点击 403"的体验;权限策略变更需改多处代码。
问题 2.1.3 | data-access 缺少数据范围过滤(P1)
- 位置:data-access.ts#L75
getTextbooks、#L125getTextbookById - 现象:查询未结合当前用户身份(年级、班级、学科权限)做过滤,任何能进入路由的用户都能读到全量教材。
- 违反规则:项目规则"所有敏感数据查询必须在 data-access 层结合当前用户权限过滤"。
- 原因:学生端页面虽调用
getCurrentStudentUser(),但拿到的 student 信息并未用于过滤教材(如按学生年级筛选)。 - 后果:跨年级学生可看到非本年级教材;多租户场景下数据越权。
2.2 国际化(i18n)
问题 2.2.1 | 全模块零 i18n 覆盖(P0)
- 位置:模块全部 19 个源文件
- 现象:项目已接入 next-intl(见 i18n/request.ts),但教材模块没有任何一处使用
useTranslations/getTranslations,所有文案硬编码,且中英文混杂:- 中文硬编码:
"章节目录"、"知识点"、"图谱"、"请选择一个章节查看知识点。"、"该章节暂无知识点。"、"添加知识点"、"取消"、"删除"、"保存"、"确认删除"、"确定要删除这个知识点吗?此操作无法撤销。"、"创建中..."、"保存中..."、"知识点已创建"、"发生错误"、"删除失败"、"更新失败"、"返回教材列表"等(textbook-reader.tsx、knowledge-point-list.tsx、knowledge-point-dialogs.tsx、use-knowledge-point-actions.ts、teacher/textbooks/[id]/page.tsx) - 英文硬编码:
"Textbooks"、"Manage your digital curriculum resources and chapters."、"Add Textbook"、"Add New Textbook"、"Create a new digital textbook."、"Save changes"、"Search by title, publisher..."、"All Subjects"、"All Grades"、"Subject"、"Grade"、"Publisher"、"Title"、"Chapters"、"Updated"、"Edit Content"、"Delete"、"Settings"、"Textbook Settings"、"Delete Textbook"、"Add Chapter"、"Add Knowledge Point"、"Knowledge Points"、"No points yet"、"Select a chapter to manage knowledge points"等(textbook-filters.tsx、textbook-form-dialog.tsx、textbook-settings-dialog.tsx、textbook-card.tsx、knowledge-point-panel.tsx)
- 中文硬编码:
- 违反规则:项目规则"所有用户可见文本必须适配 i18n(使用 next-intl),提取翻译键"。
- 原因:模块开发时未跟进 i18n 改造,文案随写随定。
- 后果:无法切换语言;同一界面中英混杂,专业度差;后续做国际化需返工全部组件。
2.3 类型安全
问题 2.3.1 | 非空断言与 as 断言(P1)
- 位置:
- chapter-sidebar-list.tsx#L141:
items={chapter.children!}—— 已在hasChildren守卫后仍用!,应改用 narrowing。 - knowledge-graph.tsx#L105:
positions.get(kp.parentId as string)!——as string+!双重断言。 - knowledge-graph.tsx#L106:
positions.get(kp.id)!
- chapter-sidebar-list.tsx#L141:
- 违反规则:项目规则"禁止
as断言(除非从unknown转换)"、"可选链后禁止跟非空断言!"。 - 后果:运行时若数据不一致(如 parentId 指向已删除节点),直接抛错而非优雅降级。
问题 2.3.2 | data-access.ts 使用 select() 无类型投影(P2)
- 位置:data-access.ts#L413:
db.select().from(chapters) - 现象:
select()不传参数返回整行,类型推断为全表 schema,与模块对外Chapter类型不完全一致(如content可空性)。 - 后果:类型边界模糊,后续 schema 变更可能静默破坏调用方。
2.4 错误与边界处理
问题 2.4.1 | 缺少 React Error Boundary(P1)
- 位置:
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#L52:
if (!confirm("Are you sure..."))使用浏览器原生confirm - 现象:模块内其他删除(章节、知识点)均用
AlertDialog,唯独教材删除用confirm()。 - 违反规则:项目规则"组合优先"与 UI 一致性;
confirm()阻塞主线程且不可定制样式。 - 后果:交互体验割裂;移动端
confirm表现不一。
问题 2.4.3 | 空状态文案与组件不统一(P2)
- 位置:
- textbook-reader.tsx#L222:内联
<div>请选择一个章节查看知识点。</div> - knowledge-point-list.tsx#L32:内联
<div>该章节暂无知识点。</div> - textbook-content-panel.tsx#L67:内联
<div>请选择一个章节开始阅读。</div> - 列表页则用
EmptyState组件
- textbook-reader.tsx#L222:内联
- 后果:同一模块内空状态有三种写法,维护成本高,a11y 属性缺失。
2.5 组件复用与组合
问题 2.5.1 | 知识点列表/面板存在重复实现(P1)
- 位置:
- knowledge-point-list.tsx(107 行,被
TextbookReader使用) - knowledge-point-panel.tsx(157 行,未被任何页面引用,疑似旧版遗留)
- knowledge-point-list.tsx(107 行,被
- 现象:两个组件职责几乎相同(展示章节知识点 + 删除),
KnowledgePointPanel还自带router.refresh(),但实际无调用方。 - 违反规则:项目规则"最大化复用"。
- 后果:死代码增加认知负担;修改知识点展示逻辑需同步两处。
问题 2.5.2 | 创建知识点弹窗存在两套实现(P1)
- 位置:
- create-knowledge-point-dialog.tsx(独立弹窗,被
KnowledgePointPanel引用,但KnowledgePointPanel本身无调用方) - knowledge-point-dialogs.tsx#L56-L85(内嵌创建弹窗,被
TextbookReader使用)
- create-knowledge-point-dialog.tsx(独立弹窗,被
- 现象:两套创建知识点弹窗,文案一中一英,字段一致但实现独立。
- 后果:同上,双份维护。
问题 2.5.3 | 学科/年级选项硬编码三处(P1)
- 位置:
- textbook-filters.tsx#L43-L66:Select 选项
- textbook-form-dialog.tsx#L89-L113:Select 选项(且 form 与 settings 的学科列表不一致:form 含 Biology/Geography,settings 缺这两项)
- textbook-settings-dialog.tsx#L106-L112:Select 选项
- textbook-card.tsx#L26-L34:
subjectColorMap学科颜色映射
- 现象:学科、年级枚举在 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)
- 位置:
- data-access.ts#L29-L73
sortChapters/buildChapterTree(模块内未导出) - knowledge-graph.tsx#L29-L117
computeGraphLayout(模块内未导出) - textbook-reader.tsx#L32-L44
buildChapterIndex
- data-access.ts#L29-L73
- 现象:这些纯函数(树构建、图布局、索引构建)是核心逻辑,但未导出,无法写单测;模块目录下无任何
__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)
- 位置:textbook-content-panel.tsx#L118 用了
rehype-sanitize(✅),但 RichTextEditor 输出未在保存前清洗。 - 后果:依赖前端 sanitize,一旦渲染端配置变更可能被绕过。
三、行业差距对比
对标国内外主流 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(紧急,阻塞多角色上线)
- 解耦跨模块 UI 依赖:将
KnowledgePointDialogs中对CreateQuestionDialog的直接 import 改为通过 props 注入(render prop 或 children),由页面层决定渲染哪个题目创建组件;或定义QuestionCreator接口,由 questions 模块实现并通过 Context 注入。 - 接入前端权限 Hook:删除
canEdit={true}硬编码,在TextbookReader内部调用usePermission().hasPermission(Permissions.TEXTBOOK_UPDATE)决定编辑按钮可见性;列表页"新增教材"按钮同理用TEXTBOOK_CREATE控制。 - 全模块 i18n 改造:新增
shared/i18n/messages/{en,zh-CN}/textbooks.json命名空间,提取所有硬编码文案;Server Component 用getTranslations,Client Component 用useTranslations;统一中英文混杂问题。 - Server Action 资源归属校验:在
updateChapterContentAction/deleteChapterAction/createKnowledgePointAction等 Action 内,先校验chapterId所属textbookId与传入textbookId一致,并结合当前用户身份做二次校验。
P1(重要,影响正确性与可维护性)
- data-access 加数据范围过滤:
getTextbooks接受scope参数(年级/班级/学科),学生端按学生年级过滤;getTextbookById校验访问权。 - 补齐 Error Boundary:在
teacher/textbooks/[id]与student/learning/textbooks/[id]下新增error.tsx;TextbookReader内对章节区、知识点区、图谱区分别用 Error Boundary 包裹。 - 消除重复组件:删除未使用的
knowledge-point-panel.tsx与create-knowledge-point-dialog.tsx;统一知识点列表与创建弹窗为单一实现。 - 抽取学科/年级配置:新建
src/modules/textbooks/constants.ts,集中导出SUBJECTS、GRADES、SUBJECT_COLORS,供 filters/form/settings/card 复用,消除不一致。 - 导出纯函数并补单测:导出
buildChapterTree/sortChapters/computeGraphLayout/buildChapterIndex,补 Vitest 单测覆盖空数组、单节点、深层嵌套、循环引用等边界。 - 修复类型断言:用类型守卫替换
!与as,例如chapter.children!改为hasChildren ? <RecursiveSortableList items={chapter.children} /> : null。 - 图谱 a11y:svg 加
role="img"+aria-label;节点加aria-label={node.name};支持方向键导航。 - 统一删除确认:
textbook-settings-dialog.tsx的confirm()改为AlertDialog,与模块其他删除一致。
P2(优化,提升体验与专业度)
- 统一空状态:内联空状态全部改用
EmptyState组件,补 a11y。 - 知识点高亮性能优化:改用一次 AST 遍历(基于 remark 插件)替换正则全局替换,避免跨标签误伤。
- 知识点懒加载:详情页仅加载当前章节知识点,切换章节时按需加载。
- 移动端阅读优化:窄屏下三栏改为抽屉式(章节侧栏可滑出)。
- 补全架构图同步(见第五节)。
- 埋点接口预留:在
data-access与actions中预留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-access(getTextbooks / 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.tsx→questions/components/create-question-dialog
应在 004 的依赖关系图与 005 的 dependencyMatrix 中补充该 UI 层依赖,并标注为"待解耦(P0)"。
5.4 未记录的跨模块 data-access 调用方
getKnowledgePointOptions(data-access 导出)被 questions 模块调用,架构图已记录(§2.4 questions 依赖 textbooks data-access),但 005 JSON 中 textbooks 节点的 exports 字段未列出该函数。建议补充。
5.5 建议的 JSON 节点更新
005_architecture_data.json 中 modules.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 依赖 CreateQuestionDialog(P0)",
"前端权限硬编码 canEdit(P0)",
"全模块零 i18n(P0)",
"Server Action 未校验资源归属(P1)",
"data-access 缺数据范围过滤(P1)",
"缺 Error Boundary(P1)",
"知识点列表/弹窗重复实现(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
}
通过 TextbookDataProvider(React 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。