diff --git a/apps/api/src/collab/handler.ts b/apps/api/src/collab/handler.ts index 5ef4c5c..6aa10ed 100644 --- a/apps/api/src/collab/handler.ts +++ b/apps/api/src/collab/handler.ts @@ -198,6 +198,7 @@ export async function handleCollabMessage(ws: CollabSocket, raw: string) { studentId?: unknown language?: unknown reason?: unknown + timestamp?: unknown } try { message = JSON.parse(raw) as typeof message @@ -213,9 +214,7 @@ export async function handleCollabMessage(ws: CollabSocket, raw: string) { // 心跳不查库,和 /ws/submissions 的处理一致 if (message.type === "ping") { - ws.send( - JSON.stringify({ type: "pong", timestamp: (message as any).timestamp }), - ) + ws.send(JSON.stringify({ type: "pong", timestamp: message.timestamp })) return } diff --git a/apps/api/src/judge/status.ts b/apps/api/src/judge/status.ts index 2094038..13e2270 100644 --- a/apps/api/src/judge/status.ts +++ b/apps/api/src/judge/status.ts @@ -1,19 +1,8 @@ -export const JudgeStatus = { - COMPILE_ERROR: -2, - WRONG_ANSWER: -1, - ACCEPTED: 0, - CPU_TIME_LIMIT_EXCEEDED: 1, - REAL_TIME_LIMIT_EXCEEDED: 2, - MEMORY_LIMIT_EXCEEDED: 3, - RUNTIME_ERROR: 4, - SYSTEM_ERROR: 5, - PENDING: 6, - JUDGING: 7, - PARTIALLY_ACCEPTED: 8, - AST_CHECK_FAILED: 10, -} as const +import { JudgeStatus, type JudgeStatusValue } from "@oj2/contract" -export type JudgeStatusValue = (typeof JudgeStatus)[keyof typeof JudgeStatus] +// 状态码的唯一一份在 packages/contract/src/judge-status.ts,这里只再导出, +// 省得二十几处 import 一起改 +export { JudgeStatus, type JudgeStatusValue } export function isAccepted(result: number) { return ( @@ -22,7 +11,7 @@ export function isAccepted(result: number) { } /** - * 判题状态的中文名,和前端 `utils/constants.ts` 的 `JUDGE_STATUS` 一致,两边必须同步。 + * 判题状态的中文名,和前端 `utils/constants.ts` 的 `JUDGE_STATUS` 措辞对应(状态码本身已收进契约,名字仍是两份)。 * 目前只用在喂给模型的 prompt 里 —— 原来那里拼的是裸状态码(`结果:-1`), * 模型根本不知道 -1 是「答案错误」还是别的什么,等于白给一条信息。 */ diff --git a/apps/web/src/admin/tutorial/components/ExerciseManager.vue b/apps/web/src/admin/tutorial/components/ExerciseManager.vue index 197b6bd..5ff3a95 100644 --- a/apps/web/src/admin/tutorial/components/ExerciseManager.vue +++ b/apps/web/src/admin/tutorial/components/ExerciseManager.vue @@ -391,7 +391,7 @@ function typeTagType(type: ExerciseType) { {{ typeName(ex.type) }} - {{ (ex.data as any).question }} + {{ (ex.data as { question?: string }).question }} diff --git a/apps/web/src/oj/class/pk.vue b/apps/web/src/oj/class/pk.vue index e6ca7bd..81036b5 100644 --- a/apps/web/src/oj/class/pk.vue +++ b/apps/web/src/oj/class/pk.vue @@ -26,6 +26,7 @@ import { Legend, Colors, Filler, + type TooltipItem, } from "chart.js" // 注册Chart.js组件 @@ -673,9 +674,12 @@ const radarChartOptions = { }, tooltip: { callbacks: { - label: function (context: any) { - const dataset = context.dataset as any - const rawValue = dataset?.rawData?.[context.dataIndex] + label: function (context: TooltipItem<"radar">) { + // rawData 是我们自己塞进 dataset 的扩展字段,chart.js 的类型里没有 + const dataset = context.dataset as typeof context.dataset & { + rawData?: (number | null)[] + } + const rawValue = dataset.rawData?.[context.dataIndex] const metric = context.label || "" const isRate = context.dataIndex >= 3 if (rawValue === undefined || rawValue === null) { diff --git a/apps/web/src/oj/problem/components/ProblemSubmission.vue b/apps/web/src/oj/problem/components/ProblemSubmission.vue index 66645e1..54d9eb9 100644 --- a/apps/web/src/oj/problem/components/ProblemSubmission.vue +++ b/apps/web/src/oj/problem/components/ProblemSubmission.vue @@ -298,7 +298,7 @@ watch(query, listSubmissions) diff --git a/apps/web/src/shared/composables/configUpdate.ts b/apps/web/src/shared/composables/configUpdate.ts index 15e5eb8..fc597a9 100644 --- a/apps/web/src/shared/composables/configUpdate.ts +++ b/apps/web/src/shared/composables/configUpdate.ts @@ -19,7 +19,8 @@ export function useConfigUpdate() { const handleConfigUpdate = (data: ConfigUpdate) => { // 认不出来的键直接忽略:后端将来多推一个字段,不该把 store 撑出个野字段 if (!(data.key in configStore.config)) return - ;(configStore.config as any)[data.key] = data.value + ;(configStore.config as unknown as Record)[data.key] = + data.value // getConfig() 里也是这么设的,站点改名后标签页跟着变,别只更新页面里那份 if (data.key === "websiteName") document.title = data.value } diff --git a/apps/web/src/shared/composables/pagination.ts b/apps/web/src/shared/composables/pagination.ts index 9460a3e..365d3b5 100644 --- a/apps/web/src/shared/composables/pagination.ts +++ b/apps/web/src/shared/composables/pagination.ts @@ -42,11 +42,13 @@ export function usePagination>( limit: parseInt(route.query.limit) || defaultLimit, ...initialQuery, }) as unknown as T & PaginationQuery + // 键是运行时按 initialQuery 枚举出来的,静态类型写不出来;写入统一走这一个口子 + const writable = query as Record // 同步 URL 查询参数到本地状态 function syncFromRoute() { - ;(query as any).page = parseInt(route.query.page) || defaultPage - ;(query as any).limit = parseInt(route.query.limit) || defaultLimit + writable.page = parseInt(route.query.page) || defaultPage + writable.limit = parseInt(route.query.limit) || defaultLimit // 同步其他查询参数 Object.keys(initialQuery).forEach((key) => { @@ -54,11 +56,11 @@ export function usePagination>( if (value !== undefined) { // 处理不同类型的参数 if (typeof initialQuery[key] === "boolean") { - ;(query as any)[key] = value === "1" || value === "true" + writable[key] = value === "1" || value === "true" } else if (typeof initialQuery[key] === "number") { - ;(query as any)[key] = parseInt(value) || initialQuery[key] + writable[key] = parseInt(value) || initialQuery[key] } else { - ;(query as any)[key] = value || initialQuery[key] + writable[key] = value || initialQuery[key] } } }) @@ -75,7 +77,7 @@ export function usePagination>( // 重置页码到第一页 function resetPage() { - ;(query as any).page = defaultPage + writable.page = defaultPage } // 清空所有查询条件(除了分页参数) @@ -83,13 +85,13 @@ export function usePagination>( Object.keys(initialQuery).forEach((key) => { const initialValue = initialQuery[key] if (typeof initialValue === "string") { - ;(query as any)[key] = "" + writable[key] = "" } else if (typeof initialValue === "boolean") { - ;(query as any)[key] = false + writable[key] = false } else if (typeof initialValue === "number") { - ;(query as any)[key] = 0 + writable[key] = 0 } else { - ;(query as any)[key] = initialValue + writable[key] = initialValue } }) resetPage() diff --git a/apps/web/src/utils/constants.ts b/apps/web/src/utils/constants.ts index 7f01b12..92880fe 100644 --- a/apps/web/src/utils/constants.ts +++ b/apps/web/src/utils/constants.ts @@ -1,3 +1,4 @@ +import type { JudgeStatusValue } from "@oj2/contract" import type { AchievementRarity, SUBMISSION_RESULT, ReactionKey } from "./types" // 与后端 judge/status.ts 的 JudgeStatus 逐条对齐(submitting 除外,见下)。 @@ -21,6 +22,18 @@ export enum SubmissionStatus { ast_check_failed = 10, } +// 编译期对齐契约:契约加/改一个码而这里没跟,下面两行会当场编译不过。 +type SyncedWithContract = + Exclude< + `${SubmissionStatus}`, + `${SubmissionStatus.submitting}` + > extends `${JudgeStatusValue}` + ? `${JudgeStatusValue}` extends `${Exclude}` + ? true + : never + : never +export const _submissionStatusSynced: SyncedWithContract = true + export enum ContestStatus { initial = "2", // 这里不需要传入到后端,只是为了一开始加载数据的时候,做一个初始位 not_started = "1", diff --git a/packages/contract/src/index.ts b/packages/contract/src/index.ts index d2a3b84..af61b57 100644 --- a/packages/contract/src/index.ts +++ b/packages/contract/src/index.ts @@ -8,6 +8,7 @@ export * from "./common" export * from "./content" export * from "./contest" export * from "./flowchart" +export * from "./judge-status" export * from "./language" export * from "./problem" export * from "./problemset" diff --git a/packages/contract/src/judge-status.ts b/packages/contract/src/judge-status.ts new file mode 100644 index 0000000..c248b60 --- /dev/null +++ b/packages/contract/src/judge-status.ts @@ -0,0 +1,32 @@ +import { z } from "zod" + +/** + * 判题状态码 —— **前后端唯一的一份**。 + * + * 这些整数是落库的值:12 万条历史提交的 `submission.result` 就是它们,判题沙箱回的 + * 也是这套编码,所以只能新增、不能改已有的含义。后端 `judge/status.ts` 从这里再导出, + * 前端 `utils/constants.ts` 的 `SubmissionStatus` 用类型断言逐条对齐这里。 + */ +export const JudgeStatus = { + COMPILE_ERROR: -2, + WRONG_ANSWER: -1, + ACCEPTED: 0, + CPU_TIME_LIMIT_EXCEEDED: 1, + REAL_TIME_LIMIT_EXCEEDED: 2, + MEMORY_LIMIT_EXCEEDED: 3, + RUNTIME_ERROR: 4, + SYSTEM_ERROR: 5, + PENDING: 6, + JUDGING: 7, + PARTIALLY_ACCEPTED: 8, + AST_CHECK_FAILED: 10, +} as const + +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[]], +) diff --git a/packages/contract/src/submission.ts b/packages/contract/src/submission.ts index 07859d4..f20dbdb 100644 --- a/packages/contract/src/submission.ts +++ b/packages/contract/src/submission.ts @@ -1,23 +1,9 @@ import { z } from "zod" import { paginatedSchema } from "./common" +import { judgeStatusSchema, type JudgeStatus } from "./judge-status" import { problemLanguageSchema } from "./language" -export const judgeStatusSchema = z.union([ - z.literal(-2), - z.literal(-1), - z.literal(0), - z.literal(1), - z.literal(2), - z.literal(3), - z.literal(4), - z.literal(5), - z.literal(6), - z.literal(7), - z.literal(8), - z.literal(10), -]) - /** * 判题机原始输出(`submission.info` 的 JSONB 原文)。**只是类型,不作运行时校验。** * @@ -397,7 +383,6 @@ export const formatCodeRequestSchema = z.object({ export const formatCodeResponseSchema = z.object({ code: z.string() }) -export type JudgeStatus = z.infer export type StatisticInfo = z.infer export type CreateSubmissionRequest = z.infer< typeof createSubmissionRequestSchema