7.6 KiB
7.6 KiB
学校基础数据模块(School)实现文档与更新记录
日期: 2026-01-07
作者: Frontend Team
状态: 已实现
1. 范围
本文档覆盖管理端「School」域的基础数据维护页面(Schools / Departments / Academic Year / Grades),并记录相关实现约束与关键更新,遵循 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/admin/school/schools:学校维护(增删改)
实现:page.tsx/admin/school/departments:部门维护(增删改)
实现:page.tsx/admin/school/academic-year:学年维护(增删改 + 设为当前学年)
实现:page.tsx/admin/school/grades:年级维护(增删改 + 指派年级组长/教研组长)
实现:page.tsx/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 并用sonnertoast 反馈。
4. 数据访问(data-access.ts)
getSchools(): Promise<SchoolListItem[]>getDepartments(): Promise<DepartmentListItem[]>getAcademicYears(): Promise<AcademicYearListItem[]>getGrades(): Promise<GradeListItem[]>- join
schools获取school.name - 收集
gradeHeadId/teachingHeadId并批量查询users以组装StaffOption
- join
getStaffOptions(): Promise<StaffOption[]>- 角色过滤
teacher/admin - 排序
name/email,用于 Select 列表可用性
- 角色过滤
getGradesForStaff(staffId: string): Promise<GradeListItem[]>- 用于按负责人(年级组长/教研组长)反查关联年级
返回 DTO 类型定义位于:types.ts
5. 写操作(actions.ts)
实现:actions.ts
通用约束:
- 输入校验:统一使用 schema.ts 的 Zod schema
- 返回结构:统一使用 ActionState
- 刷新策略:对目标页面路径执行
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
- 交互:列表 + Dialog 表单(新增/编辑)+ 删除确认(AlertDialog)
Departments:
- 实现:departments-view.tsx
- 交互:列表 + Dialog 表单(新增/编辑)+ 删除确认(AlertDialog)
Academic Year:
- 实现:academic-year-view.tsx
- 交互:列表 + Dialog 表单(新增/编辑)+ 删除确认(AlertDialog)+ 设为当前学年(isActive)
Grades:
- 实现:grades-view.tsx
- 交互:列表展示 + URL 驱动筛选(搜索/学校/负责人/排序)+ Dialog 表单(新增/编辑)+ 删除确认(AlertDialog)
- 负责人指派:
- 年级组长(gradeHeadId)
- 教研组长(teachingHeadId)
7. 关键交互与规则(Grades)
页面入口(RSC 组装)在服务端并发拉取三类数据:
- 年级列表:
getGrades() - 学校选项:
getSchools() - 负责人候选:
getStaffOptions()
实现:page.tsx
7.1 URL State(筛选/排序)
Grades 列表页的筛选状态 URL 化(nuqs):
q:关键字(匹配 grade/school)school:学校过滤(all或具体 schoolId)head:负责人过滤(全部 / 两者缺失 / 缺年级组长 / 缺教研组长)sort:排序(默认/名称/更新时间等)
7.2 表单校验
Grades 的新增/编辑表单在客户端做轻量校验:
- 必填:
schoolId、name order:非负整数- 去重:同一学校下年级名称不允许重复(忽略大小写 + 规范化空格)
说明:
- 服务端写入前仍会经过
UpsertGradeSchema校验(schema.ts),避免仅依赖客户端校验。
7.3 负责人选择(Radix Select)
Radix Select 约束:SelectItem 的 value 不能为 ""(空字符串),否则会触发运行时错误。
当前实现策略:
- UI 中 “未设置” 选项使用占位值
__none__ - 在
onValueChange中将__none__映射回""存入本地表单 state - 提交时依旧传递空字符串,由
UpsertGradeSchema将其归一为null
2. 更新记录(2026-01-07)
- 修复 Add Grades 弹窗报错:将 4 处
<SelectItem value=\"\">替换为占位值__none__,并在onValueChange中映射回\"\",保持“可清空选择/显示 placeholder”的行为不变。 - 修复新建年级按钮不可用:创建/编辑表单在状态变化时触发实时校验更新,避免校验状态滞后导致提交被禁用。
- 质量门禁:本地通过
npm run lint与npm run typecheck。