Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
530e1a3d5d
commit
195069450f
480 files changed
+15507
-24650
No files matched your search
@@ -1,8 +1,8 @@
|
||||
import { z } from "zod"
|
||||
import { z } from 'zod'
|
||||
|
||||
import { sampleUserSchema } from "./common"
|
||||
import { achievementRaritySchema } from "./achievement"
|
||||
import { userProfileSchema } from "./auth"
|
||||
import { sampleUserSchema } from './common'
|
||||
import { achievementRaritySchema } from './achievement'
|
||||
import { userProfileSchema } from './auth'
|
||||
|
||||
/** 个性签名最多几个字。以前是 256,排名上要露出来后改成 30(九成签名本来就不超过 26 字) */
|
||||
export const MOOD_MAX = 30
|
||||
@@ -49,7 +49,7 @@ export const weeklyRankItemSchema = z.object({
|
||||
export const weeklyRankSchema = z.object({
|
||||
/** 本周一 0:00(东八区)对应的 UTC 时刻,前端拿它显示统计区间 */
|
||||
start: z.string(),
|
||||
scope: z.enum(["global", "class"]),
|
||||
scope: z.enum(['global', 'class']),
|
||||
/** `scope = "class"` 时是我的班号,全服榜为 null */
|
||||
className: z.string().nullable(),
|
||||
/** 本周有新增 AC 的总人数。榜面只回前几名,这个数是全量 */
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod"
|
||||
import { z } from 'zod'
|
||||
|
||||
export const achievementRaritySchema = z.enum(["bronze", "silver", "gold", "platinum"])
|
||||
export const achievementRaritySchema = z.enum(['bronze', 'silver', 'gold', 'platinum'])
|
||||
|
||||
export const achievementSchema = z.object({
|
||||
id: z.number().int(),
|
||||
@@ -10,7 +10,7 @@ export const achievementSchema = z.object({
|
||||
rarity: achievementRaritySchema,
|
||||
hidden: z.boolean(),
|
||||
metric: z.string().nullable(),
|
||||
operator: z.enum(["gte", "lte"]).nullable(),
|
||||
operator: z.enum(['gte', 'lte']).nullable(),
|
||||
threshold: z.number().int().nullable(),
|
||||
unlocked: z.boolean(),
|
||||
unlockTime: z.string().nullable(),
|
||||
@@ -65,7 +65,7 @@ export const pendingAchievementSchema = z.object({
|
||||
* 题单奖章和成就来自两张表、id 会重叠,前端靠它区分(见 events.ts 的 publishAchievementNotification)。
|
||||
*/
|
||||
export const achievementNotificationSchema = pendingAchievementSchema.extend({
|
||||
kind: z.enum(["achievement", "badge"]),
|
||||
kind: z.enum(['achievement', 'badge']),
|
||||
})
|
||||
|
||||
export const markAchievementsReadSchema = z.object({
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { z } from "zod"
|
||||
import { z } from 'zod'
|
||||
|
||||
import { ADMIN_TYPES, PROBLEM_PERMISSIONS } from "./roles"
|
||||
import { ADMIN_TYPES, PROBLEM_PERMISSIONS } from './roles'
|
||||
|
||||
import { achievementRaritySchema } from "./achievement"
|
||||
import { rankProfileSchema } from "./account"
|
||||
import { paginatedSchema, sampleUserSchema } from "./common"
|
||||
import { reactionKeySchema } from "./content"
|
||||
import { badgeConditionTypeSchema } from "./problemset"
|
||||
import { problemLanguageSchema, runnableLanguageSchema } from "./language"
|
||||
import { achievementRaritySchema } from './achievement'
|
||||
import { rankProfileSchema } from './account'
|
||||
import { paginatedSchema, sampleUserSchema } from './common'
|
||||
import { reactionKeySchema } from './content'
|
||||
import { badgeConditionTypeSchema } from './problemset'
|
||||
import { problemLanguageSchema, runnableLanguageSchema } from './language'
|
||||
import {
|
||||
astRuleSchema,
|
||||
astRulesSchema,
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
sqlConfigSchema,
|
||||
sqlDisplaySchema,
|
||||
tagCategorySchema,
|
||||
} from "./problem"
|
||||
} from './problem'
|
||||
|
||||
// 放这里而不是 roles.ts:roles.ts 要保持不 import zod,理由见那边的文件头
|
||||
export const adminTypeSchema = z.enum(ADMIN_TYPES)
|
||||
@@ -68,7 +68,7 @@ export type AdminAnnouncementListItem = z.infer<typeof adminAnnouncementListItem
|
||||
|
||||
// ---------------------------------------------------------------- 教程 / 练习
|
||||
|
||||
export const tutorialTypeSchema = z.enum(["python", "c"])
|
||||
export const tutorialTypeSchema = z.enum(['python', 'c'])
|
||||
|
||||
/** 后台一课挂的一道「去做几道题」。按 tutorial_problem.order 排 */
|
||||
export const adminTutorialProblemSchema = z.object({
|
||||
@@ -156,15 +156,7 @@ export const tutorialProblemCandidateSchema = adminTutorialProblemSchema.extend(
|
||||
tags: z.array(z.string()),
|
||||
})
|
||||
|
||||
export const exerciseTypeSchema = z.enum([
|
||||
"mcq",
|
||||
"sort",
|
||||
"fill",
|
||||
"match",
|
||||
"predict",
|
||||
"debug",
|
||||
"group",
|
||||
])
|
||||
export const exerciseTypeSchema = z.enum(['mcq', 'sort', 'fill', 'match', 'predict', 'debug', 'group'])
|
||||
|
||||
export const adminExerciseSchema = z.object({
|
||||
id: z.number().int(),
|
||||
@@ -227,7 +219,7 @@ export const toggleAiReportPinResponseSchema = z.object({
|
||||
// ---------------------------------------------------------------- 成就
|
||||
|
||||
// rarity 复用 achievement.ts 里已有的定义,不重复声明 —— 两份枚举迟早会长歪
|
||||
export const achievementOperatorSchema = z.enum(["gte", "lte"])
|
||||
export const achievementOperatorSchema = z.enum(['gte', 'lte'])
|
||||
|
||||
export const achievementMetricSchema = z.object({
|
||||
key: z.string(),
|
||||
@@ -309,14 +301,12 @@ export const updateUserRequestSchema = z.object({
|
||||
realName: z.string().max(32).nullable().default(null),
|
||||
isDisabled: z.boolean(),
|
||||
// 空串表示不改密码,与旧 EditUserSerializer 的 allow_blank 一致
|
||||
password: z.string().max(128).default(""),
|
||||
password: z.string().max(128).default(''),
|
||||
})
|
||||
|
||||
/** 导入用户:每行 [用户名, 密码, 邮箱, 真名],与前端粘贴的 Excel 列序一致 */
|
||||
export const importUsersRequestSchema = z.object({
|
||||
users: z
|
||||
.array(z.tuple([z.string().trim().min(1).max(32), z.string().min(1), z.string(), z.string()]))
|
||||
.min(1),
|
||||
users: z.array(z.tuple([z.string().trim().min(1).max(32), z.string().min(1), z.string(), z.string()])).min(1),
|
||||
})
|
||||
|
||||
export const deleteUsersRequestSchema = z.object({
|
||||
@@ -348,7 +338,7 @@ export const judgeServerSchema = z.object({
|
||||
serviceUrl: z.string().nullable(),
|
||||
isDisabled: z.boolean(),
|
||||
/** 心跳在 6 秒内才算在线,与 dashboard 的判活口径一致 */
|
||||
status: z.enum(["normal", "abnormal"]),
|
||||
status: z.enum(['normal', 'abnormal']),
|
||||
})
|
||||
|
||||
export const judgeServerListSchema = z.object({
|
||||
@@ -367,7 +357,7 @@ export const orphanTestCaseSchema = z.object({
|
||||
|
||||
/** 上传目录里、库里哪儿都没提到的图片:编辑器插的图(upload)和换下来的头像(avatar) */
|
||||
export const orphanImageSchema = z.object({
|
||||
folder: z.enum(["upload", "avatar"]),
|
||||
folder: z.enum(['upload', 'avatar']),
|
||||
name: z.string(),
|
||||
url: z.string(),
|
||||
/** 字节 */
|
||||
@@ -434,8 +424,8 @@ export const adminContestSchema = z.object({
|
||||
password: z.string().nullable(),
|
||||
visible: z.boolean(),
|
||||
createdBy: sampleUserSchema,
|
||||
status: z.enum(["1", "0", "-1"]),
|
||||
contestType: z.enum(["Public", "Password Protected"]),
|
||||
status: z.enum(['1', '0', '-1']),
|
||||
contestType: z.enum(['Public', 'Password Protected']),
|
||||
})
|
||||
|
||||
/** 后台比赛列表的一行:比赛本身,加几道题、几人参加(交过题的人数) */
|
||||
@@ -494,7 +484,7 @@ export const adminProblemSetListSchema = paginatedSchema(adminProblemSetSchema)
|
||||
|
||||
export const createProblemSetRequestSchema = z.object({
|
||||
title: z.string().trim().min(1).max(200),
|
||||
description: z.string().default(""),
|
||||
description: z.string().default(''),
|
||||
/** 布置到哪天;null = 不布置。从「没在布置」变成「布置到将来」时,服务端把布置开始记成现在 */
|
||||
assignedUntil: z.string().nullable().default(null),
|
||||
visible: z.boolean().default(true),
|
||||
@@ -587,7 +577,7 @@ export const renameTagResponseSchema = z.object({
|
||||
export const batchProblemTagRequestSchema = z.object({
|
||||
problemIds: z.array(z.number().int().positive()).min(1),
|
||||
tagNames: z.array(z.string()).min(1),
|
||||
action: z.enum(["add", "remove"]),
|
||||
action: z.enum(['add', 'remove']),
|
||||
})
|
||||
|
||||
/** 勾了几道题,一次让学生看得到 / 藏起来。只认公开题、只认自己能改的 */
|
||||
@@ -646,9 +636,7 @@ export const stuckProblemDetailSchema = z.object({
|
||||
/** 这些人错的那些次,按判题结果分 */
|
||||
errors: z.array(z.object({ result: z.number().int(), count: z.number().int() })),
|
||||
/** 全站历年(学年按 9 月起算,`year` 是起始年),不受班级和时间段影响 */
|
||||
history: z.array(
|
||||
z.object({ year: z.number().int(), users: z.number().int(), firstTry: z.number().int() }),
|
||||
),
|
||||
history: z.array(z.object({ year: z.number().int(), users: z.number().int(), firstTry: z.number().int() })),
|
||||
})
|
||||
|
||||
const gradeTrendSideSchema = z.object({ users: z.number().int(), firstTry: z.number().int() })
|
||||
@@ -732,9 +720,7 @@ export const learnBoardSchema = z.object({
|
||||
project: z.string(),
|
||||
exerciseTotal: z.number().int(),
|
||||
/** 这课的配套题,按老师排的顺序;藏起来的题不算 */
|
||||
problems: z.array(
|
||||
z.object({ id: z.number().int(), displayId: z.string(), title: z.string() }),
|
||||
),
|
||||
problems: z.array(z.object({ id: z.number().int(), displayId: z.string(), title: z.string() })),
|
||||
}),
|
||||
),
|
||||
students: z.array(learnBoardStudentSchema),
|
||||
@@ -871,9 +857,7 @@ export const adminProblemListSchema = paginatedSchema(adminProblemListItemSchema
|
||||
* 「全部 / 学生看得到 / 藏着的」三个按钮上的数:除了可见性,筛选条件和列表一样。
|
||||
* 只有公开题列表给,比赛题列表没有这组按钮
|
||||
*/
|
||||
counts: z
|
||||
.object({ all: z.number().int(), visible: z.number().int(), hidden: z.number().int() })
|
||||
.optional(),
|
||||
counts: z.object({ all: z.number().int(), visible: z.number().int(), hidden: z.number().int() }).optional(),
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -978,10 +962,7 @@ export const challengeInputSchema = z.object({
|
||||
rules: z.array(astRuleSchema).min(1).max(CHALLENGE_RULES_MAX),
|
||||
})
|
||||
|
||||
export const challengeInputsSchema = z.record(
|
||||
z.string(),
|
||||
z.array(challengeInputSchema).max(CHALLENGES_PER_LANGUAGE_MAX),
|
||||
)
|
||||
export const challengeInputsSchema = z.record(z.string(), z.array(challengeInputSchema).max(CHALLENGES_PER_LANGUAGE_MAX))
|
||||
|
||||
export const createProblemRequestSchema = z.object({
|
||||
_id: z.string().trim().min(1).max(32),
|
||||
@@ -1006,7 +987,7 @@ export const createProblemRequestSchema = z.object({
|
||||
languages: z.array(problemLanguageSchema).min(1),
|
||||
template: z.record(z.string(), z.string()),
|
||||
visible: z.boolean(),
|
||||
difficulty: z.enum(["Low", "Mid", "High"]),
|
||||
difficulty: z.enum(['Low', 'Mid', 'High']),
|
||||
tags: z.array(z.string().max(PROBLEM_TAG_MAX_LENGTH)).min(1),
|
||||
hint: z.string().nullable().default(null),
|
||||
source: z.string().max(256).nullable().default(null),
|
||||
@@ -1082,7 +1063,7 @@ export const sqlPreviewRequestSchema = z.object({
|
||||
.string()
|
||||
.min(1)
|
||||
.max(1024 * 1024),
|
||||
mode: z.enum(["query", "modify"]),
|
||||
mode: z.enum(['query', 'modify']),
|
||||
/**
|
||||
* 这组是不是题面上给学生看的那组(第 1 组)。增删改题只有这一组必须改到行 ——
|
||||
* 其余组可以一行都不改,用来看学生会不会多改(比如 WHERE 写漏了)
|
||||
@@ -1095,7 +1076,7 @@ export const generateSqlTestCaseRequestSchema = z.object({
|
||||
.string()
|
||||
.min(1)
|
||||
.max(64 * 1024),
|
||||
mode: z.enum(["query", "modify"]),
|
||||
mode: z.enum(['query', 'modify']),
|
||||
})
|
||||
|
||||
export const generateSqlTestCaseResponseSchema = z.object({ sql: z.string() })
|
||||
@@ -1115,7 +1096,7 @@ export const TEST_CASE_MAX_CASES = 20
|
||||
export const TEST_CASE_EDIT_MAX_CASES = 100
|
||||
export const TEST_CASE_EDIT_MAX_FILE_BYTES = 32 * 1024
|
||||
|
||||
export const testCaseFilesSchema = z.discriminatedUnion("editable", [
|
||||
export const testCaseFilesSchema = z.discriminatedUnion('editable', [
|
||||
z.object({
|
||||
editable: z.literal(true),
|
||||
cases: z.array(z.object({ input: z.string(), output: z.string() })),
|
||||
@@ -1291,9 +1272,7 @@ export type UpdateProblemRequest = z.input<typeof updateProblemRequestSchema>
|
||||
export type MakeProblemPublicRequest = z.input<typeof makeProblemPublicRequestSchema>
|
||||
export type AddContestProblemRequest = z.input<typeof addContestProblemRequestSchema>
|
||||
export type ReorderContestProblemsRequest = z.input<typeof reorderContestProblemsRequestSchema>
|
||||
export type ReorderProblemSetProblemsRequest = z.input<
|
||||
typeof reorderProblemSetProblemsRequestSchema
|
||||
>
|
||||
export type ReorderProblemSetProblemsRequest = z.input<typeof reorderProblemSetProblemsRequestSchema>
|
||||
export type SetTagCategoryRequest = z.input<typeof setTagCategoryRequestSchema>
|
||||
export type CheckUsernamesRequest = z.input<typeof checkUsernamesRequestSchema>
|
||||
export type TestCaseEntry = z.infer<typeof testCaseEntrySchema>
|
||||
|
||||
+29
-31
@@ -1,18 +1,18 @@
|
||||
import { z } from "zod"
|
||||
import { z } from 'zod'
|
||||
|
||||
import { classPkPeriodSchema } from "./ranking"
|
||||
import { classPkPeriodSchema } from './ranking'
|
||||
|
||||
/**
|
||||
* 智能分析看哪段时间。全按东八区日历切(同排名页):这周 = 本周一零点起,这个月 = 本月 1 日
|
||||
* 零点起,这学期 = `termStart()`(9 月 1 日 / 2 月 1 日),全部 = 不设下限。
|
||||
*/
|
||||
export const learningPeriodSchema = z.enum(["week", "month", "term", "all"])
|
||||
export const learningPeriodSchema = z.enum(['week', 'month', 'term', 'all'])
|
||||
|
||||
export const LEARNING_PERIOD_LABELS = {
|
||||
week: "这周",
|
||||
month: "这个月",
|
||||
term: "这学期",
|
||||
all: "全部",
|
||||
week: '这周',
|
||||
month: '这个月',
|
||||
term: '这学期',
|
||||
all: '全部',
|
||||
} as const satisfies Record<z.infer<typeof learningPeriodSchema>, string>
|
||||
|
||||
/**
|
||||
@@ -27,7 +27,7 @@ export const LEARNING_PERIOD_LABELS = {
|
||||
export const learningItemSchema = z.object({
|
||||
displayId: z.string(),
|
||||
title: z.string(),
|
||||
status: z.enum(["first", "tries", "fail", "none"]),
|
||||
status: z.enum(['first', 'tries', 'fail', 'none']),
|
||||
attempts: z.number().int(),
|
||||
/** 班上第几个做对(只数这段时间里做对的同班同学);没做对、没有班级时为 null */
|
||||
rank: z.number().int().nullable(),
|
||||
@@ -80,9 +80,7 @@ export const learningReportSchema = z.object({
|
||||
/** 没通过的代码提交按判题状态分组,多的在前;系统错误、还没判完的不算 */
|
||||
errors: z.array(z.object({ result: z.number().int(), count: z.number().int() })),
|
||||
/** 全部知识点(tag 的 knowledge 类),按题库里题数从多到少;没碰过的 solved = tried = 0 */
|
||||
knowledge: z.array(
|
||||
z.object({ name: z.string(), solved: z.number().int(), tried: z.number().int() }),
|
||||
),
|
||||
knowledge: z.array(z.object({ name: z.string(), solved: z.number().int(), tried: z.number().int() })),
|
||||
/** 老师看学生时,同班按用户名排的前一个 / 后一个;学生看自己时为 null */
|
||||
neighbors: z.object({ prev: z.string().nullable(), next: z.string().nullable() }).nullable(),
|
||||
})
|
||||
@@ -123,11 +121,11 @@ export const HINT_MIN_FAILURES = 1
|
||||
* 老师按班级 / 作业设最高等级还没做,现在所有题都开到 L4。
|
||||
*/
|
||||
export const HINT_LEVELS = [
|
||||
{ level: 0, name: "反问", summary: "只反问,不给结论" },
|
||||
{ level: 1, name: "定位", summary: "只说问题在哪,不说为什么" },
|
||||
{ level: 2, name: "概念", summary: "讲清涉及的概念,不给改法" },
|
||||
{ level: 3, name: "思路", summary: "分步骤讲解题思路,不写代码" },
|
||||
{ level: 4, name: "示例", summary: "用相似的例子演示,或给出关键的一行" },
|
||||
{ level: 0, name: '反问', summary: '只反问,不给结论' },
|
||||
{ level: 1, name: '定位', summary: '只说问题在哪,不说为什么' },
|
||||
{ level: 2, name: '概念', summary: '讲清涉及的概念,不给改法' },
|
||||
{ level: 3, name: '思路', summary: '分步骤讲解题思路,不写代码' },
|
||||
{ level: 4, name: '示例', summary: '用相似的例子演示,或给出关键的一行' },
|
||||
] as const
|
||||
|
||||
export const HINT_MAX_LEVEL = 4
|
||||
@@ -145,7 +143,7 @@ export const HINT_LEVEL_COMPILE = -1
|
||||
|
||||
/** 给界面看的等级名。编译失败那一档不叫 L-1 */
|
||||
export function hintLevelLabel(level: number) {
|
||||
if (level === HINT_LEVEL_COMPILE) return "编译错误"
|
||||
if (level === HINT_LEVEL_COMPILE) return '编译错误'
|
||||
const item = HINT_LEVELS.find((entry) => entry.level === level)
|
||||
// 给学生看的,从 1 数起、不写「L0」:学生不知道 L 是什么,也不会从 0 数
|
||||
return item ? `第 ${item.level + 1} 档 · ${item.name}` : `第 ${level + 1} 档`
|
||||
@@ -180,19 +178,19 @@ export const aiHintDoneSchema = z.object({
|
||||
* 全角冒号、多一个空格、小数位数,是这批学生最常见、也最冤的一类 WA。
|
||||
*/
|
||||
export const HINT_ERROR_TAGS = {
|
||||
syntax: "语法错误",
|
||||
input_format: "输入读取方式不对(格式、分隔、个数)",
|
||||
output_format: "输出格式不对(多余的输入提示语、全角/半角符号、多余空格或换行、小数位数)",
|
||||
condition: "条件判断写错(比较符、漏了分支)",
|
||||
loop_bound: "循环次数或边界不对(差一)",
|
||||
integer_division: "整数除法或取余用错",
|
||||
type_overflow: "数据类型不对或溢出(int 不够、浮点精度)",
|
||||
uninitialized: "变量没初始化,或累加器没清零",
|
||||
missing_case: "漏了特殊情况(0、负数、边界值)",
|
||||
runtime_error: "运行时错误(下标越界、除以零)",
|
||||
timeout: "超时(算法太慢或死循环)",
|
||||
wrong_approach: "思路整体不对",
|
||||
other: "其他,或者看不出来",
|
||||
syntax: '语法错误',
|
||||
input_format: '输入读取方式不对(格式、分隔、个数)',
|
||||
output_format: '输出格式不对(多余的输入提示语、全角/半角符号、多余空格或换行、小数位数)',
|
||||
condition: '条件判断写错(比较符、漏了分支)',
|
||||
loop_bound: '循环次数或边界不对(差一)',
|
||||
integer_division: '整数除法或取余用错',
|
||||
type_overflow: '数据类型不对或溢出(int 不够、浮点精度)',
|
||||
uninitialized: '变量没初始化,或累加器没清零',
|
||||
missing_case: '漏了特殊情况(0、负数、边界值)',
|
||||
runtime_error: '运行时错误(下标越界、除以零)',
|
||||
timeout: '超时(算法太慢或死循环)',
|
||||
wrong_approach: '思路整体不对',
|
||||
other: '其他,或者看不出来',
|
||||
} as const
|
||||
|
||||
export type HintErrorTag = keyof typeof HINT_ERROR_TAGS
|
||||
@@ -206,7 +204,7 @@ export const hintDiagnosisSchema = z.object({
|
||||
tag: z.enum(Object.keys(HINT_ERROR_TAGS) as [HintErrorTag, ...HintErrorTag[]]),
|
||||
/** 问题所在的行号区间(从 1 起,含两端);说不准就是 null */
|
||||
lines: z.tuple([z.number().int().min(1), z.number().int().min(1)]).nullable(),
|
||||
confidence: z.enum(["high", "low"]),
|
||||
confidence: z.enum(['high', 'low']),
|
||||
})
|
||||
|
||||
export type HintDiagnosis = z.infer<typeof hintDiagnosisSchema>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from "zod"
|
||||
import { z } from 'zod'
|
||||
|
||||
export const loginRequestSchema = z.object({
|
||||
username: z.string().trim().min(1).max(150),
|
||||
@@ -15,7 +15,7 @@ export const refreshTokenRequestSchema = z.object({
|
||||
* 前端传 `[WS_AUTH_PROTOCOL, token]` 两个子协议,服务端回第一个 —— 必须回一个前端
|
||||
* 传过的,否则 Chrome 判握手失败。不放 URL 查询参数:URL 会原样进反代的访问日志。
|
||||
*/
|
||||
export const WS_AUTH_PROTOCOL = "oj2.bearer"
|
||||
export const WS_AUTH_PROTOCOL = 'oj2.bearer'
|
||||
|
||||
/**
|
||||
* 登录、换 token 的出参。
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { z } from "zod"
|
||||
import { z } from 'zod'
|
||||
|
||||
import { judgeStatusSchema } from "./judge-status"
|
||||
import { lessonLanguageSchema as lessonLanguageValueSchema } from "./language"
|
||||
import { statisticInfoSchema } from "./submission"
|
||||
import { judgeStatusSchema } from './judge-status'
|
||||
import { lessonLanguageSchema as lessonLanguageValueSchema } from './language'
|
||||
import { statisticInfoSchema } from './submission'
|
||||
|
||||
/**
|
||||
* 「班里在做」:同班同一天有好几个人做的题,基本就是老师在课上点名的那几道。
|
||||
@@ -19,7 +19,7 @@ export const classActivityProblemSchema = z.object({
|
||||
* 我自己在题库里(不含比赛)做这道题的状态,不限那一天。老师布置时选了语言的,
|
||||
* 代码只认用那个语言交的(流程图照旧算)
|
||||
*/
|
||||
myStatus: z.enum(["accepted", "tried", "none"]),
|
||||
myStatus: z.enum(['accepted', 'tried', 'none']),
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -33,7 +33,7 @@ export const classActivitySchema = z.object({
|
||||
className: z.string().nullable(),
|
||||
day: z.string().nullable(),
|
||||
/** teacher = 老师在课堂看板里布置的;inferred = 从同班提交记录推断的 */
|
||||
source: z.enum(["teacher", "inferred"]).nullable(),
|
||||
source: z.enum(['teacher', 'inferred']).nullable(),
|
||||
language: lessonLanguageSchema,
|
||||
problems: z.array(classActivityProblemSchema),
|
||||
})
|
||||
@@ -97,7 +97,7 @@ export const classBoardCellSchema = z.object({
|
||||
* accepted 看的是题库里(不含比赛)有没有通过过,不限今天 —— 以前做过的也算做完。
|
||||
* 布置时选了语言的,代码的次数和通过都只数那个语言(流程图照旧)
|
||||
*/
|
||||
status: z.enum(["accepted", "tried", "none"]),
|
||||
status: z.enum(['accepted', 'tried', 'none']),
|
||||
/** 今天在这道题上交了几次(代码 + 流程图) */
|
||||
attempts: z.number().int(),
|
||||
/**
|
||||
@@ -134,7 +134,7 @@ export const classBoardCellSchema = z.object({
|
||||
* 挑战:done = 做到了这道题的一条挑战(不限今天);doing = 做对之后今天又交了、还没做到 ——
|
||||
* 做完了的学生是在挑战还是闲着,老师一眼看得出。没配挑战的题、没做对的格子为 null
|
||||
*/
|
||||
challenge: z.enum(["done", "doing"]).nullable(),
|
||||
challenge: z.enum(['done', 'doing']).nullable(),
|
||||
})
|
||||
|
||||
export const classBoardStudentSchema = z.object({
|
||||
@@ -174,7 +174,7 @@ export const classBoardStudentSchema = z.object({
|
||||
export const classBoardSchema = z.object({
|
||||
className: z.string().nullable(),
|
||||
day: z.string(),
|
||||
source: z.enum(["teacher", "inferred"]).nullable(),
|
||||
source: z.enum(['teacher', 'inferred']).nullable(),
|
||||
/** 今天布置的语言(见 lessonLanguageSchema) */
|
||||
language: lessonLanguageSchema,
|
||||
/** 今天第一次布置的时刻,之后改题不变;推断出来的课为 null */
|
||||
@@ -199,7 +199,7 @@ export const classBoardSchema = z.object({
|
||||
*/
|
||||
export const classLessonSchema = z.object({
|
||||
className: z.string().nullable(),
|
||||
source: z.enum(["teacher", "inferred"]).nullable(),
|
||||
source: z.enum(['teacher', 'inferred']).nullable(),
|
||||
/**
|
||||
* 选了语言时,列表那边连语言一起筛。C / Python 的作业里夹了 SQL 题就是 null:
|
||||
* 列表只能筛一种语言,按 C 筛会把 SQL 题交的全藏起来,宁可多列一些
|
||||
@@ -216,7 +216,7 @@ export const classTodaySchema = z.object({
|
||||
classes: z.array(
|
||||
z.object({
|
||||
className: z.string(),
|
||||
source: z.enum(["teacher", "inferred"]).nullable(),
|
||||
source: z.enum(['teacher', 'inferred']).nullable(),
|
||||
/** 老师布置的时刻;推断出来的为 null */
|
||||
assignedAt: z.string().nullable(),
|
||||
/** 布置的老师;推断出来的为 null */
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from "zod"
|
||||
import { z } from 'zod'
|
||||
|
||||
export const sampleUserSchema = z.object({
|
||||
id: z.number().int(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod"
|
||||
import { z } from 'zod'
|
||||
|
||||
import { paginatedSchema, sampleUserSchema } from "./common"
|
||||
import { paginatedSchema, sampleUserSchema } from './common'
|
||||
|
||||
export const announcementSchema = z.object({
|
||||
id: z.number().int(),
|
||||
@@ -25,15 +25,7 @@ export const announcementListItemSchema = announcementSchema.omit({
|
||||
|
||||
export const announcementListSchema = paginatedSchema(announcementListItemSchema)
|
||||
|
||||
export const reactionKeySchema = z.enum([
|
||||
"too_easy",
|
||||
"too_hard",
|
||||
"confusing",
|
||||
"buggy",
|
||||
"learned",
|
||||
"interesting",
|
||||
"want_explain",
|
||||
])
|
||||
export const reactionKeySchema = z.enum(['too_easy', 'too_hard', 'confusing', 'buggy', 'learned', 'interesting', 'want_explain'])
|
||||
|
||||
export const reactionCountsSchema = z.record(reactionKeySchema, z.number().int())
|
||||
export const reactionStateSchema = z.object({
|
||||
@@ -70,7 +62,7 @@ export const tutorialSchema = tutorialSummarySchema.extend({
|
||||
problems: z.array(tutorialProblemSchema),
|
||||
isPublic: z.boolean(),
|
||||
order: z.number().int(),
|
||||
type: z.enum(["python", "c"]),
|
||||
type: z.enum(['python', 'c']),
|
||||
createdBy: sampleUserSchema,
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
@@ -167,7 +159,7 @@ export const exerciseDataByType: Record<string, z.ZodType> = {
|
||||
*/
|
||||
export const exerciseSchema = z.object({
|
||||
id: z.number().int(),
|
||||
type: z.enum(["mcq", "sort", "fill", "match", "predict", "debug", "group"]),
|
||||
type: z.enum(['mcq', 'sort', 'fill', 'match', 'predict', 'debug', 'group']),
|
||||
data: z.record(z.string(), z.unknown()),
|
||||
order: z.number().int(),
|
||||
/** 当前学生做对过没有。未登录恒为 false;一课的练一练全做对就算学完这课 */
|
||||
@@ -199,11 +191,7 @@ export type ExerciseAttemptRequest = z.input<typeof exerciseAttemptRequestSchema
|
||||
*
|
||||
* 前端目录、首页、后台「自学情况」都走这一个函数,别各写一份。
|
||||
*/
|
||||
export function isLessonDone(progress: {
|
||||
viewCount: number
|
||||
exerciseTotal: number
|
||||
exerciseSolved: number
|
||||
}) {
|
||||
export function isLessonDone(progress: { viewCount: number; exerciseTotal: number; exerciseSolved: number }) {
|
||||
if (progress.exerciseTotal > 0) return progress.exerciseSolved >= progress.exerciseTotal
|
||||
return progress.viewCount > 0
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { z } from "zod"
|
||||
import { z } from 'zod'
|
||||
|
||||
import { paginatedSchema, sampleUserSchema } from "./common"
|
||||
import { problemDetailSchema, problemListItemSchema } from "./problem"
|
||||
import { paginatedSchema, sampleUserSchema } from './common'
|
||||
import { problemDetailSchema, problemListItemSchema } from './problem'
|
||||
|
||||
export const contestStatusSchema = z.enum(["-1", "0", "1"])
|
||||
export const contestStatusSchema = z.enum(['-1', '0', '1'])
|
||||
|
||||
export const contestSchema = z.object({
|
||||
id: z.number().int(),
|
||||
@@ -16,7 +16,7 @@ export const contestSchema = z.object({
|
||||
lastUpdateTime: z.string(),
|
||||
createdBy: sampleUserSchema,
|
||||
status: contestStatusSchema,
|
||||
contestType: z.enum(["Public", "Password Protected"]),
|
||||
contestType: z.enum(['Public', 'Password Protected']),
|
||||
now: z.string().optional(),
|
||||
/** 列表才带:几道题(可见的)、几个学生交过题 */
|
||||
problemCount: z.number().int().optional(),
|
||||
@@ -68,7 +68,7 @@ export const contestSubmissionInfoSchema = z.object({
|
||||
* 期中、期末:进行中学生看不到排名、每题做对人数、谁最先做对,考完一起公布(2026-10 用户定的)。
|
||||
* 练习照常全看得到 —— 「我需要竞争」。标签只有 练习 / 期中 / 期末 三个值
|
||||
*/
|
||||
export const EXAM_TAGS = ["期中", "期末"] as const
|
||||
export const EXAM_TAGS = ['期中', '期末'] as const
|
||||
export function isExamTag(tag: string) {
|
||||
return (EXAM_TAGS as readonly string[]).includes(tag)
|
||||
}
|
||||
|
||||
@@ -11,16 +11,16 @@
|
||||
* 那些只能看 Mermaid 渲染出来的图,载不回画布,评分也只能退回老办法。
|
||||
*/
|
||||
|
||||
export type BoxKind = "input" | "output" | "process"
|
||||
export type StepKind = BoxKind | "if" | "while"
|
||||
export type BoxKind = 'input' | 'output' | 'process'
|
||||
export type StepKind = BoxKind | 'if' | 'while'
|
||||
|
||||
interface StepBase {
|
||||
id: string
|
||||
text: string
|
||||
}
|
||||
export type BoxStep = StepBase & { kind: BoxKind }
|
||||
export type IfStep = StepBase & { kind: "if"; yes: FlowStep[]; no: FlowStep[] }
|
||||
export type WhileStep = StepBase & { kind: "while"; body: FlowStep[] }
|
||||
export type IfStep = StepBase & { kind: 'if'; yes: FlowStep[]; no: FlowStep[] }
|
||||
export type WhileStep = StepBase & { kind: 'while'; body: FlowStep[] }
|
||||
export type FlowStep = BoxStep | IfStep | WhileStep
|
||||
|
||||
export interface FlowDoc {
|
||||
@@ -29,53 +29,46 @@ export interface FlowDoc {
|
||||
}
|
||||
|
||||
/** 开始、结束在画布上的 id(它们不进数据) */
|
||||
export const FLOW_START_ID = "__start"
|
||||
export const FLOW_END_ID = "__end"
|
||||
export const FLOW_START_ID = '__start'
|
||||
export const FLOW_END_ID = '__end'
|
||||
|
||||
export function flowChildren(step: FlowStep): FlowStep[][] {
|
||||
if (step.kind === "if") return [step.yes, step.no]
|
||||
if (step.kind === "while") return [step.body]
|
||||
if (step.kind === 'if') return [step.yes, step.no]
|
||||
if (step.kind === 'while') return [step.body]
|
||||
return []
|
||||
}
|
||||
|
||||
/** 还没写字的框有几个 */
|
||||
export function countBlankSteps(steps: FlowStep[]): number {
|
||||
return steps.reduce(
|
||||
(sum, step) =>
|
||||
sum +
|
||||
(step.text.trim() ? 0 : 1) +
|
||||
flowChildren(step).reduce((n, c) => n + countBlankSteps(c), 0),
|
||||
0,
|
||||
)
|
||||
return steps.reduce((sum, step) => sum + (step.text.trim() ? 0 : 1) + flowChildren(step).reduce((n, c) => n + countBlankSteps(c), 0), 0)
|
||||
}
|
||||
|
||||
/** 一张图「长什么样」,不带 id:载回旧版、和交过的那一版比,只看结构和字 */
|
||||
export function flowSignature(doc: FlowDoc): string {
|
||||
const strip = (steps: FlowStep[]): unknown[] =>
|
||||
steps.map((step) => [step.kind, step.text.trim(), ...flowChildren(step).map(strip)])
|
||||
const strip = (steps: FlowStep[]): unknown[] => steps.map((step) => [step.kind, step.text.trim(), ...flowChildren(step).map(strip)])
|
||||
return JSON.stringify(strip(doc.steps))
|
||||
}
|
||||
|
||||
// ==================== 读回来的数据 ====================
|
||||
|
||||
const KINDS = new Set<string>(["input", "output", "process", "if", "while"])
|
||||
const KINDS = new Set<string>(['input', 'output', 'process', 'if', 'while'])
|
||||
|
||||
function parseSteps(raw: unknown, prefix: string): FlowStep[] | null {
|
||||
if (!Array.isArray(raw)) return null
|
||||
const out: FlowStep[] = []
|
||||
for (const [index, item] of raw.entries()) {
|
||||
if (!item || typeof item !== "object") return null
|
||||
if (!item || typeof item !== 'object') return null
|
||||
const step = item as Record<string, unknown>
|
||||
const kind = step.kind
|
||||
if (typeof kind !== "string" || !KINDS.has(kind)) return null
|
||||
const id = typeof step.id === "string" && step.id ? step.id : `${prefix}${index}`
|
||||
const text = typeof step.text === "string" ? step.text : ""
|
||||
if (kind === "if") {
|
||||
if (typeof kind !== 'string' || !KINDS.has(kind)) return null
|
||||
const id = typeof step.id === 'string' && step.id ? step.id : `${prefix}${index}`
|
||||
const text = typeof step.text === 'string' ? step.text : ''
|
||||
if (kind === 'if') {
|
||||
const yes = parseSteps(step.yes ?? [], `${id}y`)
|
||||
const no = parseSteps(step.no ?? [], `${id}n`)
|
||||
if (!yes || !no) return null
|
||||
out.push({ id, text, kind, yes, no })
|
||||
} else if (kind === "while") {
|
||||
} else if (kind === 'while') {
|
||||
const body = parseSteps(step.body ?? [], `${id}b`)
|
||||
if (!body) return null
|
||||
out.push({ id, text, kind, body })
|
||||
@@ -88,10 +81,10 @@ function parseSteps(raw: unknown, prefix: string): FlowStep[] | null {
|
||||
|
||||
/** 本地草稿、交过的某一版:是新格式就读出来,旧的 vue-flow 格式或者坏数据给 null */
|
||||
export function parseFlowDoc(raw: unknown): FlowDoc | null {
|
||||
if (!raw || typeof raw !== "object") return null
|
||||
if (!raw || typeof raw !== 'object') return null
|
||||
const data = raw as Record<string, unknown>
|
||||
if (data.version !== 2) return null
|
||||
const steps = parseSteps(data.steps, "p")
|
||||
const steps = parseSteps(data.steps, 'p')
|
||||
return steps ? { version: 2, steps } : null
|
||||
}
|
||||
|
||||
@@ -119,7 +112,7 @@ export function mermaidOrder(doc: FlowDoc): string[] {
|
||||
* 学生不知道扣分是因为一个引号。`#` 必须先转,否则本来就有的 `#quot;` 会被当成实体。
|
||||
*/
|
||||
function escapeLabel(raw: string) {
|
||||
return raw.replace(/#/g, "#35;").replace(/"/g, "#quot;").replace(/\r?\n/g, "<br/>")
|
||||
return raw.replace(/#/g, '#35;').replace(/"/g, '#quot;').replace(/\r?\n/g, '<br/>')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,12 +122,12 @@ function escapeLabel(raw: string) {
|
||||
* class 行留着给前端 useMermaid 上色;classDef 不写 —— 颜色是渲染那边的事。
|
||||
*/
|
||||
export function flowToMermaid(doc: FlowDoc): string {
|
||||
const lines: string[] = ["flowchart TD"]
|
||||
const lines: string[] = ['flowchart TD']
|
||||
const classes: string[] = []
|
||||
let n = 0
|
||||
const node = (shape: (label: string) => string, text: string, cls: string) => {
|
||||
const id = `n${n++}`
|
||||
lines.push(` ${id}${shape(escapeLabel(text.trim() || " "))}`)
|
||||
lines.push(` ${id}${shape(escapeLabel(text.trim() || ' '))}`)
|
||||
classes.push(` class ${id} ${cls}`)
|
||||
return id
|
||||
}
|
||||
@@ -142,29 +135,23 @@ export function flowToMermaid(doc: FlowDoc): string {
|
||||
type Exit = { from: string; label?: string }
|
||||
const link = (exits: Exit[], to: string) => {
|
||||
for (const exit of exits) {
|
||||
lines.push(
|
||||
exit.label ? ` ${exit.from} -->|"${exit.label}"| ${to}` : ` ${exit.from} --> ${to}`,
|
||||
)
|
||||
lines.push(exit.label ? ` ${exit.from} -->|"${exit.label}"| ${to}` : ` ${exit.from} --> ${to}`)
|
||||
}
|
||||
}
|
||||
const walk = (steps: FlowStep[], entries: Exit[]): Exit[] => {
|
||||
let exits = entries
|
||||
for (const step of steps) {
|
||||
if (step.kind === "if") {
|
||||
const id = node((l) => `{"${l}"}`, step.text, "decision")
|
||||
if (step.kind === 'if') {
|
||||
const id = node((l) => `{"${l}"}`, step.text, 'decision')
|
||||
link(exits, id)
|
||||
exits = [
|
||||
...walk(step.yes, [{ from: id, label: "是" }]),
|
||||
...walk(step.no, [{ from: id, label: "否" }]),
|
||||
]
|
||||
} else if (step.kind === "while") {
|
||||
const id = node((l) => `{"${l}"}`, step.text, "decision")
|
||||
exits = [...walk(step.yes, [{ from: id, label: '是' }]), ...walk(step.no, [{ from: id, label: '否' }])]
|
||||
} else if (step.kind === 'while') {
|
||||
const id = node((l) => `{"${l}"}`, step.text, 'decision')
|
||||
link(exits, id)
|
||||
link(walk(step.body, [{ from: id, label: "是" }]), id)
|
||||
exits = [{ from: id, label: "否" }]
|
||||
link(walk(step.body, [{ from: id, label: '是' }]), id)
|
||||
exits = [{ from: id, label: '否' }]
|
||||
} else {
|
||||
const shape =
|
||||
step.kind === "process" ? (l: string) => `["${l}"]` : (l: string) => `[/"${l}"/]`
|
||||
const shape = step.kind === 'process' ? (l: string) => `["${l}"]` : (l: string) => `[/"${l}"/]`
|
||||
const id = node(shape, step.text, step.kind)
|
||||
link(exits, id)
|
||||
exits = [{ from: id }]
|
||||
@@ -172,9 +159,9 @@ export function flowToMermaid(doc: FlowDoc): string {
|
||||
}
|
||||
return exits
|
||||
}
|
||||
const start = node((l) => `(["${l}"])`, "开始", "startNode")
|
||||
const start = node((l) => `(["${l}"])`, '开始', 'startNode')
|
||||
const exits = walk(doc.steps, [{ from: start }])
|
||||
const end = node((l) => `(["${l}"])`, "结束", "endNode")
|
||||
const end = node((l) => `(["${l}"])`, '结束', 'endNode')
|
||||
link(exits, end)
|
||||
return [...lines, "", ...classes].join("\n")
|
||||
return [...lines, '', ...classes].join('\n')
|
||||
}
|
||||
@@ -1,13 +1,8 @@
|
||||
import { z } from "zod"
|
||||
import { z } from 'zod'
|
||||
|
||||
import { paginatedSchema } from "./common"
|
||||
import { paginatedSchema } from './common'
|
||||
|
||||
export const flowchartStatusSchema = z.union([
|
||||
z.literal(0),
|
||||
z.literal(1),
|
||||
z.literal(2),
|
||||
z.literal(3),
|
||||
])
|
||||
export const flowchartStatusSchema = z.union([z.literal(0), z.literal(1), z.literal(2), z.literal(3)])
|
||||
|
||||
export const createFlowchartRequestSchema = z.object({
|
||||
problemId: z.number().int().positive(),
|
||||
@@ -16,10 +11,7 @@ export const createFlowchartRequestSchema = z.object({
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(50_000)
|
||||
.refine(
|
||||
(value) => value.split("\n").filter((line) => line.trim()).length <= 200,
|
||||
"Flowchart is too complex",
|
||||
),
|
||||
.refine((value) => value.split('\n').filter((line) => line.trim()).length <= 200, 'Flowchart is too complex'),
|
||||
flowchartData: z.record(z.string(), z.unknown()).default({}),
|
||||
})
|
||||
|
||||
@@ -44,10 +36,7 @@ export const flowchartSubmissionSchema = z.object({
|
||||
* 同一个人同一道题的上一次(评完了的)走对了几组,给「比上次多对了 n 组」用。
|
||||
* 上一次不是走例子的评法(2026-10 之前的四项分)、或者没有上一次,给 null
|
||||
*/
|
||||
previousCases: z
|
||||
.object({ passed: z.number().int(), total: z.number().int() })
|
||||
.nullable()
|
||||
.optional(),
|
||||
previousCases: z.object({ passed: z.number().int(), total: z.number().int() }).nullable().optional(),
|
||||
})
|
||||
|
||||
export const flowchartListItemSchema = z.object({
|
||||
@@ -72,16 +61,16 @@ export const flowchartListSchema = paginatedSchema(flowchartListItemSchema).exte
|
||||
})
|
||||
export const createFlowchartResponseSchema = z.object({
|
||||
submissionId: z.string(),
|
||||
status: z.literal("pending"),
|
||||
status: z.literal('pending'),
|
||||
})
|
||||
/**
|
||||
* 流程图画到这两档算这道题做完:题单进度、课堂条、课堂看板、「你画的流程图」、结果页签的 ✓
|
||||
* 都按它(设计文档 2026-09-28-problem-page-redesign 第 3 节决定 4、6)。前后端只有这一份
|
||||
*/
|
||||
export const FLOWCHART_PASS_GRADES = ["S", "A"] as const
|
||||
export const FLOWCHART_PASS_GRADES = ['S', 'A'] as const
|
||||
|
||||
export function isFlowchartPass(grade: string | null | undefined) {
|
||||
return (FLOWCHART_PASS_GRADES as readonly string[]).includes(grade ?? "")
|
||||
return (FLOWCHART_PASS_GRADES as readonly string[]).includes(grade ?? '')
|
||||
}
|
||||
|
||||
/** 自己在一道题上评完的历次分数,早的在前;hidden = 被题单闸门藏起来的次数 */
|
||||
@@ -168,11 +157,7 @@ export interface FlowchartCaseReport {
|
||||
}
|
||||
|
||||
export const flowchartUpdateSchema = z.object({
|
||||
type: z.enum([
|
||||
"flowchart_evaluation_completed",
|
||||
"flowchart_evaluation_failed",
|
||||
"flowchart_evaluation_update",
|
||||
]),
|
||||
type: z.enum(['flowchart_evaluation_completed', 'flowchart_evaluation_failed', 'flowchart_evaluation_update']),
|
||||
submissionId: z.string(),
|
||||
score: z.number().optional(),
|
||||
grade: z.string().optional(),
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
export * from "./admin"
|
||||
export * from "./auth"
|
||||
export * from "./account"
|
||||
export * from "./achievement"
|
||||
export * from "./ai"
|
||||
export * from "./classroom"
|
||||
export * from "./common"
|
||||
export * from "./content"
|
||||
export * from "./contest"
|
||||
export * from "./flowchart"
|
||||
export * from "./flowchart-doc"
|
||||
export * from "./judge-status"
|
||||
export * from "./language"
|
||||
export * from "./problem"
|
||||
export * from "./problemset"
|
||||
export * from "./ranking"
|
||||
export * from "./roles"
|
||||
export * from "./site"
|
||||
export * from "./submission"
|
||||
export * from "./time"
|
||||
export * from './admin'
|
||||
export * from './auth'
|
||||
export * from './account'
|
||||
export * from './achievement'
|
||||
export * from './ai'
|
||||
export * from './classroom'
|
||||
export * from './common'
|
||||
export * from './content'
|
||||
export * from './contest'
|
||||
export * from './flowchart'
|
||||
export * from './flowchart-doc'
|
||||
export * from './judge-status'
|
||||
export * from './language'
|
||||
export * from './problem'
|
||||
export * from './problemset'
|
||||
export * from './ranking'
|
||||
export * from './roles'
|
||||
export * from './site'
|
||||
export * from './submission'
|
||||
export * from './time'
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from "zod"
|
||||
import { z } from 'zod'
|
||||
|
||||
/**
|
||||
* 判题状态码 —— **前后端唯一的一份**。
|
||||
@@ -27,6 +27,4 @@ export type JudgeStatusValue = (typeof JudgeStatus)[keyof typeof JudgeStatus]
|
||||
// 同名的类型:原来契约里就有 `type JudgeStatus`(各处按类型引用),值与类型同名合并
|
||||
export type JudgeStatus = JudgeStatusValue
|
||||
|
||||
export const judgeStatusSchema = z.literal(
|
||||
Object.values(JudgeStatus) as [JudgeStatusValue, ...JudgeStatusValue[]],
|
||||
)
|
||||
export const judgeStatusSchema = z.literal(Object.values(JudgeStatus) as [JudgeStatusValue, ...JudgeStatusValue[]])
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from "zod"
|
||||
import { z } from 'zod'
|
||||
|
||||
/**
|
||||
* 判题沙箱认得的语言。**这是全仓唯一的语言集合定义处。**
|
||||
@@ -22,7 +22,7 @@ import { z } from "zod"
|
||||
* 从别处冒出来(旧客户端的 localStorage、迁移之前排进队列的任务),走下面的
|
||||
* `normalizeLanguage()`,别直接 parse。
|
||||
*/
|
||||
export const judgeLanguageSchema = z.enum(["C", "C++", "Python", "Java", "JavaScript", "Golang"])
|
||||
export const judgeLanguageSchema = z.enum(['C', 'C++', 'Python', 'Java', 'JavaScript', 'Golang'])
|
||||
|
||||
/**
|
||||
* 题目可以挂的语言 = 沙箱语言 + 两种非沙箱题型。
|
||||
@@ -35,26 +35,26 @@ export const judgeLanguageSchema = z.enum(["C", "C++", "Python", "Java", "JavaSc
|
||||
* 于是那 91 条提交的 `language` 在类型上是 `undefined` —— 这就是两份真相
|
||||
* 各自演进的代价,现在并成一份。
|
||||
*/
|
||||
export const problemLanguageSchema = z.enum([...judgeLanguageSchema.options, "SQL", "Flowchart"])
|
||||
export const problemLanguageSchema = z.enum([...judgeLanguageSchema.options, 'SQL', 'Flowchart'])
|
||||
|
||||
/**
|
||||
* 判题沙箱**现在**跑得了的语言,就是 `apps/api/src/judge/languages.ts` 里那三块。
|
||||
* 试运行(`POST /trial-runs`)只收这几个:历史语言交上来只会在判题机那边报系统错误。
|
||||
*/
|
||||
export const runnableLanguageSchema = judgeLanguageSchema.extract(["C", "C++", "Python"])
|
||||
export const runnableLanguageSchema = judgeLanguageSchema.extract(['C', 'C++', 'Python'])
|
||||
|
||||
/**
|
||||
* 老师布置作业能选的语言:判题机跑得了的三种,加 SQL(数据库课一整节都是 SQL 题)。
|
||||
* SQL 题只收 SQL,所以 C / Python 的作业里夹 SQL 题也行 —— 那几道不受作业语言限制
|
||||
*/
|
||||
export const lessonLanguageSchema = problemLanguageSchema.extract(["Python", "C", "C++", "SQL"])
|
||||
export const lessonLanguageSchema = problemLanguageSchema.extract(['Python', 'C', 'C++', 'SQL'])
|
||||
|
||||
/**
|
||||
* SQL 题:可选语言里有 SQL(后端保存时强校验 SQL 只能单独出现)。题型只认这一条,
|
||||
* 前后端一律走它,别再各自看 sqlConfig 或 languages
|
||||
*/
|
||||
export function isSqlProblem(problem: { languages: readonly string[] }) {
|
||||
return problem.languages.includes("SQL")
|
||||
return problem.languages.includes('SQL')
|
||||
}
|
||||
|
||||
export type JudgeLanguage = z.infer<typeof judgeLanguageSchema>
|
||||
@@ -72,13 +72,13 @@ export type ProblemLanguage = z.infer<typeof problemLanguageSchema>
|
||||
* 别只滚代码。
|
||||
*/
|
||||
const LANGUAGE_ALIASES: Record<string, ProblemLanguage> = {
|
||||
Python2: "Python",
|
||||
Python3: "Python",
|
||||
Python2: 'Python',
|
||||
Python3: 'Python',
|
||||
}
|
||||
|
||||
/** 把可能是旧值的语言名归一化;认不出来返回 null,由调用方决定怎么兜底。 */
|
||||
export function normalizeLanguage(value: unknown): ProblemLanguage | null {
|
||||
if (typeof value !== "string") return null
|
||||
if (typeof value !== 'string') return null
|
||||
const parsed = problemLanguageSchema.safeParse(LANGUAGE_ALIASES[value] ?? value)
|
||||
return parsed.success ? parsed.data : null
|
||||
}
|
||||
+117
-140
@@ -1,16 +1,16 @@
|
||||
import { z } from "zod"
|
||||
import { z } from 'zod'
|
||||
|
||||
import { achievementRaritySchema } from "./achievement"
|
||||
import { paginatedSchema, sampleUserSchema } from "./common"
|
||||
import { judgeStatusSchema } from "./judge-status"
|
||||
import { problemLanguageSchema } from "./language"
|
||||
import { achievementRaritySchema } from './achievement'
|
||||
import { paginatedSchema, sampleUserSchema } from './common'
|
||||
import { judgeStatusSchema } from './judge-status'
|
||||
import { problemLanguageSchema } from './language'
|
||||
|
||||
/**
|
||||
* 题目难度。生产库 956 道题只有这三个值(旧 Django 的 Problem.difficulty choices
|
||||
* 也是这三个),前端的 DIFFICULTY 映射表按它建 —— 写成 z.string() 的话
|
||||
* 多出来的值会静默渲染成 undefined。
|
||||
*/
|
||||
export const problemDifficultySchema = z.enum(["Low", "Mid", "High"])
|
||||
export const problemDifficultySchema = z.enum(['Low', 'Mid', 'High'])
|
||||
|
||||
/**
|
||||
* 题目详情/列表里的难度。**可为 null** —— 比赛进行中、看的人又不是管理员时,
|
||||
@@ -31,7 +31,7 @@ export const maskedProblemDifficultySchema = problemDifficultySchema.nullable()
|
||||
* SQLDisplay 接口才能渲染表格,抄错了没人拦得住。
|
||||
*/
|
||||
export const sqlConfigSchema = z.object({
|
||||
mode: z.enum(["query", "modify"]),
|
||||
mode: z.enum(['query', 'modify']),
|
||||
order_sensitive: z.boolean(),
|
||||
})
|
||||
|
||||
@@ -61,10 +61,7 @@ export const sqlDisplaySchema = z.object({
|
||||
tables: z.array(sqlDisplayTableSchema),
|
||||
// query 题给结果集,modify 题给改动后的表 —— 两种形态,前端按有没有
|
||||
// changed_tables 分支
|
||||
expected: z.union([
|
||||
sqlResultSetSchema,
|
||||
z.object({ changed_tables: z.array(sqlDisplayTableSchema) }),
|
||||
]),
|
||||
expected: z.union([sqlResultSetSchema, z.object({ changed_tables: z.array(sqlDisplayTableSchema) })]),
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -78,17 +75,17 @@ export const sqlDisplaySchema = z.object({
|
||||
* (见 ast.ts 的 evaluateRule),缺了就走默认文案。
|
||||
*/
|
||||
export const astRuleEngineSchema = z.enum([
|
||||
"must_exist_node",
|
||||
"must_not_exist_node",
|
||||
"count_node",
|
||||
"must_call_function",
|
||||
"must_not_call_function",
|
||||
"count_function_call",
|
||||
"must_call_method",
|
||||
"must_not_call_method",
|
||||
"must_use_operator",
|
||||
'must_exist_node',
|
||||
'must_not_exist_node',
|
||||
'count_node',
|
||||
'must_call_function',
|
||||
'must_not_call_function',
|
||||
'count_function_call',
|
||||
'must_call_method',
|
||||
'must_not_call_method',
|
||||
'must_use_operator',
|
||||
// 判题机实现了,但后台编辑器还没有对应的选项,目前只能手工造数据用上
|
||||
"must_have_nesting",
|
||||
'must_have_nesting',
|
||||
])
|
||||
|
||||
export const astRuleSchema = z.object({
|
||||
@@ -134,20 +131,20 @@ export interface AstNodeTarget {
|
||||
}
|
||||
|
||||
const C_NODE_TARGETS = {
|
||||
for_loop: { label: "for 循环", node: "for_statement" },
|
||||
while_loop: { label: "while 循环", node: "while_statement" },
|
||||
do_while: { label: "do-while 循环", node: "do_statement" },
|
||||
if_statement: { label: "if 条件", node: "if_statement" },
|
||||
else_clause: { label: "else 子句", node: "else_clause" },
|
||||
switch_statement: { label: "switch 语句", node: "switch_statement" },
|
||||
case_statement: { label: "case 分支", node: "case_statement" },
|
||||
break: { label: "break 语句", node: "break_statement" },
|
||||
continue: { label: "continue 语句", node: "continue_statement" },
|
||||
return: { label: "return 语句", node: "return_statement" },
|
||||
function_definition: { label: "函数定义", node: "function_definition" },
|
||||
assignment: { label: "赋值语句", node: "assignment_expression" },
|
||||
struct: { label: "结构体", node: "struct_specifier" },
|
||||
include: { label: "#include 指令", node: "preproc_include" },
|
||||
for_loop: { label: 'for 循环', node: 'for_statement' },
|
||||
while_loop: { label: 'while 循环', node: 'while_statement' },
|
||||
do_while: { label: 'do-while 循环', node: 'do_statement' },
|
||||
if_statement: { label: 'if 条件', node: 'if_statement' },
|
||||
else_clause: { label: 'else 子句', node: 'else_clause' },
|
||||
switch_statement: { label: 'switch 语句', node: 'switch_statement' },
|
||||
case_statement: { label: 'case 分支', node: 'case_statement' },
|
||||
break: { label: 'break 语句', node: 'break_statement' },
|
||||
continue: { label: 'continue 语句', node: 'continue_statement' },
|
||||
return: { label: 'return 语句', node: 'return_statement' },
|
||||
function_definition: { label: '函数定义', node: 'function_definition' },
|
||||
assignment: { label: '赋值语句', node: 'assignment_expression' },
|
||||
struct: { label: '结构体', node: 'struct_specifier' },
|
||||
include: { label: '#include 指令', node: 'preproc_include' },
|
||||
} satisfies Record<string, AstNodeTarget>
|
||||
|
||||
export const AST_NODE_TARGETS_BY_LANGUAGE: Record<string, Record<string, AstNodeTarget>> = {
|
||||
@@ -158,9 +155,9 @@ export const AST_NODE_TARGETS_BY_LANGUAGE: Record<string, Record<string, AstNode
|
||||
* 契约和判题机两个文件里各抄了两遍(C 一份、C++ 一份),同一份数据四份拷贝。
|
||||
* 这里逐条列出来是为了保住下拉框的显示顺序(C++ 独有的几条是插在中间的)。
|
||||
*/
|
||||
"C++": {
|
||||
'C++': {
|
||||
for_loop: C_NODE_TARGETS.for_loop,
|
||||
range_for_loop: { label: "范围 for 循环", node: "for_range_loop" },
|
||||
range_for_loop: { label: '范围 for 循环', node: 'for_range_loop' },
|
||||
while_loop: C_NODE_TARGETS.while_loop,
|
||||
do_while: C_NODE_TARGETS.do_while,
|
||||
if_statement: C_NODE_TARGETS.if_statement,
|
||||
@@ -171,37 +168,37 @@ export const AST_NODE_TARGETS_BY_LANGUAGE: Record<string, Record<string, AstNode
|
||||
continue: C_NODE_TARGETS.continue,
|
||||
return: C_NODE_TARGETS.return,
|
||||
function_definition: C_NODE_TARGETS.function_definition,
|
||||
class_definition: { label: "类定义", node: "class_specifier" },
|
||||
class_definition: { label: '类定义', node: 'class_specifier' },
|
||||
struct: C_NODE_TARGETS.struct,
|
||||
assignment: C_NODE_TARGETS.assignment,
|
||||
include: C_NODE_TARGETS.include,
|
||||
try_except: { label: "try-catch", node: "try_statement" },
|
||||
throw: { label: "throw 语句", node: "throw_statement" },
|
||||
namespace: { label: "namespace 定义", node: "namespace_definition" },
|
||||
template: { label: "模板定义", node: "template_declaration" },
|
||||
lambda: { label: "lambda 表达式", node: "lambda_expression" },
|
||||
using: { label: "using 声明", node: "using_declaration" },
|
||||
try_except: { label: 'try-catch', node: 'try_statement' },
|
||||
throw: { label: 'throw 语句', node: 'throw_statement' },
|
||||
namespace: { label: 'namespace 定义', node: 'namespace_definition' },
|
||||
template: { label: '模板定义', node: 'template_declaration' },
|
||||
lambda: { label: 'lambda 表达式', node: 'lambda_expression' },
|
||||
using: { label: 'using 声明', node: 'using_declaration' },
|
||||
},
|
||||
Python: {
|
||||
for_loop: { label: "for 循环", node: "for_statement" },
|
||||
while_loop: { label: "while 循环", node: "while_statement" },
|
||||
if_statement: { label: "if 条件", node: "if_statement" },
|
||||
elif_clause: { label: "elif 子句", node: "elif_clause" },
|
||||
else_clause: { label: "else 子句", node: "else_clause" },
|
||||
break: { label: "break 语句", node: "break_statement" },
|
||||
continue: { label: "continue 语句", node: "continue_statement" },
|
||||
return: { label: "return 语句", node: "return_statement" },
|
||||
function_definition: { label: "函数定义", node: "function_definition" },
|
||||
class_definition: { label: "类定义", node: "class_definition" },
|
||||
assignment: { label: "赋值语句", node: "assignment" },
|
||||
try_except: { label: "try-except", node: "try_statement" },
|
||||
with_statement: { label: "with 语句", node: "with_statement" },
|
||||
import: { label: "import 语句", node: "import_statement" },
|
||||
import_from: { label: "from-import 语句", node: "import_from_statement" },
|
||||
list_comprehension: { label: "列表推导式", node: "list_comprehension" },
|
||||
list_literal: { label: "列表", node: "list" },
|
||||
dict_literal: { label: "字典", node: "dictionary" },
|
||||
set_literal: { label: "集合", node: "set" },
|
||||
for_loop: { label: 'for 循环', node: 'for_statement' },
|
||||
while_loop: { label: 'while 循环', node: 'while_statement' },
|
||||
if_statement: { label: 'if 条件', node: 'if_statement' },
|
||||
elif_clause: { label: 'elif 子句', node: 'elif_clause' },
|
||||
else_clause: { label: 'else 子句', node: 'else_clause' },
|
||||
break: { label: 'break 语句', node: 'break_statement' },
|
||||
continue: { label: 'continue 语句', node: 'continue_statement' },
|
||||
return: { label: 'return 语句', node: 'return_statement' },
|
||||
function_definition: { label: '函数定义', node: 'function_definition' },
|
||||
class_definition: { label: '类定义', node: 'class_definition' },
|
||||
assignment: { label: '赋值语句', node: 'assignment' },
|
||||
try_except: { label: 'try-except', node: 'try_statement' },
|
||||
with_statement: { label: 'with 语句', node: 'with_statement' },
|
||||
import: { label: 'import 语句', node: 'import_statement' },
|
||||
import_from: { label: 'from-import 语句', node: 'import_from_statement' },
|
||||
list_comprehension: { label: '列表推导式', node: 'list_comprehension' },
|
||||
list_literal: { label: '列表', node: 'list' },
|
||||
dict_literal: { label: '字典', node: 'dictionary' },
|
||||
set_literal: { label: '集合', node: 'set' },
|
||||
/**
|
||||
* f-string 认的是 `interpolation`(`f"{x}"` 里的 `{x}`),不是 `format_string` ——
|
||||
* 这个版本的 tree-sitter-python 里**没有** format_string 这种节点,f-string 是
|
||||
@@ -213,7 +210,7 @@ export const AST_NODE_TARGETS_BY_LANGUAGE: Record<string, Record<string, AstNode
|
||||
* 这已经是严格的改善。实测 `%` 格式化、`.format()`、普通字符串、字符串拼接
|
||||
* 都不会误伤。
|
||||
*/
|
||||
f_string: { label: "f-string", node: "interpolation" },
|
||||
f_string: { label: 'f-string', node: 'interpolation' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -227,59 +224,59 @@ export const AST_NODE_TARGETS_BY_LANGUAGE: Record<string, Record<string, AstNode
|
||||
* 恒等的条目(`+`、`==` …)写全是因为这张表同时是后台下拉的选项来源。
|
||||
*/
|
||||
const C_OPERATOR_TARGETS = {
|
||||
"+": "+",
|
||||
"-": "-",
|
||||
"*": "*",
|
||||
"/": "/",
|
||||
"%": "%",
|
||||
"+=": "+=",
|
||||
"-=": "-=",
|
||||
"*=": "*=",
|
||||
"/=": "/=",
|
||||
"%=": "%=",
|
||||
"++": "++",
|
||||
"--": "--",
|
||||
"==": "==",
|
||||
"!=": "!=",
|
||||
">": ">",
|
||||
">=": ">=",
|
||||
"<": "<",
|
||||
"<=": "<=",
|
||||
and: "&&",
|
||||
or: "||",
|
||||
not: "!",
|
||||
"&": "&",
|
||||
"|": "|",
|
||||
'+': '+',
|
||||
'-': '-',
|
||||
'*': '*',
|
||||
'/': '/',
|
||||
'%': '%',
|
||||
'+=': '+=',
|
||||
'-=': '-=',
|
||||
'*=': '*=',
|
||||
'/=': '/=',
|
||||
'%=': '%=',
|
||||
'++': '++',
|
||||
'--': '--',
|
||||
'==': '==',
|
||||
'!=': '!=',
|
||||
'>': '>',
|
||||
'>=': '>=',
|
||||
'<': '<',
|
||||
'<=': '<=',
|
||||
and: '&&',
|
||||
or: '||',
|
||||
not: '!',
|
||||
'&': '&',
|
||||
'|': '|',
|
||||
}
|
||||
|
||||
export const AST_OPERATOR_TARGETS_BY_LANGUAGE: Record<string, Record<string, string>> = {
|
||||
C: C_OPERATOR_TARGETS,
|
||||
// `<<` / `>>` 对 C++ 主要是 cout/cin 的流运算符(位移是同一个 token)
|
||||
"C++": { ...C_OPERATOR_TARGETS, "<<": "<<", ">>": ">>" },
|
||||
'C++': { ...C_OPERATOR_TARGETS, '<<': '<<', '>>': '>>' },
|
||||
Python: {
|
||||
"+": "+",
|
||||
"-": "-",
|
||||
"*": "*",
|
||||
"/": "/",
|
||||
"//": "//",
|
||||
"%": "%",
|
||||
"**": "**",
|
||||
"+=": "+=",
|
||||
"-=": "-=",
|
||||
"*=": "*=",
|
||||
"/=": "/=",
|
||||
"%=": "%=",
|
||||
"==": "==",
|
||||
"!=": "!=",
|
||||
">": ">",
|
||||
">=": ">=",
|
||||
"<": "<",
|
||||
"<=": "<=",
|
||||
and: "and",
|
||||
or: "or",
|
||||
not: "not",
|
||||
"&": "&",
|
||||
"|": "|",
|
||||
'+': '+',
|
||||
'-': '-',
|
||||
'*': '*',
|
||||
'/': '/',
|
||||
'//': '//',
|
||||
'%': '%',
|
||||
'**': '**',
|
||||
'+=': '+=',
|
||||
'-=': '-=',
|
||||
'*=': '*=',
|
||||
'/=': '/=',
|
||||
'%=': '%=',
|
||||
'==': '==',
|
||||
'!=': '!=',
|
||||
'>': '>',
|
||||
'>=': '>=',
|
||||
'<': '<',
|
||||
'<=': '<=',
|
||||
and: 'and',
|
||||
or: 'or',
|
||||
not: 'not',
|
||||
'&': '&',
|
||||
'|': '|',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -292,9 +289,7 @@ export const AST_SUPPORTED_LANGUAGES = Object.keys(AST_NODE_TARGETS_BY_LANGUAGE)
|
||||
|
||||
/** 全语言的节点中文名并集,只给拿不到语言的场合做回落。有语言一律走 astNodeLabel() */
|
||||
export const AST_NODE_TARGET_LABELS: Record<string, string> = Object.fromEntries(
|
||||
Object.values(AST_NODE_TARGETS_BY_LANGUAGE).flatMap((table) =>
|
||||
Object.entries(table).map(([target, entry]) => [target, entry.label]),
|
||||
),
|
||||
Object.values(AST_NODE_TARGETS_BY_LANGUAGE).flatMap((table) => Object.entries(table).map(([target, entry]) => [target, entry.label])),
|
||||
)
|
||||
|
||||
export function astNodeLabel(target: string, language?: string): string {
|
||||
@@ -308,11 +303,7 @@ export function astNodeLabel(target: string, language?: string): string {
|
||||
* 都对不上就回落到裸 target(恒等的运算符 `+` / `==` 走的就是这条)。
|
||||
*/
|
||||
export function astTargetNodeType(target: string, language: string): string {
|
||||
return (
|
||||
AST_NODE_TARGETS_BY_LANGUAGE[language]?.[target]?.node ??
|
||||
AST_OPERATOR_TARGETS_BY_LANGUAGE[language]?.[target] ??
|
||||
target
|
||||
)
|
||||
return AST_NODE_TARGETS_BY_LANGUAGE[language]?.[target]?.node ?? AST_OPERATOR_TARGETS_BY_LANGUAGE[language]?.[target] ?? target
|
||||
}
|
||||
|
||||
export function astOperatorLabel(target: string, language?: string): string {
|
||||
@@ -327,13 +318,8 @@ export function astOperatorLabel(target: string, language?: string): string {
|
||||
* 读取路径(判题、题目页要求)用它把这种规则整条丢掉,而不是让 schema 校验失败:
|
||||
* astRulesSchema 同时用于**读**后台题目详情,在那儿抛错会让整个详情打不开。
|
||||
*/
|
||||
export function astRuleIsMeaningful(rule: {
|
||||
engine: string
|
||||
exact?: number
|
||||
min?: number
|
||||
max?: number
|
||||
}): boolean {
|
||||
if (!rule.engine.startsWith("count")) return true
|
||||
export function astRuleIsMeaningful(rule: { engine: string; exact?: number; min?: number; max?: number }): boolean {
|
||||
if (!rule.engine.startsWith('count')) return true
|
||||
return rule.exact !== undefined || rule.min !== undefined || rule.max !== undefined
|
||||
}
|
||||
|
||||
@@ -349,7 +335,7 @@ export const astRequirementSchema = z.object({
|
||||
/** 已经渲染好的中文文案,例如「if 条件 出现 2 次」 */
|
||||
description: z.string(),
|
||||
/** 标签配色:必须做 / 不能做 / 次数约束 */
|
||||
kind: z.enum(["require", "forbid", "count"]),
|
||||
kind: z.enum(['require', 'forbid', 'count']),
|
||||
})
|
||||
|
||||
/** 按语言分组,与 astRulesSchema 同一套键 */
|
||||
@@ -495,20 +481,13 @@ export const problemListItemSchema = z.object({
|
||||
export const problemListSchema = paginatedSchema(problemListItemSchema)
|
||||
|
||||
/** 题目列表「全部类型」下拉:SQL / 有参考图 / 画流程图 / 语法要求 */
|
||||
export const problemTypeFilterSchema = z.enum(["sql", "reference", "flowchart", "ast"])
|
||||
export const problemTypeFilterSchema = z.enum(['sql', 'reference', 'flowchart', 'ast'])
|
||||
|
||||
/**
|
||||
* 题目列表的排序。值沿用原来的(旧链接照样能用):空串 = 最新创建、create_time = 最早创建,
|
||||
* ±submission_number / ±accepted_number 是做过 / 做对的**人数**,带 - 是从多到少
|
||||
*/
|
||||
export const problemListSortSchema = z.enum([
|
||||
"",
|
||||
"create_time",
|
||||
"-submission_number",
|
||||
"submission_number",
|
||||
"-accepted_number",
|
||||
"accepted_number",
|
||||
])
|
||||
export const problemListSortSchema = z.enum(['', 'create_time', '-submission_number', 'submission_number', '-accepted_number', 'accepted_number'])
|
||||
|
||||
/**
|
||||
* 题目列表顶上那行和左栏的进度。**只说做到了什么**:做对几道、这周新做对几道、
|
||||
@@ -543,7 +522,7 @@ export const problemProgressSchema = z.object({
|
||||
* 题材(LOL、周杰伦、哈利波特……)。两类原来混在一起,题目列表上四十多个标签
|
||||
* 挤成一片,学生想按知识点找题得在一堆游戏名里挑。新标签默认算知识点。
|
||||
*/
|
||||
export const tagCategorySchema = z.enum(["knowledge", "theme"])
|
||||
export const tagCategorySchema = z.enum(['knowledge', 'theme'])
|
||||
|
||||
export const tagSchema = z.object({
|
||||
id: z.number().int(),
|
||||
@@ -577,9 +556,7 @@ export const problemStatsSchema = z.object({
|
||||
/** 我自己:交了几次、做对没有。没登录为 null */
|
||||
me: z.object({ attempts: z.number().int(), solved: z.boolean() }).nullable(),
|
||||
/** 学生自己班这题 x / y 人做对(y 是班里交过的人)。没班级、比赛里为 null */
|
||||
myClass: z
|
||||
.object({ className: z.string(), tried: z.number().int(), solved: z.number().int() })
|
||||
.nullable(),
|
||||
myClass: z.object({ className: z.string(), tried: z.number().int(), solved: z.number().int() }).nullable(),
|
||||
/**
|
||||
* 老师:各班做这道题的情况,和数据统计「按班级汇总」**同一个口径**(只算学生的提交;
|
||||
* 做完 / 班级人数、交了没对、没交、正确率、最近一次),交过的人多的在前。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from "zod"
|
||||
import { z } from 'zod'
|
||||
|
||||
import { paginatedSchema, sampleUserSchema } from "./common"
|
||||
import { maskedProblemDifficultySchema } from "./problem"
|
||||
import { paginatedSchema, sampleUserSchema } from './common'
|
||||
import { maskedProblemDifficultySchema } from './problem'
|
||||
|
||||
/**
|
||||
* 我在这份题单里的进度。
|
||||
@@ -22,7 +22,7 @@ export const problemSetUserProgressSummarySchema = z.object({
|
||||
})
|
||||
|
||||
/** 奖章只有两种条件:全部做完、做对 N 道(「总分达到 N」随分数一起拿掉了,0026 换算过) */
|
||||
export const badgeConditionTypeSchema = z.enum(["all_problems", "problem_count"])
|
||||
export const badgeConditionTypeSchema = z.enum(['all_problems', 'problem_count'])
|
||||
|
||||
export const problemSetBadgeSchema = z.object({
|
||||
id: z.number().int(),
|
||||
@@ -117,9 +117,7 @@ export const problemSetClassStudentSchema = z.object({
|
||||
|
||||
export const problemSetClassViewSchema = z.object({
|
||||
/** 加入过这份题单的学生分属哪些班:三分之一以上的人加入了的班在前(按最近一次加入),零星几个人的在后 */
|
||||
classes: z.array(
|
||||
z.object({ className: z.string(), joined: z.number().int(), size: z.number().int() }),
|
||||
),
|
||||
classes: z.array(z.object({ className: z.string(), joined: z.number().int(), size: z.number().int() })),
|
||||
/** 这次看的是哪个班;谁都没加入时为 null */
|
||||
className: z.string().nullable(),
|
||||
problems: z.array(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod"
|
||||
import { z } from 'zod'
|
||||
|
||||
import { sampleUserSchema } from "./common"
|
||||
import { sampleUserSchema } from './common'
|
||||
|
||||
/**
|
||||
* 排名页(设计稿「排名重设计」G1–G3)。用户要的是**激发竞争、一眼看出排名**,所以:
|
||||
@@ -10,8 +10,8 @@ import { sampleUserSchema } from "./common"
|
||||
* 按先后排,名次差的是「这节课谁先做完」。
|
||||
* - 「做对」= 这道题**第一次**做对落在这段时间里(比赛里的提交不算),老题重交不算成绩。
|
||||
*/
|
||||
export const rankScopeSchema = z.enum(["class", "grade", "all"])
|
||||
export const rankPeriodSchema = z.enum(["week", "term", "all"])
|
||||
export const rankScopeSchema = z.enum(['class', 'grade', 'all'])
|
||||
export const rankPeriodSchema = z.enum(['week', 'term', 'all'])
|
||||
|
||||
export const rankRowSchema = z.object({
|
||||
rank: z.number().int().positive(),
|
||||
@@ -147,9 +147,7 @@ export const classDetailSchema = z.object({
|
||||
/** 每个同学这学期做对几道,画分布点阵用 */
|
||||
distribution: z.array(z.number().int().nonnegative()),
|
||||
/** 这学期每周(周一起)人均新做对,和同年级在用的班平均比 */
|
||||
weeks: z.array(
|
||||
z.object({ weekStart: z.string(), perCapita: z.number(), gradeAvg: z.number().nullable() }),
|
||||
),
|
||||
weeks: z.array(z.object({ weekStart: z.string(), perCapita: z.number(), gradeAvg: z.number().nullable() })),
|
||||
/** 要多关心的同学(这学期做对不到 3 道),只给老师;学生拿到的是 null */
|
||||
care: z
|
||||
.array(
|
||||
@@ -175,7 +173,7 @@ export type ClassDetail = z.infer<typeof classDetailSchema>
|
||||
* 不然 37/39 和 38/40 都显示 95%,皇冠却只给一边。口径和排名页一样:比赛里的提交不算,
|
||||
* 不计入排名的人分子分母都不算。
|
||||
*/
|
||||
export const classPkPeriodSchema = z.enum(["week", "term"])
|
||||
export const classPkPeriodSchema = z.enum(['week', 'term'])
|
||||
|
||||
export const classPkCellSchema = z.object({
|
||||
/** 这段时间交过这道题的人 */
|
||||
|
||||
@@ -12,15 +12,10 @@
|
||||
* **这个文件不能 import zod。** 前端入口要用这里的 USER_TYPE / toAdminType,
|
||||
* 一 import 就把整个 zod 运行时(~100KB)拖进首屏包。对应的 zod schema 在 admin.ts。
|
||||
*/
|
||||
export const ADMIN_TYPES = [
|
||||
"Regular User",
|
||||
"Student Admin",
|
||||
"Teacher Admin",
|
||||
"Super Admin",
|
||||
] as const
|
||||
export const ADMIN_TYPES = ['Regular User', 'Student Admin', 'Teacher Admin', 'Super Admin'] as const
|
||||
export type AdminType = (typeof ADMIN_TYPES)[number]
|
||||
|
||||
export const PROBLEM_PERMISSIONS = ["None", "Own", "All"] as const
|
||||
export const PROBLEM_PERMISSIONS = ['None', 'Own', 'All'] as const
|
||||
export type ProblemPermission = (typeof PROBLEM_PERMISSIONS)[number]
|
||||
|
||||
/**
|
||||
@@ -32,29 +27,27 @@ export type ProblemPermission = (typeof PROBLEM_PERMISSIONS)[number]
|
||||
*/
|
||||
|
||||
/** 能进后台的三种 */
|
||||
export const ADMIN_ROLES: readonly AdminType[] = ["Student Admin", "Teacher Admin", "Super Admin"]
|
||||
export const ADMIN_ROLES: readonly AdminType[] = ['Student Admin', 'Teacher Admin', 'Super Admin']
|
||||
|
||||
/** 老师及以上 */
|
||||
export const TEACHER_ROLES: readonly AdminType[] = ["Teacher Admin", "Super Admin"]
|
||||
export const TEACHER_ROLES: readonly AdminType[] = ['Teacher Admin', 'Super Admin']
|
||||
|
||||
/**
|
||||
* 学生口径:排行榜、班级榜、比赛榜、自学统计都只算这两种,教师和超管不入榜。
|
||||
* 注意 Student Admin 算学生 —— 他要参赛、要上榜,只是多了个后台入口。
|
||||
*/
|
||||
export const STUDENT_ROLES: readonly AdminType[] = ["Regular User", "Student Admin"]
|
||||
export const STUDENT_ROLES: readonly AdminType[] = ['Regular User', 'Student Admin']
|
||||
|
||||
/**
|
||||
* 把库里读出来的裸字符串收成联合类型。**认不出来的一律降成最低权限**,
|
||||
* 不抛错:脏数据不该让人登不上,但更不该让人凭一个拼错的角色名拿到权限。
|
||||
*/
|
||||
export function toAdminType(value: string): AdminType {
|
||||
return (ADMIN_TYPES as readonly string[]).includes(value) ? (value as AdminType) : "Regular User"
|
||||
return (ADMIN_TYPES as readonly string[]).includes(value) ? (value as AdminType) : 'Regular User'
|
||||
}
|
||||
|
||||
export function toProblemPermission(value: string): ProblemPermission {
|
||||
return (PROBLEM_PERMISSIONS as readonly string[]).includes(value)
|
||||
? (value as ProblemPermission)
|
||||
: "None"
|
||||
return (PROBLEM_PERMISSIONS as readonly string[]).includes(value) ? (value as ProblemPermission) : 'None'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,14 +58,14 @@ export function toProblemPermission(value: string): ProblemPermission {
|
||||
* 原先 `apps/web/src/utils/constants.ts` 里另抄了一份,那是这四个字符串的第三份副本。
|
||||
*/
|
||||
export const USER_TYPE = {
|
||||
REGULAR_USER: "Regular User",
|
||||
STUDENT_ADMIN: "Student Admin",
|
||||
TEACHER_ADMIN: "Teacher Admin",
|
||||
SUPER_ADMIN: "Super Admin",
|
||||
REGULAR_USER: 'Regular User',
|
||||
STUDENT_ADMIN: 'Student Admin',
|
||||
TEACHER_ADMIN: 'Teacher Admin',
|
||||
SUPER_ADMIN: 'Super Admin',
|
||||
} as const satisfies Record<string, AdminType>
|
||||
|
||||
export const PROBLEM_PERMISSION = {
|
||||
NONE: "None",
|
||||
OWN: "Own",
|
||||
ALL: "All",
|
||||
NONE: 'None',
|
||||
OWN: 'Own',
|
||||
ALL: 'All',
|
||||
} as const satisfies Record<string, ProblemPermission>
|
||||
@@ -1,4 +1,4 @@
|
||||
import { z } from "zod"
|
||||
import { z } from 'zod'
|
||||
|
||||
/**
|
||||
* 站点配置。网址、简称、页脚三项 2026-10-13 删了(前后端从来没读过,改了也看不到变化),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { z } from "zod"
|
||||
import { z } from 'zod'
|
||||
|
||||
import { paginatedSchema } from "./common"
|
||||
import { judgeStatusSchema, type JudgeStatus } from "./judge-status"
|
||||
import { problemLanguageSchema, runnableLanguageSchema } from "./language"
|
||||
import { paginatedSchema } from './common'
|
||||
import { judgeStatusSchema, type JudgeStatus } from './judge-status'
|
||||
import { problemLanguageSchema, runnableLanguageSchema } from './language'
|
||||
|
||||
/**
|
||||
* 判题机原始输出(`submission.info` 的 JSONB 原文)。**只是类型,不作运行时校验。**
|
||||
@@ -285,10 +285,10 @@ export const submissionDetailSchema = z.object({
|
||||
* 拉一次就是了,不必让推送顺带背一份 JSONB 的形状。
|
||||
*/
|
||||
export const submissionUpdateSchema = z.object({
|
||||
type: z.literal("submission_update"),
|
||||
type: z.literal('submission_update'),
|
||||
submissionId: z.string(),
|
||||
result: judgeStatusSchema,
|
||||
status: z.enum(["pending", "judging", "finished", "error"]),
|
||||
status: z.enum(['pending', 'judging', 'finished', 'error']),
|
||||
score: z.number().optional(),
|
||||
})
|
||||
|
||||
@@ -439,7 +439,7 @@ export const submissionStatisticsSchema = z.object({
|
||||
* 这批号(夏令营、兴趣班、自己注册的)`user.class_name` 是 null,当成一个班来看。
|
||||
* 真班名全是数字(`253`、`2511`),撞不上
|
||||
*/
|
||||
export const NO_CLASS = "none"
|
||||
export const NO_CLASS = 'none'
|
||||
|
||||
/**
|
||||
* 统计「一行一节课」的总览(GET /submissions/statistics/lessons,设计稿「统计合二为一」B)。
|
||||
@@ -564,14 +564,14 @@ export const todaySubmissionStatisticsSchema = z.object({
|
||||
userCount: z.number().int(),
|
||||
acceptedUsers: z.number().int(),
|
||||
/** 我自己今天在这道题上:做对了 / 交了没对 / 没交;没登录为 null */
|
||||
mine: z.enum(["accepted", "tried", "none"]).nullable(),
|
||||
mine: z.enum(['accepted', 'tried', 'none']).nullable(),
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
export const formatCodeRequestSchema = z.object({
|
||||
code: z.string().max(1024 * 1024),
|
||||
language: z.enum(["python", "c", "cpp", "sql"]),
|
||||
language: z.enum(['python', 'c', 'cpp', 'sql']),
|
||||
/** C / C++ 格式化前要先编一遍,得套上这道题的模板才编得过 */
|
||||
problemId: z.number().int().optional(),
|
||||
})
|
||||
@@ -627,15 +627,15 @@ export const trialRunRequestSchema = z.object({
|
||||
*/
|
||||
export type TrialRunResponse =
|
||||
| {
|
||||
status: "done"
|
||||
status: 'done'
|
||||
cases: {
|
||||
result: JudgeStatus
|
||||
output: string
|
||||
runtimeError?: NonNullable<StatisticInfo["runtime_error"]>
|
||||
runtimeError?: NonNullable<StatisticInfo['runtime_error']>
|
||||
}[]
|
||||
}
|
||||
| { status: "compile-error"; message: string }
|
||||
| { status: "too-much-output" }
|
||||
| { status: 'compile-error'; message: string }
|
||||
| { status: 'too-much-output' }
|
||||
|
||||
export type StatisticInfo = z.infer<typeof statisticInfoSchema>
|
||||
export type AstResultInfo = z.infer<typeof astResultSchema>
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
* 两边都按**固定偏移**算、不查 tzdata:中国大陆 1991 年起没有夏令时,东八区恒为 UTC+8,
|
||||
* 这样进程 TZ、浏览器时区、镜像里有没有 tzdata 都不影响结果。换时区时两个常量一起改。
|
||||
*/
|
||||
export const TIME_ZONE = "Asia/Shanghai"
|
||||
export const TIME_ZONE = 'Asia/Shanghai'
|
||||
export const TIME_ZONE_OFFSET_MINUTES = 8 * 60
|
||||
Reference in new issue
Block a user