Files
ClassHelper/packages/server/src/modules/imports/imports.schemas.ts
T
LoveChengke 845fbdd863 feat(teachers,classes,schedules,web): 教师录入(仅管理员)+ 班主任可设置/更改 + 授课科目全校统一 + 登录页去示例
① 支持录入教师(与学生录入对齐),权限只有管理员
- 新增 Web「教师管理」页(`Views/TeachersView.vue`):单个录入(姓名/用户名/初始密码/角色)、
  导入名单(xlsx/csv:用户名、姓名、初始密码、角色)、编辑(改名/改角色)、重置密码、删除
- 服务端 `teachers` 模块补齐:GET / POST / PATCH /:id / POST /:id/reset-password / DELETE /:id,
  全部 `requireRole('ADMIN')`;表格导入新增 kind=teachers(`IMPORT_FIELDS.teachers`、`commitTeachers`、
  教师名单模板),教师调用预览/提交一律 403
- 删除护栏:还是班主任(`Class.teacherId`)或持有课程/班级分配的账号拒绝删除(409,提示先更换班主任)——
  教师是数据创建者,级联删除会连带删掉班级数据
- 前端路由加 `meta.roles`:`/classes`、`/students`、`/teachers` 仅 ADMIN,教师直接输网址会被挡回仪表盘
  (菜单隐藏只是"看不见",这次补上真正的守卫);新增环境变量 `DEFAULT_TEACHER_PASSWORD`

② 创建班级仅管理员 + 设置 / 更改班主任
- 新建班级时可直接指定班主任(不填则创建者本人),班级详情顶部「更换班主任」下拉随时改:
  新增 `PATCH /api/classes/:id/head-teacher`(仅管理员,校验目标账号是教师/管理员,广播 class:updated)
- 原班主任保留在协作教师名单里,避免换人后丢掉历史协作关系

③ 授课科目全校统一(用户澄清:"统一"指的是授课科目固定,不是各班课表相同)
- `SUBJECT_CATALOG` 改为用户给定的 18 科:语文/数学/英语/政治/历史/物理/化学/地理/生物/信息技术/
  体育/美术/音乐/班会/早读/晚读/通用技术/听力
- 课表「新增课表」与成绩「单条/批量录入」的科目下拉直接列出统一目录(该班还没有的进
  「统一科目(选中后自动建课)」分组),选中后自动为该班建同名课程再落库 —— 不必按班级一个个录科目
- 各班课表仍然各自独立(各班的课表本来就不一样)

④ 登录页去掉所有示例内容
- Web 登录页删除「演示账号(点击填充)」标签列表与"种子数据见 README"提示;客户端班级码占位符
  由「例如 G101(由老师在班级管理中提供)」改为「请输入班级码」
- `verify:web` 与 `verify:desktop` 各自断言登录页正文/占位符不含 `演示/示例/admin123/teacher123/G101`

⑤ 验证脚本不再依赖演示数据(并修掉自身残留)
- 清空服务端作业/通知后,联网自检改为**自建**一条作业/通知并在收尾删除(`seedSmokeContent`);
  权限用例需要"其它教师的班级/科任班级"时现建临时班级并在收尾删除
- e2e 新增清理:实时推送用例造的通知/作业、上课时段用例造的两条通知、教师名单/临时班级
  (此前这几条会留在库里);班级详情相关断言不再依赖种子班级布局
- ui-smoke 新增 7 项:登录页无示例、教师端访问 /teachers 被挡回、统一科目下拉 + 自动建课 + 收尾删除
  课表与课程、管理员真人录入教师 → 列表出现 → 删除
- 冒烟里"岛就绪"与"截图留档"改为轮询等待(偶发丢一张留档图会误判"形态尺寸递增")

实测:verify:e2e 163/163(安装实例)、verify:web 27/27、verify:desktop 77/77(离线复跑 69/69)、
verify:packaged 对 release/win-unpacked 与 D:\class\@classhelperdesktop-client 均 77/77 且 packaged=true;
typecheck / lint / format:check 全绿。服务端安装实例与客户端安装目录已就地更新。
2026-09-13 10:34:46 +08:00

71 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { z } from 'zod';
/** 导入类型:成绩 / 学生名单 / 教师名单(仅管理员) */
export const IMPORT_KINDS = ['grades', 'students', 'teachers'] as const;
/** 表格导入:上传的文件(base64)+ 类型 */
export const tableFileSchema = z.object({
kind: z.enum(IMPORT_KINDS),
fileName: z.string().trim().min(1, '缺少文件名').max(200),
/**
* base64(不含 data: 前缀)。
* 这里刻意不限制最小长度:空内容交给 service 返回 IMPORT_EMPTY_FILE,
* 这样前端能拿到"文件为空"这一精确原因,而不是笼统的参数校验错误。
*/
contentBase64: z.string(),
});
/** 表格提交:在预览基础上带字段映射、目标班级与写入模式 */
export const tableCommitSchema = tableFileSchema.extend({
/** 目标班级:成绩/学生名单必填;教师名单与班级无关(可省略) */
classId: z.string().min(1, '请选择班级').optional(),
/** 列映射:规范字段 → 文件列名(或列索引字符串) */
mapping: z.record(z.string(), z.string().min(1)),
/** append = 仅新增;upsert = 已存在则更新(成绩按 学生+考试+课程,名单按用户名) */
mode: z.enum(['append', 'upsert']).default('upsert'),
});
/** ClassIsland 课表时间导入:先校验/预览,再提交 */
export const timeLayoutImportSchema = z.object({
classId: z.string().min(1, '请选择班级'),
name: z.string().trim().max(64).optional(),
/** replace = 覆盖该班现有时间配置;merge = 按开始时间合并 */
mode: z.enum(['replace', 'merge']).default('replace'),
/**
* ClassIsland 导出的 JSON(对象或字符串均可)。
* 字符串是为了兼容"直接粘贴 JSON 文本"的场景。
*/
payload: z.union([
z.string().min(1, '请粘贴或选择 ClassIsland 导出的 JSON'),
z.record(z.string(), z.unknown()),
z.array(z.unknown()),
]),
});
/** 时间配置查询 */
export const timeLayoutQuerySchema = z.object({
classId: z.string().min(1, '请选择班级'),
});
/**
* ClassIsland 课程表(ClassPlan)导入:先预览再提交。
* 真实档案 JSON 里 TimeLayouts / ClassPlans / Subjects 都是 "Guid → 对象" 的字典,
* 单双周由 ClassPlan.TimeRule.WeekCountDiv / WeekCountDivTotal 表达。
*/
export const classPlanImportSchema = z.object({
classId: z.string().min(1, '请选择班级'),
/** replace = 清空该班现有课表后写入;merge = 保留现有条目,只新增(同 星期+节次+单双周 视为重复则更新) */
mode: z.enum(['replace', 'merge']).default('replace'),
/** 学期起始周(用于计算单双周;不传则用服务端配置的开学日期推算) */
termStartWeek: z.coerce.number().int().min(1).max(60).optional(),
payload: z.union([
z.string().min(1, '请粘贴或选择 ClassIsland 导出的 JSON'),
z.record(z.string(), z.unknown()),
]),
});
export type TableFileInput = z.infer<typeof tableFileSchema>;
export type TableCommitInput = z.infer<typeof tableCommitSchema>;
export type TimeLayoutImportInput = z.infer<typeof timeLayoutImportSchema>;
export type ClassPlanImportInput = z.infer<typeof classPlanImportSchema>;