feat(课堂): 老师的课堂看板——布置这节课的题、看全班谁还没开始谁卡住了,布置的题直接出现在学生首页

2025 秋平均每节课有 14 个平时在用的学生一道都没交(三分之一),交了 3 次以上还没过的
平均不到 1 个;而老师原来只能一道题一道题打开统计弹窗看。

- 新表 class_lesson(迁移 0023,仅建表):一个班一天一行,存老师布置的 problem.id 与顺序;
  day 存东八区日历日文本,created_by CASCADE
- GET /classroom/board:这个班今天的题 × 全班学生(通过与否不限今天、今天交了几次、
  最后提交时间);不给班级时猜最近两小时提交人数最多的班,不记老师上次选的
- PUT /classroom/lesson:按展示题号布置,对不上的题号点名拒绝,空数组 = 清掉
- /me/class-activity 优先返回老师布置的题(source = teacher),否则照旧推断
- 前端 /classroom:没交过的名单置顶,按 没交过 → 卡住 → 在做 → 做完 排序,15 秒刷新;
  投影模式只显示每题完成人数、不出名字;用户菜单给老师加入口
- 学生首页:老师布置的标题写「今天老师布置的题」,没人交时不显示 0/0

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-09-27 22:37:59 +08:00
1 parent 7e964fdbdd
commit 80106eaa04
12 files changed
+4992 -69

No files matched your search

+50
View File
@@ -81,15 +81,65 @@ export const classActivityProblemSchema = z.object({
export const classActivitySchema = z.object({
className: z.string().nullable(),
day: z.string().nullable(),
/** teacher = 老师在课堂看板里布置的;inferred = 从同班提交记录推断的 */
source: z.enum(["teacher", "inferred"]).nullable(),
problems: z.array(classActivityProblemSchema),
})
/** 课堂看板里老师布置这节课的题:按输入顺序给展示题号,最多 20 道,空数组 = 清掉 */
export const classLessonRequestSchema = z.object({
className: z.string().trim().min(1),
problemDisplayIds: z.array(z.string().trim().min(1)).max(20),
})
export const classBoardProblemSchema = z.object({
problemId: z.number().int(),
problemDisplayId: z.string(),
title: z.string(),
})
export const classBoardCellSchema = z.object({
/** accepted 看的是题库里(不含比赛)有没有通过过,不限今天 —— 以前做过的也算做完 */
status: z.enum(["accepted", "tried", "none"]),
/** 今天在这道题上交了几次 */
attempts: z.number().int(),
/** 第一次通过的时刻;今天之前通过的,前端显示成「之前」 */
acceptedAt: z.string().nullable(),
})
export const classBoardStudentSchema = z.object({
userId: z.number().int(),
username: z.string(),
realName: z.string().nullable(),
/** 和 problems 同序 */
cells: z.array(classBoardCellSchema),
/** 今天最后一次提交(任何题),没交过为 null */
lastSubmitAt: z.string().nullable(),
})
/**
* 课堂看板:一个班、今天、这节课的几道题 × 全班学生。
* className 为 null = 没指定班级、最近两小时也没有哪个班在交(没法猜)。
*/
export const classBoardSchema = z.object({
className: z.string().nullable(),
day: z.string(),
source: z.enum(["teacher", "inferred"]).nullable(),
problems: z.array(classBoardProblemSchema),
students: z.array(classBoardStudentSchema),
})
export type ClassRankItem = z.infer<typeof classRankItemSchema>
export type ClassUserRank = z.infer<typeof classUserRankSchema>
export type ClassComparison = z.infer<typeof classComparisonSchema>
export type ClassComparisonResponse = z.infer<typeof classComparisonResponseSchema>
export type ClassActivity = z.infer<typeof classActivitySchema>
export type ClassLessonRequest = z.infer<typeof classLessonRequestSchema>
export type ClassBoard = z.infer<typeof classBoardSchema>
export type ClassBoardProblem = z.infer<typeof classBoardProblemSchema>
export type ClassBoardStudent = z.infer<typeof classBoardStudentSchema>
export type ClassBoardCell = z.infer<typeof classBoardCellSchema>
export type ClassActivityProblem = z.infer<typeof classActivityProblemSchema>
export type ClassUserRankItem = z.infer<typeof classUserRankItemSchema>