1 parent
23844ae301
commit
d25b5dbe6d
268 files changed
+3684
-8861
No files matched your search
@@ -24,10 +24,7 @@ export enum SubmissionStatus {
|
||||
|
||||
// 编译期对齐契约:契约加/改一个码而这里没跟,下面两行会当场编译不过。
|
||||
type SyncedWithContract =
|
||||
Exclude<
|
||||
`${SubmissionStatus}`,
|
||||
`${SubmissionStatus.submitting}`
|
||||
> extends `${JudgeStatusValue}`
|
||||
Exclude<`${SubmissionStatus}`, `${SubmissionStatus.submitting}`> extends `${JudgeStatusValue}`
|
||||
? `${JudgeStatusValue}` extends `${Exclude<SubmissionStatus, SubmissionStatus.submitting>}`
|
||||
? true
|
||||
: never
|
||||
@@ -165,8 +162,7 @@ export const DIFFICULTY = {
|
||||
High: "困难",
|
||||
} as const
|
||||
|
||||
const cSource =
|
||||
"#include<stdio.h>\r\n\r\nint main()\r\n{\r\n \r\n return 0;\r\n}"
|
||||
const cSource = "#include<stdio.h>\r\n\r\nint main()\r\n{\r\n \r\n return 0;\r\n}"
|
||||
const cppSource =
|
||||
"#include<iostream>\r\n\r\nusing namespace std;\r\n\r\nint main()\r\n{\r\n \r\n return 0;\r\n}"
|
||||
const pythonSource = ""
|
||||
@@ -285,10 +281,7 @@ export const RARITY_COLOR: Record<AchievementRarity, string> = {
|
||||
|
||||
// 文字要另配一套。上面那组是照深色底调的,搬到白底上白金只有 1.7:1、
|
||||
// 黄金 2.2:1,12px 的稀有度标签根本看不清。取色见 useRarityColor
|
||||
export const RARITY_TEXT_COLOR: Record<
|
||||
"dark" | "light",
|
||||
Record<AchievementRarity, string>
|
||||
> = {
|
||||
export const RARITY_TEXT_COLOR: Record<"dark" | "light", Record<AchievementRarity, string>> = {
|
||||
dark: {
|
||||
bronze: "#e08d63",
|
||||
silver: "#b6bcc7",
|
||||
@@ -311,17 +304,10 @@ export const RARITY_TEXT_COLOR: Record<
|
||||
* 它按「键长度 + 字节序」重排,读出来会变成 完整性 / 清晰度 / 规范性 / 逻辑正确性,
|
||||
* 40 分的那项排到最后。凡是要展示评分明细的地方都按这个顺序排,别直接遍历对象。
|
||||
*/
|
||||
export const FLOWCHART_CRITERIA_ORDER = [
|
||||
"逻辑正确性",
|
||||
"完整性",
|
||||
"规范性",
|
||||
"清晰度",
|
||||
]
|
||||
export const FLOWCHART_CRITERIA_ORDER = ["逻辑正确性", "完整性", "规范性", "清晰度"]
|
||||
|
||||
/** 按 FLOWCHART_CRITERIA_ORDER 排序,表里没有的键排在后面并保持原有相对顺序 */
|
||||
export function sortFlowchartCriteria<T>(
|
||||
details: Record<string, T>,
|
||||
): [string, T][] {
|
||||
export function sortFlowchartCriteria<T>(details: Record<string, T>): [string, T][] {
|
||||
const rank = (key: string) => {
|
||||
const i = FLOWCHART_CRITERIA_ORDER.indexOf(key)
|
||||
return i === -1 ? Number.MAX_SAFE_INTEGER : i
|
||||
@@ -380,9 +366,7 @@ export const CLASS_NAME_MIN_DIGITS = 3
|
||||
export const CLASS_NAME_MAX_DIGITS = 4
|
||||
|
||||
/** 合法班级号:3~4 位纯数字 */
|
||||
export const CLASS_NAME_RE = new RegExp(
|
||||
`^\\d{${CLASS_NAME_MIN_DIGITS},${CLASS_NAME_MAX_DIGITS}}$`,
|
||||
)
|
||||
export const CLASS_NAME_RE = new RegExp(`^\\d{${CLASS_NAME_MIN_DIGITS},${CLASS_NAME_MAX_DIGITS}}$`)
|
||||
|
||||
/** 用户名开头的 ks<班级号>,用于从用户名里认出班级 */
|
||||
export const USERNAME_CLASS_RE = new RegExp(
|
||||
|
||||
@@ -33,10 +33,7 @@ export function contract<T extends z.ZodType>(
|
||||
.slice(0, 5)
|
||||
.map((issue) => `${issue.path.join(".") || "(根)"}: ${issue.message}`)
|
||||
.join(";")
|
||||
const more =
|
||||
result.error.issues.length > 5
|
||||
? `;另有 ${result.error.issues.length - 5} 处`
|
||||
: ""
|
||||
const more = result.error.issues.length > 5 ? `;另有 ${result.error.issues.length - 5} 处` : ""
|
||||
console.error(
|
||||
`[契约] ${endpoint} 的响应不符合契约 —— ${issues}${more}\n` +
|
||||
" 已放行原始数据(页面照常渲染)。契约在 packages/contract/src/。",
|
||||
|
||||
@@ -3,14 +3,7 @@ import type { JudgeCaseResult, JudgeInfo } from "@oj2/contract"
|
||||
import { getTime, intervalToDuration, parseISO, type Duration } from "date-fns"
|
||||
import { AdminUser, Submission } from "./types"
|
||||
import { JUDGE_STATUS, USER_TYPE } from "./constants"
|
||||
import {
|
||||
strFromU8,
|
||||
strToU8,
|
||||
unzlibSync,
|
||||
zipSync,
|
||||
zlibSync,
|
||||
type Zippable,
|
||||
} from "fflate"
|
||||
import { strFromU8, strToU8, unzlibSync, zipSync, zlibSync, type Zippable } from "fflate"
|
||||
import copyTextFallback from "copy-text-to-clipboard"
|
||||
import { normalizeDate } from "@vueuse/core"
|
||||
import { customAlphabet } from "nanoid"
|
||||
@@ -44,24 +37,17 @@ export function submissionCaseResults(info: unknown): JudgeCaseResult[] {
|
||||
* 一个都没过不给:「通过 0/8」只是把「答案错误」换个更刺眼的说法再说一遍。
|
||||
* 全过也不给:AC 用不着;AST_CHECK_FAILED 是测试点全过、语法规则没过,那边有自己的规则清单。
|
||||
*/
|
||||
export function submissionPartialCases(
|
||||
submission: Pick<Submission, "caseSummary"> | undefined,
|
||||
) {
|
||||
export function submissionPartialCases(submission: Pick<Submission, "caseSummary"> | undefined) {
|
||||
const summary = submission?.caseSummary
|
||||
if (!summary || summary.passed === 0 || summary.passed >= summary.total)
|
||||
return null
|
||||
if (!summary || summary.passed === 0 || summary.passed >= summary.total) return null
|
||||
return summary
|
||||
}
|
||||
|
||||
/** 结果标题:部分测试点通过时缀上「通过 x/y 个测试点」 */
|
||||
export function submissionResultTitle(
|
||||
submission: Pick<Submission, "result" | "caseSummary">,
|
||||
) {
|
||||
export function submissionResultTitle(submission: Pick<Submission, "result" | "caseSummary">) {
|
||||
const title = JUDGE_STATUS[submission.result]["title"]
|
||||
const partial = submissionPartialCases(submission)
|
||||
return partial
|
||||
? `${title} · 通过 ${partial.passed}/${partial.total} 个测试点`
|
||||
: title
|
||||
return partial ? `${title} · 通过 ${partial.passed}/${partial.total} 个测试点` : title
|
||||
}
|
||||
|
||||
export function getACRate(acCount: number, totalCount: number): string {
|
||||
@@ -72,9 +58,7 @@ export function getACRateNumber(acCount: number, totalCount: number): number {
|
||||
return parseFloat(calculateACRate(acCount, totalCount))
|
||||
}
|
||||
|
||||
export function filterEmptyValue<T extends Record<string, any>>(
|
||||
object: T,
|
||||
): Partial<T> {
|
||||
export function filterEmptyValue<T extends Record<string, any>>(object: T): Partial<T> {
|
||||
return Object.entries(object).reduce((query, [key, value]) => {
|
||||
if (value != null && value !== "" && value !== undefined) {
|
||||
query[key as keyof T] = value
|
||||
@@ -83,9 +67,7 @@ export function filterEmptyValue<T extends Record<string, any>>(
|
||||
}, {} as Partial<T>)
|
||||
}
|
||||
|
||||
export function getTagColor(
|
||||
tag: "Low" | "Mid" | "High" | "简单" | "中等" | "困难",
|
||||
) {
|
||||
export function getTagColor(tag: "Low" | "Mid" | "High" | "简单" | "中等" | "困难") {
|
||||
return <"success" | "info" | "error">{
|
||||
Low: "success",
|
||||
Mid: "info",
|
||||
@@ -203,11 +185,7 @@ function formatDurationUnits(
|
||||
.join("")
|
||||
}
|
||||
|
||||
export function duration(
|
||||
start: Date | string,
|
||||
end: Date | string,
|
||||
showSeconds = false,
|
||||
): string {
|
||||
export function duration(start: Date | string, end: Date | string, showSeconds = false): string {
|
||||
const durationObj = getDurationObject(start, end)
|
||||
const units = [
|
||||
{ key: "years" as const, suffix: "年" },
|
||||
@@ -234,10 +212,7 @@ export function readableDuration(seconds: number): string {
|
||||
return rest ? `${hours} 小时 ${rest} 分` : `${hours} 小时`
|
||||
}
|
||||
|
||||
export function durationToDays(
|
||||
start: Date | string,
|
||||
end: Date | string,
|
||||
): string {
|
||||
export function durationToDays(start: Date | string, end: Date | string): string {
|
||||
const durationObj = getDurationObject(start, end)
|
||||
const units = [
|
||||
{ key: "years" as const, suffix: "年" },
|
||||
@@ -255,9 +230,7 @@ export function secondsToDuration(seconds: number): string {
|
||||
})
|
||||
const hours = (duration.days ?? 0) * 24 + (duration.hours ?? 0)
|
||||
const pad = (n: number) => String(n).padStart(2, "0")
|
||||
return [hours, pad(duration.minutes ?? 0), pad(duration.seconds ?? 0)].join(
|
||||
":",
|
||||
)
|
||||
return [hours, pad(duration.minutes ?? 0), pad(duration.seconds ?? 0)].join(":")
|
||||
}
|
||||
|
||||
export function submissionMemoryFormat(memory: number | string | undefined) {
|
||||
@@ -329,9 +302,7 @@ export function base64DecodeUtf8(bytes?: string): string {
|
||||
if (!bytes) return ""
|
||||
const latin = atob(bytes)
|
||||
return new TextDecoder("utf-8").decode(
|
||||
Uint8Array.from({ length: latin.length }, (_, index) =>
|
||||
latin.charCodeAt(index),
|
||||
),
|
||||
Uint8Array.from({ length: latin.length }, (_, index) => latin.charCodeAt(index)),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("解码失败:", error)
|
||||
|
||||
@@ -36,10 +36,7 @@ export async function createTestSubmission(code: Code, input: string) {
|
||||
const data = response.data
|
||||
return {
|
||||
status: data.status && data.status.id,
|
||||
output: [
|
||||
base64DecodeUtf8(data.compile_output),
|
||||
base64DecodeUtf8(data.stdout),
|
||||
]
|
||||
output: [base64DecodeUtf8(data.compile_output), base64DecodeUtf8(data.stdout)]
|
||||
.join("\n")
|
||||
.trim(),
|
||||
}
|
||||
|
||||
@@ -62,12 +62,7 @@ export type LANGUAGE = ProblemLanguage
|
||||
* 而契约那边是 `Record<string, unknown>`,等于渲染表格的那段代码全靠手抄件兜底。
|
||||
* 键名的 snake_case 是 JSONB 原文,见契约 sqlDisplaySchema 的注释。
|
||||
*/
|
||||
export type {
|
||||
SqlConfig,
|
||||
SqlDisplay,
|
||||
SqlDisplayTable,
|
||||
SqlDisplayColumn,
|
||||
} from "@oj2/contract"
|
||||
export type { SqlConfig, SqlDisplay, SqlDisplayTable, SqlDisplayColumn } from "@oj2/contract"
|
||||
|
||||
/**
|
||||
* 判题状态码 + 前端本地的「正在提交」(9)。
|
||||
@@ -146,10 +141,7 @@ type ExcludeKeys =
|
||||
| "isPublic"
|
||||
| "contestId"
|
||||
|
||||
export type BlankProblem = Omit<
|
||||
AdminProblem,
|
||||
ExcludeKeys | "hint" | "mermaidCode"
|
||||
> & {
|
||||
export type BlankProblem = Omit<AdminProblem, ExcludeKeys | "hint" | "mermaidCode"> & {
|
||||
id?: number
|
||||
// 新建比赛题时由 detail.vue 在提交前写进来
|
||||
contestId?: number | null
|
||||
@@ -293,11 +285,7 @@ export type { MyRank } from "@oj2/contract"
|
||||
/** 本周进步榜:`rank` 是周榜名次,跟存量总榜的名次没有关系 */
|
||||
export type { WeeklyRank, WeeklyRankItem } from "@oj2/contract"
|
||||
|
||||
export type {
|
||||
ClassComparison,
|
||||
ClassRankItem,
|
||||
ClassUserRank,
|
||||
} from "@oj2/contract"
|
||||
export type { ClassComparison, ClassRankItem, ClassUserRank } from "@oj2/contract"
|
||||
|
||||
/**
|
||||
* 学生侧的比赛:不含 password / visible。后台用 `AdminContest` ——
|
||||
@@ -307,12 +295,7 @@ export type { Contest, AdminContest } from "@oj2/contract"
|
||||
|
||||
export type BlankContest = Omit<
|
||||
AdminContest,
|
||||
| "id"
|
||||
| "createdBy"
|
||||
| "createTime"
|
||||
| "lastUpdateTime"
|
||||
| "status"
|
||||
| "contestType"
|
||||
"id" | "createdBy" | "createTime" | "lastUpdateTime" | "status" | "contestType"
|
||||
>
|
||||
|
||||
/**
|
||||
@@ -408,11 +391,7 @@ export type {
|
||||
|
||||
/** 学生自己的自学留痕,学习页的目录拿它打勾 */
|
||||
export type { TutorialProgress } from "@oj2/contract"
|
||||
import type {
|
||||
AdminExercise,
|
||||
AdminTutorial,
|
||||
CreateSubmissionRequest,
|
||||
} from "@oj2/contract"
|
||||
import type { AdminExercise, AdminTutorial, CreateSubmissionRequest } from "@oj2/contract"
|
||||
|
||||
/**
|
||||
* 教程编辑表单。只留可编辑字段 —— createdBy / createdAt / updatedAt 由后端产出,
|
||||
@@ -420,10 +399,9 @@ import type {
|
||||
*
|
||||
* `code` 收窄成 string:读回来时统一 `?? ""`,代码编辑器的 v-model 不接受 null。
|
||||
*/
|
||||
export type TutorialEdit = Omit<
|
||||
AdminTutorial,
|
||||
"createdBy" | "createdAt" | "updatedAt" | "code"
|
||||
> & { code: string }
|
||||
export type TutorialEdit = Omit<AdminTutorial, "createdBy" | "createdAt" | "updatedAt" | "code"> & {
|
||||
code: string
|
||||
}
|
||||
|
||||
export interface ExerciseMcqData {
|
||||
question: string
|
||||
@@ -514,5 +492,4 @@ export type {
|
||||
* 弹窗队列里的条目。`/achievements/pending` 拉来的没有 kind,
|
||||
* WebSocket 推来的有 —— 两个来源会汇进同一个队列。
|
||||
*/
|
||||
export type QueuedAchievement = PendingAchievement &
|
||||
Partial<Pick<AchievementNotification, "kind">>
|
||||
export type QueuedAchievement = PendingAchievement & Partial<Pick<AchievementNotification, "kind">>
|
||||
Reference in new issue
Block a user