feat(布置作业): 能布置「画流程图」「先画再写」,看板分清卡在画图还是写代码
Deploy / deploy (push) Canceled after 0s

布置时多一个「做什么」:写代码 / 先画再写 / 画流程图(只能选打开了画流程图的题)。
- 先画再写:一道题图画到 A/S、代码也交对才算做完;学生画一道写一道、全画完再写都行,
  打开题目自动停在该做的那一步(图没画对停画布,画对了换到布置的语言)
- 也能分开布置:先一节画流程图,看板上「用这几道题写代码」再开一节;学生那边把今天
  几节课的要求合起来,还没画完的照样看得到要画
- 看板格子是一条进度:图 1/6 → 空心「图 ✓」→ 代码 ✗ → ✓,先写代码没画图的写「缺图」;
  卡在画图上的点开讲那张图(要改的框标红 + AI 的哪里错/怎么改),按走对几组归堆
- 写代码的课不再拿画对的流程图顶替做完;数据统计里画流程图的课数流程图提交

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-11-02 21:50:48 +08:00
1 parent 71323d9b63
commit 38a331c2b7
33 files changed
+4987 -209

No files matched your search

+56 -2
View File
@@ -1,9 +1,12 @@
import { z } from 'zod'
import { judgeStatusSchema } from './judge-status'
import { lessonLanguageSchema as lessonLanguageValueSchema } from './language'
import { lessonLanguageSchema as lessonLanguageValueSchema, lessonModeSchema } from './language'
import { statisticInfoSchema } from './submission'
/** 课堂条上一道题一步的状态(画 / 写),和 myStatus 同一套取值 */
const stepStatusSchema = z.enum(['accepted', 'tried', 'none'])
/**
* 「班里在做」:同班同一天有好几个人做的题,基本就是老师在课上点名的那几道。
* `day` 是东八区日历日;最近 7 天都没有这样的题时为 null,`problems` 为空。
@@ -20,6 +23,15 @@ export const classActivityProblemSchema = z.object({
* 代码只认用那个语言交的(流程图照旧算)
*/
myStatus: z.enum(['accepted', 'tried', 'none']),
/**
* 今天布置的课要这道题画流程图 / 写代码时,这一步我做到哪了;不要这一步为 null。
* 推断出来的课两个都是 null(不分步,myStatus 照旧代码或流程图做对都算)。
*
* 一道题今天可能被布置了两次(先布置画流程图、再布置写代码):两节课的要求合起来算,
* 学生那边就和「先画再写」一样是两步 —— 老师开了写代码的那节,还没画完的人照样看得到要画
*/
draw: stepStatusSchema.nullable(),
code: stepStatusSchema.nullable(),
})
/**
@@ -34,7 +46,10 @@ export const classActivitySchema = z.object({
day: z.string().nullable(),
/** teacher = 老师在课堂看板里布置的;inferred = 从同班提交记录推断的 */
source: z.enum(['teacher', 'inferred']).nullable(),
/** 写代码那一步用的语言:今天最后布置的、要写代码的那节选的(只画流程图的课为 null) */
language: lessonLanguageSchema,
/** 今天最后布置的那节做什么;推断出来的课为 null */
mode: lessonModeSchema.nullable(),
problems: z.array(classActivityProblemSchema),
})
@@ -86,6 +101,8 @@ export const classLessonRequestSchema = z.object({
className: z.string().trim().min(1),
problemDisplayIds: z.array(z.string().trim().min(1)).max(20),
language: lessonLanguageSchema.nullable(),
/** 不带 = 写代码(开着的旧页面发的就是这种)。画流程图时 language 不用,存成 null */
mode: lessonModeSchema.default('code'),
lessonId: z.number().int().positive().optional(),
newLesson: z.boolean().optional(),
})
@@ -114,8 +131,39 @@ export const classBoardCellSchema = z.object({
*/
codeAttempts: z.number().int(),
flowchartAttempts: z.number().int(),
/** 第一次通过的时刻;今天之前通过的,前端显示成「之前」 */
/**
* 第一次通过的时刻;今天之前通过的,前端显示成「之前」。先画再写的课是两步都做到的那一刻
* (晚的那一步)
*/
acceptedAt: z.string().nullable(),
/**
* 代码这一步:交对过(不限今天)/ 交过没对 / 没交过。写代码的课和 status 一样;先画再写的课
* 要它分出「图画对了、代码还没对」
*/
codeStatus: stepStatusSchema,
/**
* 流程图这一步,只在这节课要画流程图时有(mode 是 flowchart / both),否则 null。
* - `passedAt`:第一次画到 A / S 的时刻(不限今天)
* - `latest`:还没画对时最后一张图(不限今天,和 passedAt 同一个范围):点格子打开它讲哪里画错了,
* 题卡片下面按走对几组归堆。评分中、评失败的也是它(passed / total 为 null)
*/
flowchart: z
.object({
status: stepStatusSchema,
passedAt: z.string().nullable(),
latest: z
.object({
id: z.string(),
/** 0 排队 1 评分中 2 评完 3 评失败(flowchartStatusSchema) */
status: z.number().int(),
grade: z.string().nullable(),
/** 走对几组 / 一共几组;评完之前、2026-10 之前的老评法为 null */
passed: z.number().int().nullable(),
total: z.number().int().nullable(),
})
.nullable(),
})
.nullable(),
/**
* 还没做对时,今天最后一次**代码**提交:点格子打开它讲错误,题卡片下面按错法归堆。
* 做对了、今天没交代码(只画了流程图)的为 null。
@@ -188,6 +236,8 @@ export const classBoardSchema = z.object({
lessonId: z.number().int().nullable(),
/** 这一节布置的语言(见 lessonLanguageSchema) */
language: lessonLanguageSchema,
/** 这一节做什么;推断出来的课为 null(代码交对或流程图画对都算) */
mode: lessonModeSchema.nullable(),
/** 这一节第一次布置的时刻,之后改题不变;推断出来的课为 null */
assignedAt: z.string().nullable(),
/** 第一次布置的老师(真名,没有真名用用户名);推断出来的课为 null */
@@ -266,6 +316,8 @@ export const lessonUpdateSchema = z.object({
type: z.literal('lesson_update'),
kind: z.enum(['assigned', 'changed', 'cleared']),
language: lessonLanguageSchema,
/** 清掉了为 null。languageChanged 连它一起算:改成画流程图、改成先画再写也是「做法变了」 */
mode: lessonModeSchema.nullable(),
problems: z.array(lessonUpdateProblemSchema),
added: z.array(lessonUpdateProblemSchema),
removed: z.array(lessonUpdateProblemSchema),
@@ -301,6 +353,7 @@ export const adminLessonSchema = z.object({
/** 布置的老师:真名,没有真名用用户名(和看板「谁 布置」一样) */
createdBy: z.string(),
language: lessonLanguageSchema,
mode: lessonModeSchema,
/** 按布置的顺序;题后来被藏起来的 visible 为 false(统计、学生那边都不再出现) */
problems: z.array(z.object({ problemDisplayId: z.string(), title: z.string(), visible: z.boolean() })),
/** 班里的学生数(STUDENT_ROLES、没禁用),和 doneCount 同一个花名册 */
@@ -325,6 +378,7 @@ export const adminLessonRequestSchema = z.object({
day: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
time: z.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/),
language: lessonLanguageSchema,
mode: lessonModeSchema.default('code'),
problemDisplayIds: z.array(z.string().trim().min(1)).min(1).max(20),
})
export type AdminLesson = z.infer<typeof adminLessonSchema>
+18
View File
@@ -47,6 +47,24 @@ export const runnableLanguageSchema = judgeLanguageSchema.extract(['C', 'C++', '
*/
export const lessonLanguageSchema = problemLanguageSchema.extract(['Python', 'C', 'C++', 'SQL'])
/**
* 这节课做什么,老师布置时选(2026-11 加):
* - `code`:写代码,「做完」只认代码交对(加这一项之前布置的课都是它);
* - `flowchart`:画流程图,只认画到 A / S,不选语言(language 为 null);
* - `both`:先画再写,一道题画到 A / S **并且**代码交对才算做完。学生画一道写一道、
* 全画完再写都行,所以是一节课里的两步,不是两节课。
*
* 画流程图的两种只能布置打开了「让学生自己画」的题(problem.allowFlowchart)。
* 推断出来的课没有它(null):代码交对或流程图画到 A / S 都算做完,和加这一项之前一样
*/
export const lessonModeSchema = z.enum(['code', 'flowchart', 'both'])
export type LessonMode = z.infer<typeof lessonModeSchema>
export const LESSON_MODE_LABEL: Record<LessonMode, string> = { code: '写代码', flowchart: '画流程图', both: '先画再写' }
/** 这种课要不要画、要不要写 */
export function lessonNeeds(mode: LessonMode) {
return { draw: mode !== 'code', code: mode !== 'flowchart' }
}
/**
* SQL 题:可选语言里有 SQL(后端保存时强校验 SQL 只能单独出现)。题型只认这一条,
* 前后端一律走它,别再各自看 sqlConfig 或 languages
+6 -1
View File
@@ -2,7 +2,7 @@ import { z } from 'zod'
import { paginatedSchema } from './common'
import { judgeStatusSchema, type JudgeStatus } from './judge-status'
import { problemLanguageSchema, runnableLanguageSchema } from './language'
import { lessonModeSchema, problemLanguageSchema, runnableLanguageSchema } from './language'
/**
* 判题机原始输出(`submission.info` 的 JSONB 原文)。**只是类型,不作运行时校验。**
@@ -478,6 +478,11 @@ export const submissionLessonsSchema = z.object({
problems: z.array(z.string()),
/** 老师在课堂看板布置的课(不是从提交推断的) */
assigned: z.boolean(),
/**
* 布置的课做什么(写代码 / 画流程图 / 先画再写);推断的课为 null。要画流程图的课,次数和
* 正确率连流程图一起数(画到 A / S 算对)
*/
mode: lessonModeSchema.nullable(),
/**
* 布置的课:班里把布置的题全做对了的人数(那天结束前,以前做对的也算;布置时选了语言的
* 只认那个语言,流程图评到 A / S 也算 —— 和看板同一个口径)。推断的课为 null