update
Deploy / deploy (push) Canceled after 0s

This commit is contained in:
xuyue committed 2026-09-27 19:36:28 +08:00
1 parent 23844ae301
commit d25b5dbe6d
268 files changed
+3684 -8861

No files matched your search

@@ -1,10 +1,6 @@
<script lang="ts" setup>
import { errorCode, errorMessage } from "utils/api"
import {
deleteContestProblem,
deleteProblem,
makeProblemPublic,
} from "admin/api"
import { deleteContestProblem, deleteProblem, makeProblemPublic } from "admin/api"
import download from "utils/download"
interface Props {
@@ -18,9 +14,7 @@ const route = useRoute()
const router = useRouter()
const message = useMessage()
const isContestProblem = computed(
() => route.name === "admin contest problem list",
)
const isContestProblem = computed(() => route.name === "admin contest problem list")
const showMakePublicModal = ref(false)
const newDisplayID = ref("")
@@ -95,20 +89,11 @@ async function handleMakePublic() {
</script>
<template>
<n-flex>
<n-button size="small" secondary type="primary" @click="goEdit">
编辑
</n-button>
<n-button size="small" secondary type="info" @click="goCheck">
查看
</n-button>
<n-button size="small" secondary type="primary" @click="goEdit"> 编辑 </n-button>
<n-button size="small" secondary type="info" @click="goCheck"> 查看 </n-button>
<n-tooltip v-if="isContestProblem">
<template #trigger>
<n-button
size="small"
secondary
type="warning"
@click="openMakePublicModal"
>
<n-button size="small" secondary type="warning" @click="openMakePublicModal">
公开
</n-button>
</template>
@@ -135,9 +120,7 @@ async function handleMakePublic() {
style="width: 500px"
>
<n-space vertical>
<p>
将竞赛题目转为公开题目后,会创建一个新的公开题目副本,原题目保持不变。
</p>
<p>将竞赛题目转为公开题目后,会创建一个新的公开题目副本,原题目保持不变。</p>
<n-form>
<n-form-item label="新的题目编号" required>
<n-input
@@ -148,8 +131,7 @@ async function handleMakePublic() {
/>
</n-form-item>
</n-form>
<n-alert type="info" title="提示:请输入一个未被使用的题目编号">
</n-alert>
<n-alert type="info" title="提示:请输入一个未被使用的题目编号"> </n-alert>
</n-space>
<template #footer>
<n-flex justify="end">
@@ -17,11 +17,7 @@ const displayID = ref(props.nextDisplayId || "")
async function addProblem() {
if (!displayID.value) return
try {
await addProblemForContest(
props.contestID,
props.problemID,
displayID.value,
)
await addProblemForContest(props.contestID, props.problemID, displayID.value)
emit("added")
} catch (err) {
if (errorCode(err) === "display-id-exists") {
@@ -73,18 +73,9 @@ watchDebounced(
title="从题库中添加"
@close="$emit('update:show', false)"
>
<n-input
class="search"
v-model:value="query.keyword"
clearable
placeholder="搜索标题或编号"
/>
<n-input class="search" v-model:value="query.keyword" clearable placeholder="搜索标题或编号" />
<n-data-table striped :columns="columns" :data="problems" />
<Pagination
:total="total"
v-model:limit="query.limit"
v-model:page="query.page"
/>
<Pagination :total="total" v-model:limit="query.limit" v-model:page="query.page" />
</n-modal>
</template>
<style scoped>
@@ -70,27 +70,22 @@ const ENGINE_OPTIONS: SelectOption[] = [
// 列表推导式、f-string 这些 C 根本没有的东西 —— 存得进去,判题时永远失败
// (或者反过来,「不能使用 f-string」永远通过),两头都不报错。
function nodeTargetOptions(lang: string): SelectOption[] {
return Object.entries(AST_NODE_TARGETS_BY_LANGUAGE[lang] ?? {}).map(
([value, entry]) => ({ label: entry.label, value }),
)
return Object.entries(AST_NODE_TARGETS_BY_LANGUAGE[lang] ?? {}).map(([value, entry]) => ({
label: entry.label,
value,
}))
}
// 逻辑名 and/or/not 在 C 里显示成 && / || / !,存进去的还是逻辑名
function operatorTargetOptions(lang: string): SelectOption[] {
return Object.entries(AST_OPERATOR_TARGETS_BY_LANGUAGE[lang] ?? {}).map(
([value, label]) => ({
label: label === value ? value : `${label}(${value})`,
value,
}),
)
return Object.entries(AST_OPERATOR_TARGETS_BY_LANGUAGE[lang] ?? {}).map(([value, label]) => ({
label: label === value ? value : `${label}(${value})`,
value,
}))
}
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"]
@@ -171,13 +166,8 @@ function updateRules(lang: string, rules: AstRule[]) {
emit("update:modelValue", Object.keys(current).length > 0 ? current : null)
}
function getTargetLabel(
lang: string,
engine: string,
target: string,
): string | undefined {
if (isNodeEngine(engine))
return AST_NODE_TARGETS_BY_LANGUAGE[lang]?.[target]?.label
function getTargetLabel(lang: string, engine: string, target: string): string | undefined {
if (isNodeEngine(engine)) return AST_NODE_TARGETS_BY_LANGUAGE[lang]?.[target]?.label
// 运算符不写 label:判题结果的文案按语言翻译(astOperatorLabel),
// 存一个固定 label 反而会把 C 的 && 钉死成 and
return undefined
@@ -260,17 +250,8 @@ watch(supportedLanguages, (langs) => {
暂不支持代码规则检查,判题机只能检查
{{ AST_SUPPORTED_LANGUAGES.join(" / ") }}
</n-alert>
<n-tabs
v-if="supportedLanguages.length"
type="segment"
v-model:value="activeTab"
>
<n-tab-pane
v-for="lang in supportedLanguages"
:key="lang"
:name="lang"
:tab="lang"
>
<n-tabs v-if="supportedLanguages.length" type="segment" v-model:value="activeTab">
<n-tab-pane v-for="lang in supportedLanguages" :key="lang" :name="lang" :tab="lang">
<n-flex vertical>
<div
v-for="(rule, index) in getRulesForLang(lang)"
@@ -281,9 +262,7 @@ watch(supportedLanguages, (langs) => {
<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"
/>
@@ -291,9 +270,7 @@ watch(supportedLanguages, (langs) => {
v-if="needsTargetDropdown(rule.engine)"
:options="nodeTargetOptions(lang)"
: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
@@ -301,9 +278,7 @@ watch(supportedLanguages, (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"
@@ -312,9 +287,7 @@ watch(supportedLanguages, (langs) => {
v-if="needsOperatorDropdown(rule.engine)"
:options="operatorTargetOptions(lang)"
: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"
/>
@@ -322,18 +295,14 @@ watch(supportedLanguages, (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"
@@ -343,9 +312,7 @@ watch(supportedLanguages, (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"
@@ -354,9 +321,7 @@ watch(supportedLanguages, (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"
@@ -367,29 +332,17 @@ watch(supportedLanguages, (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>
@@ -397,9 +350,7 @@ watch(supportedLanguages, (langs) => {
</n-tabs>
<n-empty
v-else
:description="
languages.length ? '当前语言不支持代码规则检查' : '请先选择编程语言'
"
:description="languages.length ? '当前语言不支持代码规则检查' : '请先选择编程语言'"
/>
</n-collapse-item>
</n-collapse>
@@ -20,9 +20,7 @@ const tags = ref<AdminTag[]>([])
const selected = ref<string[]>([])
const newTags = ref<string[]>([])
const title = computed(() =>
props.action === "add" ? "批量添加标签" : "批量移除标签",
)
const title = computed(() => (props.action === "add" ? "批量添加标签" : "批量移除标签"))
const selectedSet = computed(() => new Set(selected.value))
@@ -53,11 +51,7 @@ async function submit() {
message.error("请先选择标签")
return
}
const res = await batchTagProblems(
props.problemIds,
names.value,
props.action,
)
const res = await batchTagProblems(props.problemIds, names.value, props.action)
const verb = props.action === "add" ? "添加" : "移除"
message.success(`已为 ${res.problemCount} 道题${verb} ${res.tagCount} 个标签`)
close()
@@ -40,9 +40,7 @@ function blankEntry(): ScriptEntry {
const scripts = ref<ScriptEntry[]>([blankEntry(), blankEntry(), blankEntry()])
const refSQL = computed(
() =>
props.answers.find((a) => a.language === "SQL" && a.code.trim())?.code ??
"",
() => props.answers.find((a) => a.language === "SQL" && a.code.trim())?.code ?? "",
)
/** 已有脚本读取失败时的提示。空白编辑器和「本来就没测试点」长得一样,必须区分开 */
@@ -54,9 +52,7 @@ const isGenerating = ref(false)
const hasAnyScript = computed(() => scripts.value.some((s) => s.sql.trim()))
const hasBlankScript = computed(() => scripts.value.some((s) => !s.sql.trim()))
const filledCount = computed(
() => scripts.value.filter((s) => s.sql.trim()).length,
)
const filledCount = computed(() => scripts.value.filter((s) => s.sql.trim()).length)
const canUpload = computed(() => {
const filled = scripts.value.filter((s) => s.sql.trim())
@@ -206,29 +202,15 @@ async function upload() {
<template>
<n-flex vertical>
<n-alert
v-if="!refSQL"
type="warning"
:show-icon="false"
style="margin-bottom: 8px"
>
<n-alert v-if="!refSQL" type="warning" :show-icon="false" style="margin-bottom: 8px">
还没有填写 SQL 标准答案,请先在上方"本题参考答案"中填写,再来编写测试点
</n-alert>
<n-alert
v-if="loadError"
type="error"
:show-icon="false"
style="margin-bottom: 8px"
>
<n-alert v-if="loadError" type="error" :show-icon="false" style="margin-bottom: 8px">
{{ loadError }}
</n-alert>
<n-flex align="center" wrap>
<n-button :disabled="isPreviewing || isGenerating" @click="reset">
清空
</n-button>
<n-button :disabled="isPreviewing || isGenerating" @click="add">
+1
</n-button>
<n-button :disabled="isPreviewing || isGenerating" @click="reset"> 清空 </n-button>
<n-button :disabled="isPreviewing || isGenerating" @click="add"> +1 </n-button>
<n-tooltip :disabled="!!refSQL && hasBlankScript">
<template #trigger>
<span>
@@ -297,9 +279,7 @@ async function upload() {
placeholder="-- 本测试点的建表 + 插入数据脚本
CREATE TABLE ...;
INSERT INTO ...;"
:status="
s.error ? 'error' : s.display && !s.stale ? 'success' : undefined
"
:status="s.error ? 'error' : s.display && !s.stale ? 'success' : undefined"
/>
<n-alert v-if="s.error" type="error" :show-icon="false">
{{ s.error }}
@@ -32,21 +32,15 @@ const columns: DataTableColumn<AdminProblemRow>[] = [
key: "title",
minWidth: 200,
render: (row) =>
h(
NButton,
{ text: true, type: "primary", onClick: () => goEdit(row) },
() => row.title,
),
h(NButton, { text: true, type: "primary", onClick: () => goEdit(row) }, () => row.title),
},
{
title: "可见",
key: "visible",
width: 80,
render: (row) =>
h(
NTag,
{ size: "small", type: row.visible ? "success" : "default" },
() => (row.visible ? "公开" : "隐藏"),
h(NTag, { size: "small", type: row.visible ? "success" : "default" }, () =>
row.visible ? "公开" : "隐藏",
),
},
{
@@ -54,25 +48,14 @@ const columns: DataTableColumn<AdminProblemRow>[] = [
key: "actions",
width: 110,
render: (row) =>
h(
NButton,
{ size: "small", type: "error", onClick: () => removeTag(row) },
() => "移除标签",
),
h(NButton, { size: "small", type: "error", onClick: () => removeTag(row) }, () => "移除标签"),
},
]
async function listProblems() {
if (page.value < 1) page.value = 1
const offset = (page.value - 1) * limit.value
const res = await getProblemList(
offset,
limit.value,
keyword.value,
"",
undefined,
props.tagId,
)
const res = await getProblemList(offset, limit.value, keyword.value, "", undefined, props.tagId)
problems.value = res.results
total.value = res.total
}
@@ -54,15 +54,11 @@ const availableLanguages = computed(() =>
props.answers.map((a) => ({ label: a.language, value: a.language })),
)
const hasAnyAnswerCode = computed(() =>
props.answers.some((a) => a.code.trim()),
)
const hasAnyAnswerCode = computed(() => props.answers.some((a) => a.code.trim()))
// 当前选中语言是否有答案代码(用于控制"先运行"按钮)
const hasAnswerCode = computed(() => {
const answer = props.answers.find(
(a) => a.language === selectedLanguage.value,
)
const answer = props.answers.find((a) => a.language === selectedLanguage.value)
return !!answer?.code.trim()
})
@@ -70,10 +66,7 @@ const hasAnswerCode = computed(() => {
watch(
availableLanguages,
(langs) => {
if (
langs.length &&
!langs.find((l) => l.value === selectedLanguage.value)
) {
if (langs.length && !langs.find((l) => l.value === selectedLanguage.value)) {
selectedLanguage.value = langs[0].value
}
},
@@ -136,9 +129,7 @@ function remove(index: number) {
}
async function run() {
const answer = props.answers.find(
(a) => a.language === selectedLanguage.value,
)
const answer = props.answers.find((a) => a.language === selectedLanguage.value)
if (!answer?.code.trim()) return
// 过滤没填过的行,去重(按输入内容)。全没填就留一行,当成无输入题跑一次 ——
@@ -210,12 +201,7 @@ async function upload() {
<template>
<n-flex vertical>
<n-alert
v-if="!hasAnyAnswerCode"
type="warning"
:show-icon="false"
style="margin-bottom: 8px"
>
<n-alert v-if="!hasAnyAnswerCode" type="warning" :show-icon="false" style="margin-bottom: 8px">
还没有填写答案代码,请先在上方"本题参考答案"中填写至少一种语言的答案,再来生成测试用例
</n-alert>
<n-flex align="center" wrap>
@@ -230,34 +216,19 @@ async function upload() {
<n-tooltip :disabled="hasAnswerCode">
<template #trigger>
<span>
<n-button
type="success"
:loading="isRunning"
:disabled="!hasAnswerCode"
@click="run"
>
<n-button type="success" :loading="isRunning" :disabled="!hasAnswerCode" @click="run">
先运行
</n-button>
</span>
</template>
请先在题目中填写答案代码
</n-tooltip>
<n-button
type="primary"
:loading="isUploading"
:disabled="!canUpload"
@click="upload"
>
<n-button type="primary" :loading="isUploading" :disabled="!canUpload" @click="upload">
上传
</n-button>
</n-flex>
<n-flex
v-for="(file, index) in files"
:key="file.id"
align="start"
style="gap: 8px"
>
<n-flex v-for="(file, index) in files" :key="file.id" align="start" style="gap: 8px">
<n-flex vertical style="flex: 1">
<span>{{ index + 1 }}.in</span>
<n-input type="textarea" v-model:value="file.in" :rows="3" />