1 parent
23844ae301
commit
d25b5dbe6d
268 files changed
+3684
-8861
No files matched your search
@@ -14,15 +14,7 @@ import {
|
||||
import { getTopACTrend } from "admin/api"
|
||||
import { zonedYear } from "utils/functions"
|
||||
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
Filler,
|
||||
LinearScale,
|
||||
LineElement,
|
||||
PointElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
)
|
||||
ChartJS.register(CategoryScale, Filler, LinearScale, LineElement, PointElement, Title, Tooltip)
|
||||
|
||||
type ProblemTrend = AcTrend
|
||||
|
||||
@@ -161,10 +153,7 @@ onMounted(fetchData)
|
||||
<n-tag type="info" size="small">共 {{ data.length }} 题</n-tag>
|
||||
</n-space>
|
||||
<n-spin :show="loading">
|
||||
<div
|
||||
v-if="!loading && data.length === 0"
|
||||
style="text-align: center; padding: 40px"
|
||||
>
|
||||
<div v-if="!loading && data.length === 0" style="text-align: center; padding: 40px">
|
||||
暂无数据
|
||||
</div>
|
||||
<div v-else class="grid">
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -6,11 +6,7 @@ import TextEditor from "shared/components/TextEditor.vue"
|
||||
import TestcaseGenerator from "./components/TestcaseGenerator.vue"
|
||||
import SQLTestcaseEditor from "./components/SQLTestcaseEditor.vue"
|
||||
import AstRulesEditor from "./components/AstRulesEditor.vue"
|
||||
import {
|
||||
CODE_TEMPLATES,
|
||||
LANGUAGE_SHOW_VALUE,
|
||||
STORAGE_KEY,
|
||||
} from "utils/constants"
|
||||
import { CODE_TEMPLATES, LANGUAGE_SHOW_VALUE, STORAGE_KEY } from "utils/constants"
|
||||
import download from "utils/download"
|
||||
import { unique } from "utils/functions"
|
||||
import type { BlankProblem, LANGUAGE, Tag, Testcase } from "utils/types"
|
||||
@@ -23,13 +19,9 @@ import {
|
||||
uploadTestcases,
|
||||
} from "../api"
|
||||
|
||||
const CodeEditor = defineAsyncComponent(
|
||||
() => import("shared/components/CodeEditor.vue"),
|
||||
)
|
||||
const CodeEditor = defineAsyncComponent(() => import("shared/components/CodeEditor.vue"))
|
||||
|
||||
const MermaidEditor = defineAsyncComponent(
|
||||
() => import("shared/components/MermaidEditor.vue"),
|
||||
)
|
||||
const MermaidEditor = defineAsyncComponent(() => import("shared/components/MermaidEditor.vue"))
|
||||
|
||||
interface Props {
|
||||
problemID?: string
|
||||
@@ -349,8 +341,7 @@ async function validateProblem() {
|
||||
// 题目
|
||||
else if (
|
||||
!problem.value.description ||
|
||||
(!isSQLProblem.value &&
|
||||
(!problem.value.inputDescription || !problem.value.outputDescription))
|
||||
(!isSQLProblem.value && (!problem.value.inputDescription || !problem.value.outputDescription))
|
||||
) {
|
||||
message.error("题目或输入或输出没有填写")
|
||||
hasErrors = true
|
||||
@@ -363,9 +354,7 @@ async function validateProblem() {
|
||||
// 样例是空的
|
||||
else if (
|
||||
!isSQLProblem.value &&
|
||||
problem.value.samples.some(
|
||||
(sample) => sample.output === "" || sample.input === "",
|
||||
)
|
||||
problem.value.samples.some((sample) => sample.output === "" || sample.input === "")
|
||||
) {
|
||||
message.error("空样例没有删干净")
|
||||
hasErrors = true
|
||||
@@ -384,9 +373,7 @@ async function validateProblem() {
|
||||
hasErrors = true
|
||||
} else if (
|
||||
isSQLProblem.value &&
|
||||
!problem.value.answers.find(
|
||||
(ans) => ans.language === "SQL" && ans.code.trim() !== "",
|
||||
)
|
||||
!problem.value.answers.find((ans) => ans.language === "SQL" && ans.code.trim() !== "")
|
||||
) {
|
||||
message.error("SQL 题必须填写标准答案(判题时用它生成期望结果)")
|
||||
hasErrors = true
|
||||
@@ -430,9 +417,7 @@ function filterHint() {
|
||||
}
|
||||
|
||||
function filterAnswers() {
|
||||
problem.value.answers = problem.value.answers.filter(
|
||||
(ans) => ans.code.trim() !== "",
|
||||
)
|
||||
problem.value.answers = problem.value.answers.filter((ans) => ans.code.trim() !== "")
|
||||
}
|
||||
|
||||
function filterSamplesForSQL() {
|
||||
@@ -465,16 +450,10 @@ async function submit() {
|
||||
problem.value = null
|
||||
selectedTags.value = []
|
||||
newTags.value = []
|
||||
if (
|
||||
route.name === "admin problem create" ||
|
||||
route.name === "admin contest problem create"
|
||||
) {
|
||||
if (route.name === "admin problem create" || route.name === "admin contest problem create") {
|
||||
message.success("恭喜你 💐 出题成功")
|
||||
}
|
||||
if (
|
||||
route.name === "admin problem create" ||
|
||||
route.name === "admin problem edit"
|
||||
) {
|
||||
if (route.name === "admin problem create" || route.name === "admin problem edit") {
|
||||
router.push({ name: "admin problem list" })
|
||||
} else {
|
||||
router.push({
|
||||
@@ -492,9 +471,7 @@ async function submit() {
|
||||
}
|
||||
|
||||
const showClear = computed(
|
||||
() =>
|
||||
route.name === "admin problem create" ||
|
||||
route.name === "admin contest problem create",
|
||||
() => route.name === "admin problem create" || route.name === "admin contest problem create",
|
||||
)
|
||||
|
||||
function clear() {
|
||||
@@ -523,10 +500,7 @@ async function generateMermaid() {
|
||||
|
||||
const showGeneratorModal = ref(false)
|
||||
|
||||
function handleTestcasesGenerated(
|
||||
testCaseId: string,
|
||||
testCaseScore: Testcase[],
|
||||
) {
|
||||
function handleTestcasesGenerated(testCaseId: string, testCaseScore: Testcase[]) {
|
||||
problem.value.testCaseId = testCaseId
|
||||
problem.value.testCaseScore = testCaseScore
|
||||
showGeneratorModal.value = false
|
||||
@@ -545,9 +519,7 @@ watch(
|
||||
() => problem.value.languages,
|
||||
(langs) => {
|
||||
const answers = langs.map((lang) => {
|
||||
const existing = problem.value.answers.find(
|
||||
(ans) => ans.language === lang,
|
||||
)
|
||||
const existing = problem.value.answers.find((ans) => ans.language === lang)
|
||||
return existing || { language: lang, code: "" }
|
||||
})
|
||||
problem.value.answers = answers
|
||||
@@ -569,11 +541,7 @@ watch(
|
||||
<n-input class="problemTitleInput" v-model:value="problem.title" />
|
||||
</n-form-item>
|
||||
<n-form-item label="难度">
|
||||
<n-select
|
||||
class="w-100"
|
||||
:options="difficultyOptions"
|
||||
v-model:value="problem.difficulty"
|
||||
/>
|
||||
<n-select class="w-100" :options="difficultyOptions" v-model:value="problem.difficulty" />
|
||||
</n-form-item>
|
||||
<n-form-item label="可见">
|
||||
<n-switch v-model:value="problem.visible" />
|
||||
@@ -621,12 +589,7 @@ watch(
|
||||
<div class="box" v-for="(sample, index) in problem.samples" :key="index">
|
||||
<n-flex justify="space-between" align="center">
|
||||
<strong>测试样例 {{ index + 1 }}</strong>
|
||||
<n-button
|
||||
tertiary
|
||||
type="warning"
|
||||
size="small"
|
||||
@click="removeSample(index)"
|
||||
>
|
||||
<n-button tertiary type="warning" size="small" @click="removeSample(index)">
|
||||
删除 {{ index + 1 }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
@@ -658,10 +621,7 @@ watch(
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="本题的考察知识点(选填,用于 AI 分析)">
|
||||
<n-input
|
||||
v-model:value="problem.prompt"
|
||||
placeholder="比如考察选择、循环、算法等知识点"
|
||||
/>
|
||||
<n-input v-model:value="problem.prompt" placeholder="比如考察选择、循环、算法等知识点" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
|
||||
@@ -683,10 +643,7 @@ watch(
|
||||
</n-checkbox-group>
|
||||
</n-form-item>
|
||||
<n-form-item v-if="!isSQLProblem">
|
||||
<n-checkbox
|
||||
v-model:checked="needTemplate"
|
||||
label="预制代码(显示在编辑器中,帮助快速上手)"
|
||||
/>
|
||||
<n-checkbox v-model:checked="needTemplate" label="预制代码(显示在编辑器中,帮助快速上手)" />
|
||||
</n-form-item>
|
||||
<n-form-item>
|
||||
<n-button
|
||||
@@ -701,17 +658,11 @@ watch(
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
|
||||
<n-form
|
||||
v-if="isSQLProblem && problem.sqlConfig"
|
||||
inline
|
||||
label-placement="left"
|
||||
>
|
||||
<n-form v-if="isSQLProblem && problem.sqlConfig" inline label-placement="left">
|
||||
<n-form-item label="SQL 题型">
|
||||
<n-radio-group v-model:value="problem.sqlConfig.mode">
|
||||
<n-radio-button value="query">查询题(比对查询结果)</n-radio-button>
|
||||
<n-radio-button value="modify">
|
||||
增删改题(比对执行后的表数据)
|
||||
</n-radio-button>
|
||||
<n-radio-button value="modify"> 增删改题(比对执行后的表数据) </n-radio-button>
|
||||
</n-radio-group>
|
||||
</n-form-item>
|
||||
<n-form-item label="严格比对行顺序">
|
||||
@@ -732,11 +683,7 @@ watch(
|
||||
: '本题参考答案(选填,用于 AI 分析,不会泄露)'
|
||||
"
|
||||
>
|
||||
<n-tabs
|
||||
type="segment"
|
||||
default-value="Python"
|
||||
v-model:value="currentActiveAnswer"
|
||||
>
|
||||
<n-tabs type="segment" default-value="Python" v-model:value="currentActiveAnswer">
|
||||
<n-tab-pane
|
||||
v-for="(answer, index) in problem.answers"
|
||||
:key="index"
|
||||
@@ -756,16 +703,8 @@ watch(
|
||||
<n-gi>
|
||||
<n-form v-if="needTemplate">
|
||||
<n-form-item label="编写预制代码">
|
||||
<n-tabs
|
||||
type="segment"
|
||||
default-value="Python"
|
||||
v-model:value="currentActiveTemplate"
|
||||
>
|
||||
<n-tab-pane
|
||||
v-for="(lang, index) in problem.languages"
|
||||
:key="index"
|
||||
:name="lang"
|
||||
>
|
||||
<n-tabs type="segment" default-value="Python" v-model:value="currentActiveTemplate">
|
||||
<n-tab-pane v-for="(lang, index) in problem.languages" :key="index" :name="lang">
|
||||
<CodeEditor
|
||||
v-model:value="template[lang]"
|
||||
:language="lang"
|
||||
@@ -781,10 +720,7 @@ watch(
|
||||
|
||||
<n-grid v-if="!isSQLProblem" :cols="2">
|
||||
<n-gi :span="1">
|
||||
<AstRulesEditor
|
||||
v-model="problem.astRules!"
|
||||
:languages="problem.languages"
|
||||
/>
|
||||
<AstRulesEditor v-model="problem.astRules!" :languages="problem.languages" />
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
|
||||
@@ -794,16 +730,10 @@ watch(
|
||||
|
||||
<n-flex v-if="!isSQLProblem" align="center" style="margin-bottom: 12px">
|
||||
<div>
|
||||
<n-button type="success" @click="showGeneratorModal = true">
|
||||
(新)直接生成
|
||||
</n-button>
|
||||
<n-button type="success" @click="showGeneratorModal = true"> (新)直接生成 </n-button>
|
||||
</div>
|
||||
<div>
|
||||
<n-upload
|
||||
:show-file-list="false"
|
||||
accept=".zip"
|
||||
:custom-request="handleUploadTestcases"
|
||||
>
|
||||
<n-upload :show-file-list="false" accept=".zip" :custom-request="handleUploadTestcases">
|
||||
<n-button type="info">(老)手动上传</n-button>
|
||||
</n-upload>
|
||||
</div>
|
||||
@@ -823,12 +753,7 @@ watch(
|
||||
@uploaded="handleTestcasesGenerated"
|
||||
/>
|
||||
|
||||
<n-alert
|
||||
class="box"
|
||||
v-if="problem.testCaseScore.length"
|
||||
:show-icon="false"
|
||||
type="info"
|
||||
>
|
||||
<n-alert class="box" v-if="problem.testCaseScore.length" :show-icon="false" type="info">
|
||||
<template #header>
|
||||
<n-flex align="center">
|
||||
<div>
|
||||
@@ -836,13 +761,7 @@ watch(
|
||||
{{ problem.testCaseScore.length }}
|
||||
条测试用例
|
||||
</div>
|
||||
<n-button
|
||||
v-if="problem.id"
|
||||
tertiary
|
||||
type="info"
|
||||
size="small"
|
||||
@click="downloadTestcases"
|
||||
>
|
||||
<n-button v-if="problem.id" tertiary type="info" size="small" @click="downloadTestcases">
|
||||
下载
|
||||
</n-button>
|
||||
</n-flex>
|
||||
@@ -875,10 +794,7 @@ watch(
|
||||
<n-button
|
||||
type="primary"
|
||||
size="small"
|
||||
:disabled="
|
||||
!problem.answers.filter((a) => a.language === 'Python')[0]?.code
|
||||
.length
|
||||
"
|
||||
:disabled="!problem.answers.filter((a) => a.language === 'Python')[0]?.code.length"
|
||||
:loading="isAIGenerating"
|
||||
@click="generateMermaid"
|
||||
>
|
||||
@@ -890,15 +806,8 @@ watch(
|
||||
</n-form-item>
|
||||
<n-form-item label="显示标准流程图">
|
||||
<n-flex align="center">
|
||||
<n-switch
|
||||
v-model:value="problem.showFlowchart"
|
||||
:disabled="problem.allowFlowchart"
|
||||
/>
|
||||
<n-text
|
||||
v-if="problem.allowFlowchart"
|
||||
depth="3"
|
||||
style="font-size: 12px"
|
||||
>
|
||||
<n-switch v-model:value="problem.showFlowchart" :disabled="problem.allowFlowchart" />
|
||||
<n-text v-if="problem.allowFlowchart" depth="3" style="font-size: 12px">
|
||||
让学生自己画图时,标准流程图不会下发给学生,这个开关没有意义
|
||||
</n-text>
|
||||
</n-flex>
|
||||
@@ -907,10 +816,7 @@ watch(
|
||||
|
||||
<n-form>
|
||||
<n-form-item>
|
||||
<MermaidEditor
|
||||
v-model="problem.mermaidCode"
|
||||
@render-state="onMermaidRenderState"
|
||||
/>
|
||||
<MermaidEditor v-model="problem.mermaidCode" @render-state="onMermaidRenderState" />
|
||||
</n-form-item>
|
||||
<n-form-item label="流程图提示信息(选填)">
|
||||
<n-input
|
||||
|
||||
@@ -29,9 +29,7 @@ const title = computed(
|
||||
"admin contest problem list": "比赛题目列表",
|
||||
})[route.name as string],
|
||||
)
|
||||
const isContestProblemList = computed(
|
||||
() => route.name === "admin contest problem list",
|
||||
)
|
||||
const isContestProblemList = computed(() => route.name === "admin contest problem list")
|
||||
|
||||
const [show, toggleShow] = useToggle()
|
||||
const { count, inc } = useCounter(0)
|
||||
@@ -42,9 +40,7 @@ const selectedRowKeys = ref<DataTableRowKey[]>([])
|
||||
const batchTagAction = ref<"add" | "remove">("add")
|
||||
const [showBatchTag, toggleBatchTag] = useToggle(false)
|
||||
|
||||
const selectedProblemIds = computed(() =>
|
||||
selectedRowKeys.value.map((key) => Number(key)),
|
||||
)
|
||||
const selectedProblemIds = computed(() => selectedRowKeys.value.map((key) => Number(key)))
|
||||
|
||||
const rowKey = (row: AdminProblemRow) => row.id
|
||||
|
||||
@@ -103,9 +99,7 @@ const baseColumns: DataTableColumn<AdminProblemRow>[] = [
|
||||
key: "tags",
|
||||
minWidth: 120,
|
||||
render: (row) =>
|
||||
h(NFlex, { size: 4 }, () =>
|
||||
row.tags.map((t) => h(NTag, { key: t, size: "small" }, () => t)),
|
||||
),
|
||||
h(NFlex, { size: 4 }, () => row.tags.map((t) => h(NTag, { key: t, size: "small" }, () => t))),
|
||||
},
|
||||
{
|
||||
title: "功能",
|
||||
@@ -254,16 +248,10 @@ watch(() => [query.page, query.limit, query.author], listProblems)
|
||||
>
|
||||
卡点分析
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="!isContestProblemList"
|
||||
@click="$router.push({ name: 'admin top ac trend' })"
|
||||
>
|
||||
<n-button v-if="!isContestProblemList" @click="$router.push({ name: 'admin top ac trend' })">
|
||||
年度趋势
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="!isContestProblemList"
|
||||
@click="$router.push({ name: 'admin tag list' })"
|
||||
>
|
||||
<n-button v-if="!isContestProblemList" @click="$router.push({ name: 'admin tag list' })">
|
||||
标签管理
|
||||
</n-button>
|
||||
</n-flex>
|
||||
@@ -274,14 +262,8 @@ watch(() => [query.page, query.limit, query.author], listProblems)
|
||||
</n-button>
|
||||
<n-button @click="openBatchTag('remove')">移除标签</n-button>
|
||||
</template>
|
||||
<n-button v-if="isContestProblemList" @click="createContestProblem">
|
||||
新建比赛题目
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="isContestProblemList"
|
||||
type="primary"
|
||||
@click="selectProblems"
|
||||
>
|
||||
<n-button v-if="isContestProblemList" @click="createContestProblem"> 新建比赛题目 </n-button>
|
||||
<n-button v-if="isContestProblemList" type="primary" @click="selectProblems">
|
||||
从题目中选择
|
||||
</n-button>
|
||||
<n-flex align="center" v-if="!props.contestID">
|
||||
@@ -305,11 +287,7 @@ watch(() => [query.page, query.limit, query.author], listProblems)
|
||||
:row-key="rowKey"
|
||||
@update:checked-row-keys="chooseProblems"
|
||||
/>
|
||||
<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" />
|
||||
<AddFromLibraryModal
|
||||
v-model:show="show"
|
||||
:count="count"
|
||||
|
||||
@@ -54,10 +54,8 @@ const columns: DataTableColumn<AdminTag>[] = [
|
||||
key: "problem_count",
|
||||
width: 100,
|
||||
render: (row) =>
|
||||
h(
|
||||
NButton,
|
||||
{ text: true, type: "primary", onClick: () => openTagProblems(row) },
|
||||
() => String(row.problemCount),
|
||||
h(NButton, { text: true, type: "primary", onClick: () => openTagProblems(row) }, () =>
|
||||
String(row.problemCount),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -76,11 +74,7 @@ const columns: DataTableColumn<AdminTag>[] = [
|
||||
h(NButton, { size: "small", onClick: cancelEdit }, () => "取消"),
|
||||
]
|
||||
: [
|
||||
h(
|
||||
NButton,
|
||||
{ size: "small", onClick: () => startEdit(row) },
|
||||
() => "重命名",
|
||||
),
|
||||
h(NButton, { size: "small", onClick: () => startEdit(row) }, () => "重命名"),
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
@@ -153,16 +147,9 @@ watchDebounced(keyword, listTags, { debounce: 500, maxWait: 1000 })
|
||||
<n-flex class="titleWrapper" justify="space-between">
|
||||
<n-flex align="center">
|
||||
<h2 class="title">标签管理</h2>
|
||||
<n-button @click="$router.push({ name: 'admin problem list' })">
|
||||
返回题目列表
|
||||
</n-button>
|
||||
<n-button @click="$router.push({ name: 'admin problem list' })"> 返回题目列表 </n-button>
|
||||
</n-flex>
|
||||
<n-input
|
||||
v-model:value="keyword"
|
||||
style="width: 200px"
|
||||
placeholder="搜索标签"
|
||||
clearable
|
||||
/>
|
||||
<n-input v-model:value="keyword" style="width: 200px" placeholder="搜索标签" clearable />
|
||||
</n-flex>
|
||||
<n-data-table striped :columns="columns" :data="tags" />
|
||||
<TagProblemsModal
|
||||
|
||||
Reference in new issue
Block a user