完整性更新
现在已经实现了大部分基础功能
This commit is contained in:
@@ -94,7 +94,120 @@ React 的 hydration 过程对 HTML 有效性要求极高。将 `div` 放入 `p`
|
||||
2. `teacher-stats.tsx`
|
||||
3. `teacher-schedule.tsx`
|
||||
|
||||
## 5. 下一步计划
|
||||
- 将 Mock Data 对接到真实的 API 端点 (React Server Actions)。
|
||||
- 实现 "Quick Actions" (快捷操作) 的具体功能。
|
||||
- 为 Submissions 和 Schedule 添加 "View All" (查看全部) 跳转导航。
|
||||
## 5. 更新记录(2026-01-04)
|
||||
- 教师仪表盘从 Mock Data 切换为真实数据查询:`/teacher/dashboard` 组合 `getTeacherClasses`、`getClassSchedule`、`getHomeworkSubmissions({ creatorId })` 渲染 KPI / 今日课表 / 最近提交。
|
||||
- Quick Actions 落地为真实路由跳转(创建作业、查看列表等)。
|
||||
- Schedule / Submissions 增加 “View All” 跳转到对应列表页(并携带筛选参数)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 教师端班级管理模块(真实数据接入记录)
|
||||
|
||||
**日期**: 2025-12-31
|
||||
**范围**: 教师端「我的班级 / 学生 / 课表」页面与 MySQL(Drizzle) 真数据对接
|
||||
|
||||
### 6.1 页面入口与路由
|
||||
|
||||
班级管理相关页面位于:
|
||||
- `src/app/(dashboard)/teacher/classes/my/page.tsx`
|
||||
- `src/app/(dashboard)/teacher/classes/students/page.tsx`
|
||||
- `src/app/(dashboard)/teacher/classes/schedule/page.tsx`
|
||||
|
||||
为避免构建期/预渲染阶段访问数据库导致失败,以上页面显式启用动态渲染:
|
||||
- `export const dynamic = "force-dynamic"`
|
||||
|
||||
### 6.2 模块结构(Vertical Slice)
|
||||
|
||||
班级模块采用垂直切片架构,代码位于 `src/modules/classes/`:
|
||||
```
|
||||
src/modules/classes/
|
||||
├── components/
|
||||
│ ├── my-classes-grid.tsx
|
||||
│ ├── students-filters.tsx
|
||||
│ ├── students-table.tsx
|
||||
│ ├── schedule-filters.tsx
|
||||
│ └── schedule-view.tsx
|
||||
├── data-access.ts
|
||||
└── types.ts
|
||||
```
|
||||
|
||||
其中 `data-access.ts` 负责班级、学生、课表三类查询的服务端数据读取,并作为页面层唯一的数据入口。
|
||||
|
||||
### 6.3 数据库表与迁移
|
||||
|
||||
新增班级领域表:
|
||||
- `classes`
|
||||
- `class_enrollments`
|
||||
- `class_schedule`
|
||||
|
||||
对应 Drizzle Schema:
|
||||
- `src/shared/db/schema.ts`
|
||||
- `src/shared/db/relations.ts`
|
||||
|
||||
对应迁移文件:
|
||||
- `drizzle/0003_petite_newton_destine.sql`
|
||||
|
||||
外键关系(核心):
|
||||
- `classes.teacher_id` -> `users.id`
|
||||
- `class_enrollments.class_id` -> `classes.id`
|
||||
- `class_enrollments.student_id` -> `users.id`
|
||||
- `class_schedule.class_id` -> `classes.id`
|
||||
|
||||
索引(核心):
|
||||
- `classes_teacher_idx`, `classes_grade_idx`
|
||||
- `class_enrollments_class_idx`, `class_enrollments_student_idx`
|
||||
- `class_schedule_class_idx`, `class_schedule_class_day_idx`
|
||||
|
||||
### 6.4 Seed 数据
|
||||
|
||||
Seed 脚本已覆盖班级相关数据,以便在开发环境快速验证页面渲染与关联关系:
|
||||
- `scripts/seed.ts`
|
||||
- 运行命令:`npm run db:seed`
|
||||
|
||||
### 6.5 开发过程中的问题与处理
|
||||
|
||||
- 端口占用(EADDRINUSE):开发服务器端口被占用时,通过更换端口启动规避(例如 `next dev -p <port>`)。
|
||||
- Next dev 锁文件:出现 `.next/dev/lock` 无法获取锁时,需要确保只有一个 dev 实例在运行,并清理残留 lock。
|
||||
- 头像资源 404:移除 Header 中硬编码的本地头像资源引用,避免 `public/avatars/...` 不存在导致的 404 噪音(见 `src/modules/layout/components/site-header.tsx`)。
|
||||
- 班级人数统计查询失败:`class_enrollments` 表实际列名为 `class_enrollment_status`,修复查询中引用的列名以恢复教师端班级列表渲染。
|
||||
|
||||
### 6.6 班级详情页(聚合视图 + Schedule Builder + Homework 统计)
|
||||
|
||||
**日期**: 2026-01-04
|
||||
**入口**: `src/app/(dashboard)/teacher/classes/my/[id]/page.tsx`
|
||||
|
||||
聚合数据在单次 RSC 请求内并发获取:
|
||||
- 学生:`getClassStudents({ classId })`
|
||||
- 课表:`getClassSchedule({ classId })`
|
||||
- 作业统计:`getClassHomeworkInsights({ classId, limit })`(包含 latest、历史列表、overallScores、以及每次作业的 scoreStats:avg/median)
|
||||
|
||||
页面呈现:
|
||||
- 顶部 KPI 卡片:学生数、课表条目数、作业数、整体 avg/median
|
||||
- Latest homework:目标人数、提交数、批改数、avg/median,直达作业与提交列表
|
||||
- Students / Schedule 预览:提供 View all 跳转到完整列表页
|
||||
- Homework history 表格:支持通过 URL query `?hw=all|active|overdue` 过滤作业记录,并展示每条作业的 avg/median
|
||||
|
||||
课表编辑能力复用既有 Builder:
|
||||
- 组件:`src/modules/classes/components/schedule-view.tsx`(新增/编辑/删除课表项)
|
||||
- 数据变更:`src/modules/classes/actions.ts`
|
||||
|
||||
### 6.7 班级邀请码(6 位码)加入与管理
|
||||
|
||||
**日期**: 2026-01-08
|
||||
**范围**: 为班级新增 6 位邀请码,支持学生通过输入邀请码加入班级;教师可查看与刷新邀请码
|
||||
|
||||
#### 6.7.1 数据结构
|
||||
- 表:`classes`
|
||||
- 字段:`invitation_code`(varchar(6),unique,可为空)
|
||||
- 迁移:`drizzle/0007_add_class_invitation_code.sql`
|
||||
|
||||
#### 6.7.2 教师端能力
|
||||
- 在「我的班级」卡片中展示邀请码。
|
||||
- 提供“刷新邀请码”操作:生成新的 6 位码并写入数据库(确保唯一性)。
|
||||
|
||||
#### 6.7.3 学生端能力
|
||||
- 提供“通过邀请码加入班级”的入口,输入 6 位码后完成报名。
|
||||
- 写库操作设计为幂等:重复提交同一个邀请码不会生成重复报名记录,已有记录会被更新为有效状态。
|
||||
|
||||
#### 6.7.4 Seed 支持
|
||||
- `scripts/seed.ts` 为示例班级补充 `invitationCode`,便于在开发环境直接验证加入流程。
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Textbooks Module Implementation Details
|
||||
|
||||
**Date**: 2025-12-23
|
||||
**Updated**: 2025-12-31
|
||||
**Author**: DevOps Architect
|
||||
**Module**: Textbooks (`src/modules/textbooks`)
|
||||
|
||||
@@ -27,6 +28,13 @@ src/
|
||||
│ ├── page.tsx
|
||||
│ └── loading.tsx
|
||||
│
|
||||
│ └── student/
|
||||
│ └── learning/
|
||||
│ └── textbooks/ # 学生端只读阅读(Server Components)
|
||||
│ ├── page.tsx # 列表页(复用筛选与卡片)
|
||||
│ └── [id]/ # 详情页(阅读器)
|
||||
│ └── page.tsx
|
||||
│
|
||||
├── modules/
|
||||
│ └── textbooks/ # 业务模块
|
||||
│ ├── actions.ts # Server Actions (增删改)
|
||||
@@ -34,6 +42,7 @@ src/
|
||||
│ ├── types.ts # 类型定义 (Schema-aligned)
|
||||
│ └── components/ # 模块私有组件
|
||||
│ ├── textbook-content-layout.tsx # [核心] 三栏布局工作台
|
||||
│ ├── textbook-reader.tsx # [新增] 学生端只读阅读器(URL state)
|
||||
│ ├── chapter-sidebar-list.tsx # 递归章节树
|
||||
│ ├── knowledge-point-panel.tsx # 知识点管理面板
|
||||
│ ├── create-chapter-dialog.tsx # 章节创建弹窗
|
||||
@@ -64,6 +73,11 @@ src/
|
||||
* **Optimistic UI**: 虽然使用 Server Actions,但通过本地状态 (`useState`) 实现了操作的即时反馈(如保存正文后立即退出编辑模式)。
|
||||
* **Feedback**: 使用 `sonner` (`toast`) 提供操作成功或失败的提示。
|
||||
|
||||
### 3.3 学生端阅读体验(Read-Only Reader)
|
||||
* **两栏阅读**:左侧章节树,右侧正文渲染(Markdown)。
|
||||
* **URL State**:选中章节通过 `?chapterId=` 写入 URL,支持刷新/分享后保持定位(nuqs)。
|
||||
* **只读边界**:学生端不暴露创建/删除/编辑/知识点管理入口,避免误用教师工作台能力。
|
||||
|
||||
## 4. 数据流与逻辑 (Data Flow)
|
||||
|
||||
### 4.1 Server Actions
|
||||
@@ -71,18 +85,22 @@ src/
|
||||
* `createChapterAction`: 创建章节(支持嵌套)。
|
||||
* `updateChapterContentAction`: 更新正文内容。
|
||||
* `createKnowledgePointAction`: 创建知识点并自动关联当前章节。
|
||||
* `deleteKnowledgePointAction`: 删除知识点并刷新详情页数据。
|
||||
* `updateTextbookAction`: 更新教材元数据(Title, Subject, Grade, Publisher)。
|
||||
* `deleteTextbookAction`: 删除教材及其关联数据。
|
||||
* `delete...Action`: 处理删除逻辑。
|
||||
|
||||
### 4.2 数据访问层 (Data Access)
|
||||
* **Mock Implementation**: 目前在 `data-access.ts` 中使用内存数组模拟数据库操作,并人为增加了延迟 (`setTimeout`) 以测试 Loading 状态。
|
||||
* **Type Safety**: 定义了严格的 TypeScript 类型 (`Chapter`, `KnowledgePoint`, `UpdateTextbookInput`),确保前后端数据契约一致。
|
||||
* **DB Implementation**: 教材模块已接入真实数据库访问,`data-access.ts` 使用 `drizzle-orm` 直接查询并返回教材、章节、知识点数据。
|
||||
* **章节树构建**: 章节采用父子关系存储,通过一次性拉取后在内存中构建嵌套树结构,避免 N+1 查询。
|
||||
* **级联删除**: 删除章节时会同时删除其子章节以及关联的知识点,确保数据一致性。
|
||||
* **Type Safety**: 定义严格的 TypeScript 类型(如 `Chapter`, `KnowledgePoint`, `UpdateTextbookInput`),保证数据契约与 UI 组件一致。
|
||||
|
||||
## 5. 组件复用
|
||||
* 使用了 `src/shared/components/ui` 中的 Shadcn 组件:
|
||||
* `Dialog`, `ScrollArea`, `Card`, `Button`, `Input`, `Textarea`, `Select`.
|
||||
* `Collapsible` 用于实现递归章节树。
|
||||
* `AlertDialog` 用于危险操作的二次确认(删除章节/删除知识点)。
|
||||
* 图标库统一使用 `lucide-react`.
|
||||
|
||||
## 6. Settings 功能实现 (New)
|
||||
@@ -92,7 +110,39 @@ src/
|
||||
* **Edit**: 修改教材的基本信息。
|
||||
* **Delete**: 提供红色删除按钮,二次确认后执行删除并跳转回列表页。
|
||||
|
||||
## 7. 后续计划 (Next Steps)
|
||||
* [ ] **富文本编辑器**: 集成 Tiptap 替换现有的 Markdown Textarea,支持更丰富的格式。
|
||||
* [ ] **拖拽排序**: 实现章节树的拖拽排序 (`dnd-kit`)。
|
||||
* [ ] **数据库对接**: 将 `data-access.ts` 中的 Mock 逻辑替换为真实的 `drizzle-orm` 数据库调用。
|
||||
## 7. 关键更新记录 (Changelog)
|
||||
|
||||
### 7.1 数据与页面
|
||||
* 教材模块从 Mock 切换为真实 DB:新增教材/章节/知识点的数据访问与 Server Actions 刷新策略。
|
||||
* 列表页支持过滤/搜索:通过 query 参数驱动,统一空状态反馈。
|
||||
|
||||
### 7.2 章节侧边栏与弹窗
|
||||
* 修复子章节创建弹窗“闪现后消失”:改为受控 Dialog 状态管理。
|
||||
* 修复移动端操作按钮不可见/被遮挡:调整布局与可见性策略,确保小屏可点。
|
||||
* 删除章节使用确认弹窗并提供删除中状态。
|
||||
|
||||
### 7.3 Markdown 阅读体验
|
||||
* 阅读模式使用 `react-markdown` 渲染章节内容,支持 GFM(表格/任务列表等)。
|
||||
* 启用 Typography(`prose`)排版样式,使 `h1/h2/...` 在视觉上有明显层级差异。
|
||||
* 修复阅读模式内容区无法滚动:为 flex 容器补齐 `min-h-0` 等必要约束。
|
||||
|
||||
### 7.4 知识点删除交互
|
||||
* 删除知识点从浏览器 `confirm()` 升级为 `AlertDialog`:
|
||||
* 显示目标名称、危险样式按钮
|
||||
* 删除中禁用交互并显示 loading 文案
|
||||
* 删除成功后刷新页面数据
|
||||
|
||||
### 7.5 学生端 Textbooks 列表与阅读页(New)
|
||||
* 新增学生端路由:
|
||||
* `/student/learning/textbooks`:教材列表页(RSC),复用筛选组件(nuqs)与卡片布局。
|
||||
* `/student/learning/textbooks/[id]`:教材阅读页(RSC + client 阅读器容器),章节选择与阅读不跳页。
|
||||
* 复用与适配:
|
||||
* `TextbookCard` 增加可配置跳转基地址,避免学生端卡片误跳到教师端详情页。
|
||||
* 新增 `TextbookReader`(client)用于只读阅读体验:左侧章节树 + 右侧正文渲染,章节定位 URL 化(`chapterId`)。
|
||||
* 质量门禁:
|
||||
* 通过 `npm run lint / typecheck / build`。
|
||||
|
||||
## 8. 后续计划 (Next Steps)
|
||||
* [ ] **富文本编辑器**: 集成编辑器替换当前 Markdown Textarea,提升编辑体验。
|
||||
* [ ] **拖拽排序**: 实现章节树拖拽排序与持久化。
|
||||
* [ ] **知识点能力增强**: 支持编辑、排序、分层(如需要)。
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
|
||||
- `getHomeworkAssignments`:作业列表(可按 creatorId/ids)
|
||||
- `getHomeworkAssignmentById`:作业详情(含目标人数、提交数统计)
|
||||
- `getHomeworkSubmissions`:提交列表(可按 assignmentId)
|
||||
- `getHomeworkSubmissions`:提交列表(可按 assignmentId/classId/creatorId)
|
||||
- `getHomeworkSubmissionDetails`:提交详情(题目内容 + 学生答案 + 分值/顺序)
|
||||
|
||||
### 4.2 学生侧查询
|
||||
@@ -151,3 +151,120 @@
|
||||
|
||||
- `npm run typecheck`: 通过
|
||||
- `npm run lint`: 0 errors(仓库其他位置存在 warnings,与本模块新增功能无直接关联)
|
||||
|
||||
---
|
||||
|
||||
## 9. 部署与环境变量(CI/CD)
|
||||
|
||||
### 9.1 本地开发
|
||||
|
||||
- 本地开发使用项目根目录的 `.env` 提供 `DATABASE_URL`
|
||||
- `.env` 仅用于本机开发,不应写入真实生产库凭据
|
||||
|
||||
### 9.2 CI 构建与部署(Gitea)
|
||||
|
||||
工作流位于:[ci.yml](file:///c:/Users/xiner/Desktop/CICD/.gitea/workflows/ci.yml)
|
||||
|
||||
- 构建阶段(`npm run build`)不依赖数据库连接:作业相关页面在构建时不会静态预渲染执行查库
|
||||
- 部署阶段通过 `docker run -e DATABASE_URL=...` 在运行时注入数据库连接串
|
||||
- 需要在 Gitea 仓库 Secrets 配置 `DATABASE_URL`(生产环境 MySQL 连接串)
|
||||
- CI 中关闭 Next.js telemetry:设置 `NEXT_TELEMETRY_DISABLED=1`
|
||||
|
||||
### 9.3 Next.js 渲染策略(避免 build 阶段查库)
|
||||
|
||||
作业模块相关页面在渲染时会进行数据库查询,因此显式标记为动态渲染以避免构建期预渲染触发数据库连接:
|
||||
|
||||
- 教师端作业列表:[assignments/page.tsx](file:///c:/Users/xiner/Desktop/CICD/src/app/(dashboard)/teacher/homework/assignments/page.tsx)
|
||||
|
||||
---
|
||||
|
||||
## 10. 实现更新(2026-01-05)
|
||||
|
||||
### 10.1 教师端作业详情页组件化(按 Vertical Slice 拆分)
|
||||
|
||||
将 `/teacher/homework/assignments/[id]` 页面调整为“只负责组装”,把可复用展示逻辑下沉到模块内组件:
|
||||
|
||||
- 页面组装:[page.tsx](file:///c:/Users/xiner/Desktop/CICD/src/app/(dashboard)/teacher/homework/assignments/%5Bid%5D/page.tsx)
|
||||
- 题目错误概览卡片(overview):[homework-assignment-question-error-overview-card.tsx](file:///c:/Users/xiner/Desktop/CICD/src/modules/homework/components/homework-assignment-question-error-overview-card.tsx)
|
||||
- 题目错误明细卡片(details):[homework-assignment-question-error-details-card.tsx](file:///c:/Users/xiner/Desktop/CICD/src/modules/homework/components/homework-assignment-question-error-details-card.tsx)
|
||||
- 试卷预览/错题工作台容器卡片:[homework-assignment-exam-content-card.tsx](file:///c:/Users/xiner/Desktop/CICD/src/modules/homework/components/homework-assignment-exam-content-card.tsx)
|
||||
|
||||
### 10.2 题目点击联动:试卷预览 ↔ 错题详情
|
||||
|
||||
在“试卷预览”中点击题目后,右侧联动展示该题的统计与错答列表(按学生逐条展示,不做合并):
|
||||
|
||||
- 工作台(选择题目、拼装左右面板):[homework-assignment-exam-error-explorer.tsx](file:///c:/Users/xiner/Desktop/CICD/src/modules/homework/components/homework-assignment-exam-error-explorer.tsx)
|
||||
- 试卷预览面板(可选中题目):[homework-assignment-exam-preview-pane.tsx](file:///c:/Users/xiner/Desktop/CICD/src/modules/homework/components/homework-assignment-exam-preview-pane.tsx)
|
||||
- 错题详情面板(错误人数/错误率/错答列表):[homework-assignment-question-error-detail-panel.tsx](file:///c:/Users/xiner/Desktop/CICD/src/modules/homework/components/homework-assignment-question-error-detail-panel.tsx)
|
||||
|
||||
### 10.3 统计数据增强:返回逐学生错答
|
||||
|
||||
为满足“错答列表逐条展示学生姓名 + 答案”的需求,作业统计查询返回每题的错答明细(包含学生信息):
|
||||
|
||||
- 数据访问:[getHomeworkAssignmentAnalytics](file:///c:/Users/xiner/Desktop/CICD/src/modules/homework/data-access.ts)
|
||||
- 类型定义:[types.ts](file:///c:/Users/xiner/Desktop/CICD/src/modules/homework/types.ts)
|
||||
|
||||
### 10.4 加载优化:Client Wrapper 动态分包
|
||||
|
||||
由于 `next/dynamic({ ssr: false })` 不能在 Server Component 内使用,工作台动态加载通过 Client wrapper 进行隔离:
|
||||
|
||||
- Client wrapper:[homework-assignment-exam-error-explorer-lazy.tsx](file:///c:/Users/xiner/Desktop/CICD/src/modules/homework/components/homework-assignment-exam-error-explorer-lazy.tsx)
|
||||
- 入口卡片(Server Component,渲染 wrapper):[homework-assignment-exam-content-card.tsx](file:///c:/Users/xiner/Desktop/CICD/src/modules/homework/components/homework-assignment-exam-content-card.tsx)
|
||||
|
||||
### 10.5 校验
|
||||
|
||||
- `npm run lint`: 通过
|
||||
- `npm run typecheck`: 通过
|
||||
- `npm run build`: 通过
|
||||
|
||||
---
|
||||
|
||||
## 11. 学生成绩图表与排名(2026-01-06)
|
||||
|
||||
### 11.1 目标
|
||||
|
||||
在学生主页(Dashboard)展示:
|
||||
|
||||
- 最近已批改作业的成绩趋势(百分比折线)
|
||||
- 最近若干次已批改作业明细(标题、得分、时间)
|
||||
- 班级排名(基于班级内作业总体得分百分比)
|
||||
|
||||
### 11.2 数据访问与计算口径
|
||||
|
||||
数据由 Homework 模块统一提供聚合查询,避免页面层拼 SQL:
|
||||
|
||||
- 新增查询:[getStudentDashboardGrades](file:///c:/Users/xiner/Desktop/CICD/src/modules/homework/data-access.ts)
|
||||
- `trend`:取该学生所有 `graded` 提交中“每个 assignment 最新一次”的集合,按时间升序取最近 10 个
|
||||
- `recent`:对 `trend` 再按时间降序取最近 5 条,用于表格展示
|
||||
- `maxScore`:通过 `homework_assignment_questions` 汇总每个 assignment 的总分(SUM(score))
|
||||
- `percentage`:`score / maxScore * 100`
|
||||
- `ranking`:
|
||||
- 班级选择:取该学生最早创建的一条 active enrollment 作为当前班级
|
||||
- 班级作业集合:班级内所有学生的 targets 合并得到 assignment 集合
|
||||
- 计分口径:班级内“每个学生 × 每个 assignment”取最新一次 graded 提交,累加得分与满分,得到总体百分比
|
||||
- 排名:按总体百分比降序排序(百分比相同按 studentId 作为稳定排序因子)
|
||||
|
||||
### 11.3 类型定义
|
||||
|
||||
为 Dashboard 聚合数据提供显式类型:
|
||||
|
||||
- [types.ts](file:///c:/Users/xiner/Desktop/CICD/src/modules/homework/types.ts)
|
||||
- `StudentHomeworkScoreAnalytics`
|
||||
- `StudentRanking`
|
||||
- `StudentDashboardGradeProps`
|
||||
|
||||
### 11.4 页面与组件接入
|
||||
|
||||
- 学生主页页面负责“取数 + 计算基础计数 + 传参”:
|
||||
- [student/dashboard/page.tsx](file:///c:/Users/xiner/Desktop/CICD/src/app/(dashboard)/student/dashboard/page.tsx)
|
||||
- 取数:`getStudentDashboardGrades(student.id)`
|
||||
- 传入:`<StudentDashboard grades={grades} />`
|
||||
- 展示组件负责渲染卡片:
|
||||
- [student-view.tsx](file:///c:/Users/xiner/Desktop/CICD/src/modules/dashboard/components/student-view.tsx)
|
||||
- 趋势图:使用内联 `svg polyline` 渲染折线,避免引入额外图表依赖
|
||||
|
||||
### 11.5 校验
|
||||
|
||||
- `npm run lint`: 通过
|
||||
- `npm run typecheck`: 通过
|
||||
- `npm run build`: 通过
|
||||
|
||||
164
docs/design/007_school_module_implementation.md
Normal file
164
docs/design/007_school_module_implementation.md
Normal file
@@ -0,0 +1,164 @@
|
||||
# 学校基础数据模块(School)实现文档与更新记录
|
||||
|
||||
**日期**: 2026-01-07
|
||||
**作者**: Frontend Team
|
||||
**状态**: 已实现
|
||||
|
||||
## 1. 范围
|
||||
|
||||
本文档覆盖管理端「School」域的基础数据维护页面(Schools / Departments / Academic Year / Grades),并记录相关实现约束与关键更新,遵循 [003_frontend_engineering_standards.md](file:///c:/Users/xiner/Desktop/CICD/docs/architecture/003_frontend_engineering_standards.md) 的工程规范(Vertical Slice、Server/Client 边界、质量门禁)。
|
||||
|
||||
## 2. 路由入口(Admin)
|
||||
|
||||
School 域路由位于 `src/app/(dashboard)/admin/school/*`,均显式声明 `export const dynamic = "force-dynamic"` 以避免构建期预渲染触发数据库访问。
|
||||
|
||||
- `/admin/school`:入口重定向到 Classes(当前落点不在 `school` 模块内)
|
||||
实现:[page.tsx](file:///c:/Users/xiner/Desktop/CICD/src/app/(dashboard)/admin/school/page.tsx)
|
||||
- `/admin/school/schools`:学校维护(增删改)
|
||||
实现:[page.tsx](file:///c:/Users/xiner/Desktop/CICD/src/app/(dashboard)/admin/school/schools/page.tsx)
|
||||
- `/admin/school/departments`:部门维护(增删改)
|
||||
实现:[page.tsx](file:///c:/Users/xiner/Desktop/CICD/src/app/(dashboard)/admin/school/departments/page.tsx)
|
||||
- `/admin/school/academic-year`:学年维护(增删改 + 设为当前学年)
|
||||
实现:[page.tsx](file:///c:/Users/xiner/Desktop/CICD/src/app/(dashboard)/admin/school/academic-year/page.tsx)
|
||||
- `/admin/school/grades`:年级维护(增删改 + 指派年级组长/教研组长)
|
||||
实现:[page.tsx](file:///c:/Users/xiner/Desktop/CICD/src/app/(dashboard)/admin/school/grades/page.tsx)
|
||||
- `/admin/school/grades/insights`:年级维度作业统计(跨班级聚合)
|
||||
实现:[page.tsx](file:///c:/Users/xiner/Desktop/CICD/src/app/(dashboard)/admin/school/grades/insights/page.tsx)
|
||||
|
||||
## 3. 模块结构(Vertical Slice)
|
||||
|
||||
School 模块位于 `src/modules/school`:
|
||||
|
||||
```
|
||||
src/modules/school/
|
||||
├── components/
|
||||
│ ├── schools-view.tsx
|
||||
│ ├── departments-view.tsx
|
||||
│ ├── academic-year-view.tsx
|
||||
│ └── grades-view.tsx
|
||||
├── actions.ts
|
||||
├── data-access.ts
|
||||
├── schema.ts
|
||||
└── types.ts
|
||||
```
|
||||
|
||||
边界约束:
|
||||
- `data-access.ts` 包含 `import "server-only"`,仅用于服务端查询与 DTO 组装。
|
||||
- `actions.ts` 包含 `"use server"`,写操作统一通过 Server Actions 并 `revalidatePath`。
|
||||
- `components/*` 为 Client 交互层(表单、Dialog、筛选、行级操作),调用 Server Actions 并用 `sonner` toast 反馈。
|
||||
|
||||
## 4. 数据访问(data-access.ts)
|
||||
|
||||
实现:[data-access.ts](file:///c:/Users/xiner/Desktop/CICD/src/modules/school/data-access.ts)
|
||||
|
||||
- `getSchools(): Promise<SchoolListItem[]>`
|
||||
- `getDepartments(): Promise<DepartmentListItem[]>`
|
||||
- `getAcademicYears(): Promise<AcademicYearListItem[]>`
|
||||
- `getGrades(): Promise<GradeListItem[]>`
|
||||
- join `schools` 获取 `school.name`
|
||||
- 收集 `gradeHeadId/teachingHeadId` 并批量查询 `users` 以组装 `StaffOption`
|
||||
- `getStaffOptions(): Promise<StaffOption[]>`
|
||||
- 角色过滤 `teacher/admin`
|
||||
- 排序 `name/email`,用于 Select 列表可用性
|
||||
- `getGradesForStaff(staffId: string): Promise<GradeListItem[]>`
|
||||
- 用于按负责人(年级组长/教研组长)反查关联年级
|
||||
|
||||
返回 DTO 类型定义位于:[types.ts](file:///c:/Users/xiner/Desktop/CICD/src/modules/school/types.ts)
|
||||
|
||||
## 5. 写操作(actions.ts)
|
||||
|
||||
实现:[actions.ts](file:///c:/Users/xiner/Desktop/CICD/src/modules/school/actions.ts)
|
||||
|
||||
通用约束:
|
||||
- 输入校验:统一使用 [schema.ts](file:///c:/Users/xiner/Desktop/CICD/src/modules/school/schema.ts) 的 Zod schema
|
||||
- 返回结构:统一使用 [ActionState](file:///c:/Users/xiner/Desktop/CICD/src/shared/types/action-state.ts)
|
||||
- 刷新策略:对目标页面路径执行 `revalidatePath`
|
||||
|
||||
Departments:
|
||||
- `createDepartmentAction(formData)`
|
||||
- `updateDepartmentAction(departmentId, formData)`
|
||||
- `deleteDepartmentAction(departmentId)`
|
||||
|
||||
Academic Year:
|
||||
- `createAcademicYearAction(formData)`
|
||||
- `updateAcademicYearAction(academicYearId, formData)`
|
||||
- `deleteAcademicYearAction(academicYearId)`
|
||||
- 当 `isActive=true` 时,通过事务把其它学年置为非激活,保证唯一激活学年
|
||||
|
||||
Schools:
|
||||
- `createSchoolAction(formData)`
|
||||
- `updateSchoolAction(schoolId, formData)`
|
||||
- `deleteSchoolAction(schoolId)`
|
||||
- 删除后会同时刷新 `/admin/school/schools` 与 `/admin/school/grades`
|
||||
|
||||
Grades:
|
||||
- `createGradeAction(formData)`
|
||||
- `updateGradeAction(gradeId, formData)`
|
||||
- `deleteGradeAction(gradeId)`
|
||||
|
||||
## 6. UI 组件(components/*)
|
||||
|
||||
Schools:
|
||||
- 实现:[schools-view.tsx](file:///c:/Users/xiner/Desktop/CICD/src/modules/school/components/schools-view.tsx)
|
||||
- 交互:列表 + Dialog 表单(新增/编辑)+ 删除确认(AlertDialog)
|
||||
|
||||
Departments:
|
||||
- 实现:[departments-view.tsx](file:///c:/Users/xiner/Desktop/CICD/src/modules/school/components/departments-view.tsx)
|
||||
- 交互:列表 + Dialog 表单(新增/编辑)+ 删除确认(AlertDialog)
|
||||
|
||||
Academic Year:
|
||||
- 实现:[academic-year-view.tsx](file:///c:/Users/xiner/Desktop/CICD/src/modules/school/components/academic-year-view.tsx)
|
||||
- 交互:列表 + Dialog 表单(新增/编辑)+ 删除确认(AlertDialog)+ 设为当前学年(isActive)
|
||||
|
||||
Grades:
|
||||
- 实现:[grades-view.tsx](file:///c:/Users/xiner/Desktop/CICD/src/modules/school/components/grades-view.tsx)
|
||||
- 交互:列表展示 + URL 驱动筛选(搜索/学校/负责人/排序)+ Dialog 表单(新增/编辑)+ 删除确认(AlertDialog)
|
||||
- 负责人指派:
|
||||
- 年级组长(gradeHeadId)
|
||||
- 教研组长(teachingHeadId)
|
||||
|
||||
## 7. 关键交互与规则(Grades)
|
||||
|
||||
页面入口(RSC 组装)在服务端并发拉取三类数据:
|
||||
- 年级列表:`getGrades()`
|
||||
- 学校选项:`getSchools()`
|
||||
- 负责人候选:`getStaffOptions()`
|
||||
|
||||
实现:[page.tsx](file:///c:/Users/xiner/Desktop/CICD/src/app/(dashboard)/admin/school/grades/page.tsx)
|
||||
|
||||
### 7.1 URL State(筛选/排序)
|
||||
|
||||
Grades 列表页的筛选状态 URL 化(`nuqs`):
|
||||
- `q`:关键字(匹配 grade/school)
|
||||
- `school`:学校过滤(`all` 或具体 schoolId)
|
||||
- `head`:负责人过滤(全部 / 两者缺失 / 缺年级组长 / 缺教研组长)
|
||||
- `sort`:排序(默认/名称/更新时间等)
|
||||
|
||||
实现:[grades-view.tsx](file:///c:/Users/xiner/Desktop/CICD/src/modules/school/components/grades-view.tsx)
|
||||
|
||||
### 7.2 表单校验
|
||||
|
||||
Grades 的新增/编辑表单在客户端做轻量校验:
|
||||
- 必填:`schoolId`、`name`
|
||||
- `order`:非负整数
|
||||
- 去重:同一学校下年级名称不允许重复(忽略大小写 + 规范化空格)
|
||||
|
||||
说明:
|
||||
- 服务端写入前仍会经过 `UpsertGradeSchema` 校验(schema.ts),避免仅依赖客户端校验。
|
||||
|
||||
### 7.3 负责人选择(Radix Select)
|
||||
|
||||
Radix Select 约束:`SelectItem` 的 `value` 不能为 `""`(空字符串),否则会触发运行时错误。
|
||||
|
||||
当前实现策略:
|
||||
- UI 中 “未设置” 选项使用占位值 `__none__`
|
||||
- 在 `onValueChange` 中将 `__none__` 映射回 `""` 存入本地表单 state
|
||||
- 提交时依旧传递空字符串,由 `UpsertGradeSchema` 将其归一为 `null`
|
||||
|
||||
实现:[grades-view.tsx](file:///c:/Users/xiner/Desktop/CICD/src/modules/school/components/grades-view.tsx)
|
||||
|
||||
## 2. 更新记录(2026-01-07)
|
||||
|
||||
- 修复 Add Grades 弹窗报错:将 4 处 `<SelectItem value=\"\">` 替换为占位值 `__none__`,并在 `onValueChange` 中映射回 `\"\"`,保持“可清空选择/显示 placeholder”的行为不变。
|
||||
- 修复新建年级按钮不可用:创建/编辑表单在状态变化时触发实时校验更新,避免校验状态滞后导致提交被禁用。
|
||||
- 质量门禁:本地通过 `npm run lint` 与 `npm run typecheck`。
|
||||
Reference in New Issue
Block a user