机房有两人共用一台电脑的,同桌只有一个登着,只看「此刻在不在线」会把旁边的人 当成没来。所以每次记在线时按天再记一笔「今天最后一次上线」: - 看板「在线」列(原「在写」):开着题的照旧写题号,其余写 在线 / N 分钟前在 / 今天没上线 - 还没交过的人里,今天没上过线的排最前 - 请假框分「今天没上线」「今天上过线」两堆,前一堆可以一键全勾上 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
235 lines
10 KiB
TypeScript
235 lines
10 KiB
TypeScript
import { z } from "zod"
|
||
|
||
import { judgeStatusSchema } from "./judge-status"
|
||
import { lessonLanguageSchema as lessonLanguageValueSchema } from "./language"
|
||
import { statisticInfoSchema } from "./submission"
|
||
|
||
/**
|
||
* 「班里在做」:同班同一天有好几个人做的题,基本就是老师在课上点名的那几道。
|
||
* `day` 是东八区日历日;最近 7 天都没有这样的题时为 null,`problems` 为空。
|
||
*/
|
||
export const classActivityProblemSchema = z.object({
|
||
problemDisplayId: z.string(),
|
||
title: z.string(),
|
||
/** 那天做过这道题的同班人数(含自己) */
|
||
userCount: z.number().int(),
|
||
/** 其中那天通过了的人数 */
|
||
acceptedCount: z.number().int(),
|
||
/**
|
||
* 我自己在题库里(不含比赛)做这道题的状态,不限那一天。老师布置时选了语言的,
|
||
* 代码只认用那个语言交的(流程图照旧算)
|
||
*/
|
||
myStatus: z.enum(["accepted", "tried", "none"]),
|
||
})
|
||
|
||
/**
|
||
* 老师布置这节课时选的语言(Python / C / C++ / SQL)。null = 没选:推断出来的课、加这一项之前
|
||
* 布置的课。选了之后这份作业「做完」只认这个语言交对的(SQL 题例外,交对 SQL 就算),
|
||
* 学生打开这几道题编辑器默认就是它
|
||
*/
|
||
const lessonLanguageSchema = lessonLanguageValueSchema.nullable()
|
||
|
||
export const classActivitySchema = z.object({
|
||
className: z.string().nullable(),
|
||
day: z.string().nullable(),
|
||
/** teacher = 老师在课堂看板里布置的;inferred = 从同班提交记录推断的 */
|
||
source: z.enum(["teacher", "inferred"]).nullable(),
|
||
language: lessonLanguageSchema,
|
||
problems: z.array(classActivityProblemSchema),
|
||
})
|
||
|
||
/**
|
||
* 首页的「上次来」卡(原来登录后弹的「登录速报」并进了首页,AI 那两句不要了)。
|
||
* 窗口是**上次登录到这次登录之间**,不是到现在 —— 这次登录期间卡片内容不变,
|
||
* 今天做的题不会混进「上次」里。只算题库提交,比赛的题号在题库里点不开。
|
||
*/
|
||
export const lastVisitSchema = z.object({
|
||
/** 上次登录的时刻。null = 头一回登录(或会话早于这个字段) */
|
||
previousLogin: z.string().nullable(),
|
||
/** null = 上次来已经是 30 天以前,或那次一条代码都没交 */
|
||
summary: z
|
||
.object({
|
||
submissionCount: z.number().int(),
|
||
/** 那次做对的题数(去重) */
|
||
solvedCount: z.number().int(),
|
||
/** 那次最后一条提交的时刻,前端拿它和「班里那天」比是不是同一天 */
|
||
lastSubmitTime: z.string(),
|
||
/**
|
||
* 那次交过、**到现在都还没做对**的题(这次登录后补做对了的不算),
|
||
* 按最后一次提交倒序,最多 3 道
|
||
*/
|
||
unsolved: z.array(
|
||
z.object({
|
||
problemDisplayId: z.string(),
|
||
title: z.string(),
|
||
attempts: z.number().int(),
|
||
lastResult: judgeStatusSchema,
|
||
}),
|
||
),
|
||
/** 那次做对的题,按第一次做对的先后,最多 12 道(总数看 solvedCount) */
|
||
solved: z.array(z.object({ problemDisplayId: z.string(), title: z.string() })),
|
||
})
|
||
.nullable(),
|
||
})
|
||
|
||
/**
|
||
* 课堂看板里老师布置这节课的题:按输入顺序给展示题号,最多 20 道,空数组 = 清掉。
|
||
* `language` 是这份作业用什么语言做;不传当 null(上线时还开着的旧看板页面不带它)
|
||
*/
|
||
export const classLessonRequestSchema = z.object({
|
||
className: z.string().trim().min(1),
|
||
problemDisplayIds: z.array(z.string().trim().min(1)).max(20),
|
||
language: lessonLanguageSchema.optional(),
|
||
})
|
||
|
||
export const classBoardProblemSchema = z.object({
|
||
problemId: z.number().int(),
|
||
problemDisplayId: z.string(),
|
||
title: z.string(),
|
||
/** SQL 题不受布置的语言限制:格子数的是 SQL 交的,点进提交列表也按 SQL 筛 */
|
||
isSql: z.boolean(),
|
||
})
|
||
|
||
export const classBoardCellSchema = z.object({
|
||
/**
|
||
* accepted 看的是题库里(不含比赛)有没有通过过,不限今天 —— 以前做过的也算做完。
|
||
* 布置时选了语言的,代码的次数和通过都只数那个语言(流程图照旧)
|
||
*/
|
||
status: z.enum(["accepted", "tried", "none"]),
|
||
/** 今天在这道题上交了几次(代码 + 流程图) */
|
||
attempts: z.number().int(),
|
||
/**
|
||
* 上面那个数拆开:代码几次、流程图几次。点格子要知道该跳到提交列表的哪一边 ——
|
||
* 这节课整班在画流程图时,跳到代码那边是一片空
|
||
*/
|
||
codeAttempts: z.number().int(),
|
||
flowchartAttempts: z.number().int(),
|
||
/** 第一次通过的时刻;今天之前通过的,前端显示成「之前」 */
|
||
acceptedAt: z.string().nullable(),
|
||
/**
|
||
* 还没做对时,今天最后一次**代码**提交:点格子打开它讲错误,题卡片下面按错法归堆。
|
||
* 做对了、今天没交代码(只画了流程图)的为 null。
|
||
*
|
||
* 只带归类要用的那几样(runtime_error / sample_check 和提交里存的同一份),
|
||
* 不带代码和原始报错 —— 一班四五十人,真要看代码的时候按 submissionId 再拉
|
||
*/
|
||
latest: z
|
||
.object({
|
||
submissionId: z.string(),
|
||
result: judgeStatusSchema,
|
||
runtimeError: statisticInfoSchema.shape.runtime_error,
|
||
sampleCheck: statisticInfoSchema.shape.sample_check,
|
||
})
|
||
.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(),
|
||
/**
|
||
* 今天交过**非比赛**的代码 / 画过流程图没有。点名字跳提交列表用:lastSubmitAt 把比赛提交
|
||
* 也算进去了,而提交列表只列非比赛的,只看它会跳到一个空列表
|
||
*/
|
||
codeToday: z.boolean(),
|
||
drawnToday: z.boolean(),
|
||
/** 最近几节课(不含今天,个数见 ClassBoard.recentLessons)里交过题的有几节 */
|
||
recentAttended: z.number().int(),
|
||
/**
|
||
* 他此刻开着哪道题的代码编辑器(展示题号),没开着为 null。老师点它就能打开他正在写的
|
||
* 代码,不用等他举手。只认连着**本站**的(和课堂求助一样,在 serve 进程的内存里)
|
||
*/
|
||
editing: z.string().nullable(),
|
||
/**
|
||
* 此刻在线(5 分钟内有动静)、今天最后一次上线的时刻(今天没上过线为 null)。
|
||
* 机房两人共用一台电脑时同桌只有一个登着,所以「谁没来」看 seenAt,不看 online。
|
||
* 和 editing 一样只认**本站**:服务器和机房各记各的(在各自的 Redis 里)
|
||
*/
|
||
online: z.boolean(),
|
||
seenAt: z.string().nullable(),
|
||
})
|
||
|
||
/**
|
||
* 课堂看板:一个班、今天、这节课的几道题 × 全班学生。
|
||
* className 为 null = 没指定班级、最近两小时也没有哪个班在交(没法猜)。
|
||
*/
|
||
export const classBoardSchema = z.object({
|
||
className: z.string().nullable(),
|
||
day: z.string(),
|
||
source: z.enum(["teacher", "inferred"]).nullable(),
|
||
/** 今天布置的语言(见 lessonLanguageSchema) */
|
||
language: lessonLanguageSchema,
|
||
/** 今天第一次布置的时刻,之后改题不变;推断出来的课为 null */
|
||
assignedAt: z.string().nullable(),
|
||
/** 这个班最近一次布置选的语言(含今天),给语言下拉当默认值;从没选过为 null */
|
||
lastLanguage: lessonLanguageSchema,
|
||
problems: z.array(classBoardProblemSchema),
|
||
students: z.array(classBoardStudentSchema),
|
||
/**
|
||
* 「最近几节课」一共几节(最多 5,不含今天)。一节课 = 这个班同学一起做题的一天
|
||
* (同班同一天 ≥ 5 人做同一道题,和「班里在做」同一个判据)。新班、开学头几周不满 5。
|
||
*/
|
||
recentLessons: z.number().int(),
|
||
})
|
||
|
||
/**
|
||
* 提交列表「这节课」那颗按钮要的:哪个班、这节课是哪几道题。和课堂看板同一套取法
|
||
* (老师布置过就用布置的,否则按今天同班提交推断),但不带花名册 —— 列表只拿它当筛选条件。
|
||
* className 为 null = 没指定、最近两小时也没有哪个班在交。
|
||
*/
|
||
export const classLessonSchema = z.object({
|
||
className: z.string().nullable(),
|
||
source: z.enum(["teacher", "inferred"]).nullable(),
|
||
/**
|
||
* 选了语言时,列表那边连语言一起筛。C / Python 的作业里夹了 SQL 题就是 null:
|
||
* 列表只能筛一种语言,按 C 筛会把 SQL 题交的全藏起来,宁可多列一些
|
||
*/
|
||
language: lessonLanguageSchema,
|
||
problems: z.array(z.object({ problemDisplayId: z.string(), title: z.string() })),
|
||
})
|
||
|
||
/**
|
||
* 老师首页「今天的课」:今天在上课的每个班一行(同班 ≥5 人一起交题,或者老师在看板上布置过)。
|
||
* 题目的人数是今天的(和看板同一套取法),「全做对」看的是这几道题有没有做对过,不限今天
|
||
*/
|
||
export const classTodaySchema = z.object({
|
||
classes: z.array(
|
||
z.object({
|
||
className: z.string(),
|
||
source: z.enum(["teacher", "inferred"]).nullable(),
|
||
/** 老师布置的时刻;推断出来的为 null */
|
||
assignedAt: z.string().nullable(),
|
||
problems: z.array(
|
||
z.object({
|
||
problemDisplayId: z.string(),
|
||
title: z.string(),
|
||
userCount: z.number().int(),
|
||
acceptedCount: z.number().int(),
|
||
}),
|
||
),
|
||
/** 今天交过题的人数 / 全班人数 */
|
||
activeCount: z.number().int(),
|
||
rosterCount: z.number().int(),
|
||
/** 这节课的题全做对了的人数 */
|
||
allDoneCount: z.number().int(),
|
||
/** 今天最后一次有人交题;还没人交(只布置了)为 null */
|
||
lastSubmitAt: z.string().nullable(),
|
||
}),
|
||
),
|
||
})
|
||
|
||
export type ClassActivity = z.infer<typeof classActivitySchema>
|
||
export type ClassToday = z.infer<typeof classTodaySchema>
|
||
export type LastVisit = z.infer<typeof lastVisitSchema>
|
||
export type ClassLessonRequest = z.infer<typeof classLessonRequestSchema>
|
||
export type ClassBoard = z.infer<typeof classBoardSchema>
|
||
export type ClassLesson = z.infer<typeof classLessonSchema>
|
||
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>
|