remove contest type
Some checks failed
Deploy / deploy (build, debian, 22, /root/OJDeploy/data/clientnext) (push) Has been cancelled
Deploy / deploy (build:staging, school, 8822, /root/OJ/data/dist) (push) Has been cancelled

This commit is contained in:
2026-05-26 23:10:15 -06:00
parent c1678c9060
commit cb7743367a
11 changed files with 246 additions and 89 deletions

View File

@@ -56,9 +56,7 @@ const contest = reactive<BlankContest & { id: number }>({
tag: "练习",
start_time: "",
end_time: "",
rule_type: "ACM",
password: "",
real_time_rank: true,
visible: false,
allowed_ip_ranges: [],
})
@@ -79,9 +77,7 @@ async function getContestDetail() {
contest.tag = data.tag
contest.start_time = data.start_time
contest.end_time = data.end_time
contest.rule_type = "ACM"
contest.password = data.password
contest.real_time_rank = true
contest.visible = data.visible
contest.allowed_ip_ranges = []

View File

@@ -24,23 +24,41 @@ const emit = defineEmits<{
const activeTab = ref(props.languages[0] || "Python3")
const ENGINE_OPTIONS: SelectOption[] = [
{ label: "节点检查", type: "group", key: "node_group", children: [
{ label: "必须存在", value: "must_exist_node" },
{ label: "不能存在", value: "must_not_exist_node" },
{ label: "出现次数", value: "count_node" },
]},
{ label: "函数调用", type: "group", key: "func_group", children: [
{ label: "必须调用函数", value: "must_call_function" },
{ label: "不能调用函数", value: "must_not_call_function" },
{ label: "函数调用次数", value: "count_function_call" },
]},
{ label: "方法调用", type: "group", key: "method_group", children: [
{ label: "必须调用方法", value: "must_call_method" },
{ label: "不能调用方法", value: "must_not_call_method" },
]},
{ label: "运算符", type: "group", key: "op_group", children: [
{ label: "必须使用运算符", value: "must_use_operator" },
]},
{
label: "节点检查",
type: "group",
key: "node_group",
children: [
{ label: "必须存在", value: "must_exist_node" },
{ label: "不能存在", value: "must_not_exist_node" },
{ label: "出现次数", value: "count_node" },
],
},
{
label: "函数调用",
type: "group",
key: "func_group",
children: [
{ label: "必须调用函数", value: "must_call_function" },
{ label: "不能调用函数", value: "must_not_call_function" },
{ label: "函数调用次数", value: "count_function_call" },
],
},
{
label: "方法调用",
type: "group",
key: "method_group",
children: [
{ label: "必须调用方法", value: "must_call_method" },
{ label: "不能调用方法", value: "must_not_call_method" },
],
},
{
label: "运算符",
type: "group",
key: "op_group",
children: [{ label: "必须使用运算符", value: "must_use_operator" }],
},
]
const NODE_TARGET_OPTIONS: SelectOption[] = [
@@ -83,16 +101,30 @@ const OPERATOR_TARGET_OPTIONS: SelectOption[] = [
]
const NODE_ENGINES = ["must_exist_node", "must_not_exist_node", "count_node"]
const FUNCTION_ENGINES = ["must_call_function", "must_not_call_function", "count_function_call"]
const FUNCTION_ENGINES = [
"must_call_function",
"must_not_call_function",
"count_function_call",
]
const METHOD_ENGINES = ["must_call_method", "must_not_call_method"]
const OPERATOR_ENGINES = ["must_use_operator"]
const COUNT_ENGINES = ["count_node", "count_function_call"]
function isNodeEngine(engine: string) { return NODE_ENGINES.includes(engine) }
function isFunctionEngine(engine: string) { return FUNCTION_ENGINES.includes(engine) }
function isMethodEngine(engine: string) { return METHOD_ENGINES.includes(engine) }
function isOperatorEngine(engine: string) { return OPERATOR_ENGINES.includes(engine) }
function isCountEngine(engine: string) { return COUNT_ENGINES.includes(engine) }
function isNodeEngine(engine: string) {
return NODE_ENGINES.includes(engine)
}
function isFunctionEngine(engine: string) {
return FUNCTION_ENGINES.includes(engine)
}
function isMethodEngine(engine: string) {
return METHOD_ENGINES.includes(engine)
}
function isOperatorEngine(engine: string) {
return OPERATOR_ENGINES.includes(engine)
}
function isCountEngine(engine: string) {
return COUNT_ENGINES.includes(engine)
}
const COUNT_MODE_OPTIONS: SelectOption[] = [
{ label: "精确", value: "exact" },
@@ -126,9 +158,15 @@ function updateExactCount(lang: string, index: number, v: number | null) {
updateRules(lang, rules)
}
function needsTargetDropdown(engine: string) { return isNodeEngine(engine) }
function needsTargetInput(engine: string) { return isFunctionEngine(engine) || isMethodEngine(engine) }
function needsOperatorDropdown(engine: string) { return isOperatorEngine(engine) }
function needsTargetDropdown(engine: string) {
return isNodeEngine(engine)
}
function needsTargetInput(engine: string) {
return isFunctionEngine(engine) || isMethodEngine(engine)
}
function needsOperatorDropdown(engine: string) {
return isOperatorEngine(engine)
}
function getRulesForLang(lang: string): AstRule[] {
if (!props.modelValue) return []
@@ -149,13 +187,19 @@ function getTargetLabel(engine: string, target: string): string | undefined {
if (isNodeEngine(engine))
return (NODE_TARGET_OPTIONS.find((o) => o.value === target) as any)?.label
if (isOperatorEngine(engine))
return (OPERATOR_TARGET_OPTIONS.find((o) => o.value === target) as any)?.label
return (OPERATOR_TARGET_OPTIONS.find((o) => o.value === target) as any)
?.label
return undefined
}
function addRule(lang: string) {
const rules = [...getRulesForLang(lang)]
rules.push({ engine: "must_exist_node", target: "for_loop", label: "for 循环", message: "" })
rules.push({
engine: "must_exist_node",
target: "for_loop",
label: "for 循环",
message: "",
})
updateRules(lang, rules)
}
@@ -171,9 +215,16 @@ function updateRule(lang: string, index: number, field: string, value: any) {
if (field === "engine") {
rule.engine = value
if (isNodeEngine(value)) { rule.target = "for_loop"; rule.label = "for 循环" }
else if (isOperatorEngine(value)) { rule.target = "+"; rule.label = "+" }
else { rule.target = ""; delete rule.label }
if (isNodeEngine(value)) {
rule.target = "for_loop"
rule.label = "for 循环"
} else if (isOperatorEngine(value)) {
rule.target = "+"
rule.label = "+"
} else {
rule.target = ""
delete rule.label
}
delete rule.min
delete rule.max
delete rule.exact
@@ -196,25 +247,39 @@ function updateRule(lang: string, index: number, field: string, value: any) {
updateRules(lang, rules)
}
watch(() => props.languages, (langs) => {
if (langs.length && !langs.includes(activeTab.value as LANGUAGE)) {
activeTab.value = langs[0]
}
})
watch(
() => props.languages,
(langs) => {
if (langs.length && !langs.includes(activeTab.value as LANGUAGE)) {
activeTab.value = langs[0]
}
},
)
</script>
<template>
<n-collapse>
<n-collapse-item title="代码规则检查(选填)" name="ast-rules">
<n-tabs v-if="languages.length" type="segment" v-model:value="activeTab">
<n-tab-pane v-for="lang in languages" :key="lang" :name="lang" :tab="lang">
<n-tab-pane
v-for="lang in languages"
:key="lang"
:name="lang"
:tab="lang"
>
<n-flex vertical>
<div v-for="(rule, index) in getRulesForLang(lang)" :key="index" style="margin-bottom: 8px">
<div
v-for="(rule, index) in getRulesForLang(lang)"
:key="index"
style="margin-bottom: 8px"
>
<n-flex align="center" :wrap="false">
<n-select
:options="ENGINE_OPTIONS"
:value="rule.engine"
@update:value="(v: string) => updateRule(lang, index, 'engine', v)"
@update:value="
(v: string) => updateRule(lang, index, 'engine', v)
"
style="width: 150px"
size="small"
/>
@@ -222,7 +287,9 @@ watch(() => props.languages, (langs) => {
v-if="needsTargetDropdown(rule.engine)"
:options="NODE_TARGET_OPTIONS"
:value="rule.target"
@update:value="(v: string) => updateRule(lang, index, 'target', v)"
@update:value="
(v: string) => updateRule(lang, index, 'target', v)
"
style="width: 150px"
size="small"
filterable
@@ -230,7 +297,9 @@ watch(() => props.languages, (langs) => {
<n-input
v-if="needsTargetInput(rule.engine)"
:value="rule.target"
@update:value="(v: string) => updateRule(lang, index, 'target', v)"
@update:value="
(v: string) => updateRule(lang, index, 'target', v)
"
placeholder="函数/方法名"
style="width: 150px"
size="small"
@@ -239,7 +308,9 @@ watch(() => props.languages, (langs) => {
v-if="needsOperatorDropdown(rule.engine)"
:options="OPERATOR_TARGET_OPTIONS"
:value="rule.target"
@update:value="(v: string) => updateRule(lang, index, 'target', v)"
@update:value="
(v: string) => updateRule(lang, index, 'target', v)
"
style="width: 150px"
size="small"
/>
@@ -247,14 +318,18 @@ watch(() => props.languages, (langs) => {
<n-select
:options="COUNT_MODE_OPTIONS"
:value="getCountMode(rule)"
@update:value="(v: 'exact' | 'range') => updateCountMode(lang, index, v)"
@update:value="
(v: 'exact' | 'range') => updateCountMode(lang, index, v)
"
style="width: 80px"
size="small"
/>
<n-input-number
v-if="getCountMode(rule) === 'exact'"
:value="rule.exact ?? null"
@update:value="(v: number | null) => updateExactCount(lang, index, v)"
@update:value="
(v: number | null) => updateExactCount(lang, index, v)
"
placeholder="次数"
style="width: 100px"
size="small"
@@ -264,7 +339,9 @@ watch(() => props.languages, (langs) => {
<template v-else>
<n-input-number
:value="rule.min ?? null"
@update:value="(v: number | null) => updateRule(lang, index, 'min', v)"
@update:value="
(v: number | null) => updateRule(lang, index, 'min', v)
"
placeholder="最少"
style="width: 100px"
size="small"
@@ -273,7 +350,9 @@ watch(() => props.languages, (langs) => {
/>
<n-input-number
:value="rule.max ?? null"
@update:value="(v: number | null) => updateRule(lang, index, 'max', v)"
@update:value="
(v: number | null) => updateRule(lang, index, 'max', v)
"
placeholder="最多"
style="width: 100px"
size="small"
@@ -284,17 +363,29 @@ watch(() => props.languages, (langs) => {
</template>
<n-input
:value="rule.message"
@update:value="(v: string) => updateRule(lang, index, 'message', v)"
@update:value="
(v: string) => updateRule(lang, index, 'message', v)
"
placeholder="错误提示(选填)"
style="flex: 1"
size="small"
/>
<n-button size="small" tertiary type="error" @click="removeRule(lang, index)">
<n-button
size="small"
tertiary
type="error"
@click="removeRule(lang, index)"
>
删除
</n-button>
</n-flex>
</div>
<n-button size="small" tertiary type="primary" @click="addRule(lang)">
<n-button
size="small"
tertiary
type="primary"
@click="addRule(lang)"
>
添加规则
</n-button>
</n-flex>

View File

@@ -87,12 +87,24 @@ const columns: DataTableColumn<AdminProblemFiltered>[] = [
render: (row) =>
h(NFlex, { size: 4, align: "center" }, () => [
row.allow_flowchart
? h(Icon, { width: 18, icon: "vscode-icons:file-type-drawio", title: "绘图" })
? h(Icon, {
width: 18,
icon: "vscode-icons:file-type-drawio",
title: "绘图",
})
: row.show_flowchart
? h(Icon, { width: 18, icon: "vscode-icons:file-type-graphql", title: "流程图" })
? h(Icon, {
width: 18,
icon: "vscode-icons:file-type-graphql",
title: "流程图",
})
: null,
row.has_ast_rules
? h(Icon, { width: 18, icon: "vscode-icons:file-type-light-todo", title: "AST" })
? h(Icon, {
width: 18,
icon: "vscode-icons:file-type-light-todo",
title: "AST",
})
: null,
]),
},

View File

@@ -211,7 +211,7 @@ export async function getContestProblems(contestID: string) {
export function getContestRank(
contestID: string,
query: { limit: number; offset: number; force_refresh: "1" | "0" },
query: { limit: number; offset: number },
) {
return http.get("contest_rank", {
params: {

View File

@@ -95,7 +95,6 @@ async function listRanks() {
const res = await getContestRank(props.contestID, {
limit: query.limit,
offset: query.limit * (query.page - 1),
force_refresh: "1",
})
total.value = res.data.total
data.value = res.data.results
@@ -225,7 +224,6 @@ async function downloadExcel() {
const res = await getContestRank(props.contestID, {
limit: total.value || 10000,
offset: 0,
force_refresh: "1",
})
const allRanks: ContestRank[] = res.data.results

View File

@@ -119,29 +119,41 @@ function ruleDescription(rule: AstRule): string {
const targetLabel = rule.label || NODE_TARGET_LABELS[target] || target
const countDesc = () => {
if (rule.exact !== undefined) return `出现 ${rule.exact}`
if (rule.min !== undefined && rule.max !== undefined) return `出现 ${rule.min}${rule.max}`
if (rule.min !== undefined && rule.max !== undefined)
return `出现 ${rule.min}${rule.max}`
if (rule.min !== undefined) return `至少出现 ${rule.min}`
if (rule.max !== undefined) return `至多出现 ${rule.max}`
return ""
}
const callDesc = () => {
if (rule.exact !== undefined) return `调用 ${rule.exact}`
if (rule.min !== undefined && rule.max !== undefined) return `调用 ${rule.min}${rule.max}`
if (rule.min !== undefined && rule.max !== undefined)
return `调用 ${rule.min}${rule.max}`
if (rule.min !== undefined) return `至少调用 ${rule.min}`
if (rule.max !== undefined) return `至多调用 ${rule.max}`
return ""
}
switch (rule.engine) {
case "must_exist_node": return `必须使用 ${targetLabel}`
case "must_not_exist_node": return `不能使用 ${targetLabel}`
case "count_node": return `${targetLabel} ${countDesc()}`
case "must_call_function": return `必须调${target}()`
case "must_not_call_function": return `不能调用 ${target}()`
case "count_function_call": return `${target}() ${callDesc()}`
case "must_call_method": return `必须调用 .${target}()`
case "must_not_call_method": return `不能调用 .${target}()`
case "must_use_operator": return `必须使用 ${target} 运算符`
default: return rule.engine
case "must_exist_node":
return `必须使用 ${targetLabel}`
case "must_not_exist_node":
return `不能使${targetLabel}`
case "count_node":
return `${targetLabel} ${countDesc()}`
case "must_call_function":
return `必须调用 ${target}()`
case "must_not_call_function":
return `不能调用 ${target}()`
case "count_function_call":
return `${target}() ${callDesc()}`
case "must_call_method":
return `必须调用 .${target}()`
case "must_not_call_method":
return `不能调用 .${target}()`
case "must_use_operator":
return `必须使用 ${target} 运算符`
default:
return rule.engine
}
}
@@ -153,7 +165,9 @@ function ruleTagType(engine: string): "error" | "success" | "info" {
const astRulesForDisplay = computed(() => {
if (!problem.value?.ast_rules) return []
return Object.entries(problem.value.ast_rules).filter(([, rules]) => rules.length > 0)
return Object.entries(problem.value.ast_rules).filter(
([, rules]) => rules.length > 0,
)
})
async function test(sample: Sample, index: number) {
@@ -302,14 +316,18 @@ function type(status: ProblemStatus) {
</n-flex>
</p>
<div v-for="[lang, rules] in astRulesForDisplay" :key="lang">
<p v-if="astRulesForDisplay.length > 1" class="lang-label">{{ lang }}</p>
<p v-if="astRulesForDisplay.length > 1" class="lang-label">
{{ lang }}
</p>
<n-list bordered style="margin-bottom: 8px">
<n-list-item v-for="(rule, i) in rules" :key="i">
<n-flex align="center">
<n-tag :type="ruleTagType(rule.engine)">
{{ ruleDescription(rule) }}
</n-tag>
<span v-if="rule.message" class="rule-message">{{ rule.message }}</span>
<span v-if="rule.message" class="rule-message">{{
rule.message
}}</span>
</n-flex>
</n-list-item>
</n-list>

View File

@@ -43,7 +43,10 @@ const msg = computed(() => {
msg += "请仔细检查,看看代码的格式是不是写错了!\n\n"
}
if (result !== SubmissionStatus.ast_check_failed && props.submission.statistic_info?.err_info) {
if (
result !== SubmissionStatus.ast_check_failed &&
props.submission.statistic_info?.err_info
) {
msg += props.submission.statistic_info.err_info
}
@@ -153,7 +156,14 @@ const columns: DataTableColumn<Submission["info"]["data"][number]>[] = [
:title="JUDGE_STATUS[submission.result]['title']"
class="mb-3"
/>
<n-flex vertical v-if="msg || infoTable.length || submission.statistic_info?.ast_results?.length">
<n-flex
vertical
v-if="
msg ||
infoTable.length ||
submission.statistic_info?.ast_results?.length
"
>
<n-card v-if="submission.statistic_info?.ast_results?.length" embedded>
<n-flex vertical :size="8">
<n-flex
@@ -162,7 +172,9 @@ const columns: DataTableColumn<Submission["info"]["data"][number]>[] = [
align="center"
:size="6"
>
<n-icon :color="rule.passed ? theme.successColor : theme.errorColor">
<n-icon
:color="rule.passed ? theme.successColor : theme.errorColor"
>
<Icon :icon="rule.passed ? 'ep:select' : 'ep:close-bold'" />
</n-icon>
<span>{{ rule.description }}</span>

View File

@@ -149,7 +149,10 @@ watch(
result === SubmissionStatus.submitting
)
return
if (result !== SubmissionStatus.accepted && result !== SubmissionStatus.ast_check_failed) {
if (
result !== SubmissionStatus.accepted &&
result !== SubmissionStatus.ast_check_failed
) {
problemStore.incrementFailCount()
}
},
@@ -159,7 +162,11 @@ watch(
watch(
() => submission.value?.result,
async (result) => {
if (result !== SubmissionStatus.accepted && result !== SubmissionStatus.ast_check_failed) return
if (
result !== SubmissionStatus.accepted &&
result !== SubmissionStatus.ast_check_failed
)
return
// 1. 刷新题目状态
problem.value!.my_status = 0

View File

@@ -267,7 +267,11 @@ watch(isMobile, (value) => {
>
<ProblemComment />
</n-tab-pane>
<n-tab-pane v-if="myFlowchartStore.showing" name="my-flowchart" tab="我的流程图">
<n-tab-pane
v-if="myFlowchartStore.showing"
name="my-flowchart"
tab="我的流程图"
>
<MyFlowchartTab />
</n-tab-pane>
<n-tab-pane name="submission" tab="提交" :disabled="!!props.problemSetId">

View File

@@ -102,11 +102,11 @@ export function secondsToDuration(seconds: number): string {
start: 0,
end: seconds * 1000,
})
return [
duration.hours ?? 0,
duration.minutes ?? 0,
duration.seconds ?? 0,
].join(":")
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(
":",
)
}
export function submissionMemoryFormat(memory: number | string | undefined) {

View File

@@ -65,7 +65,20 @@ export type LANGUAGE =
export type LANGUAGE_SHOW_LABEL =
(typeof LANGUAGE_SHOW_VALUE)[keyof typeof LANGUAGE_SHOW_VALUE]
export type SUBMISSION_RESULT = -2 | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10
export type SUBMISSION_RESULT =
| -2
| -1
| 0
| 1
| 2
| 3
| 4
| 5
| 6
| 7
| 8
| 9
| 10
export type ProblemStatus = "passed" | "failed" | "not_test"
@@ -137,7 +150,15 @@ export interface Problem {
flowchart_data?: Record<string, any>
flowchart_hint?: string
show_flowchart?: boolean
ast_rules?: { [key: string]: { engine: string; target?: string; min?: number; max?: number; message: string }[] } | null
ast_rules?: {
[key: string]: {
engine: string
target?: string
min?: number
max?: number
message: string
}[]
} | null
has_ast_rules?: boolean
}
@@ -482,9 +503,7 @@ export interface BlankContest {
tag: string
start_time: string
end_time: string
rule_type: "ACM" | "OI"
password: string
real_time_rank: boolean
visible: boolean
allowed_ip_ranges: { value: string }[]
}