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