# 学校基础数据模块(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` - `getDepartments(): Promise` - `getAcademicYears(): Promise` - `getGrades(): Promise` - join `schools` 获取 `school.name` - 收集 `gradeHeadId/teachingHeadId` 并批量查询 `users` 以组装 `StaffOption` - `getStaffOptions(): Promise` - 角色过滤 `teacher/admin` - 排序 `name/email`,用于 Select 列表可用性 - `getGradesForStaff(staffId: string): Promise` - 用于按负责人(年级组长/教研组长)反查关联年级 返回 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 处 `` 替换为占位值 `__none__`,并在 `onValueChange` 中映射回 `\"\"`,保持“可清空选择/显示 placeholder”的行为不变。 - 修复新建年级按钮不可用:创建/编辑表单在状态变化时触发实时校验更新,避免校验状态滞后导致提交被禁用。 - 质量门禁:本地通过 `npm run lint` 与 `npm run typecheck`。