feat(后台): SQL 题有了自己的出题页——左边写,右边就是学生看到的样子,改答案、改数据马上重跑

- 「新建题目」先选编程题还是 SQL 题,选了就不能改;编辑时按题目本身进对应的页
- SQL 出题页(设计稿「SQL 出题页」方案 B):测试数据一组一行,改完自动重跑,
  跑不通的用中文说原因;保存时才打包上传测试点,不用再手动「预览」「上传」
- 新建时存草稿,误关页面不丢
- 编程题出题页去掉 SQL 那一堆分支(以前勾上 SQL 会静默清掉语法要求和流程图)
- 题目列表能按题型筛,SQL 题标题后面挂标签

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-09 18:59:15 +08:00
1 parent 25d3296410
commit 2cb39fd96b
13 files changed
+1414 -185

No files matched your search

+38 -9
View File
@@ -13,7 +13,9 @@ import { useRouteQuery } from "@vueuse/router"
import AuthorSelect from "shared/components/AuthorSelect.vue"
import type { DataTableRowKey } from "naive-ui"
import BatchTagModal from "./components/BatchTagModal.vue"
import CreateProblemMenu from "./components/CreateProblemMenu.vue"
import PageHeader from "admin/components/PageHeader.vue"
import ProblemTypeTag from "oj/problem/components/ProblemTypeTag.vue"
interface Props {
contestID?: string
@@ -72,18 +74,31 @@ const nextDisplayID = computed(() => {
interface ProblemQuery {
keyword: string
author: string
kind: "" | "code" | "sql"
}
// 使用分页 composable
const { query, clearQuery } = usePagination<ProblemQuery>({
keyword: useRouteQuery("keyword", "").value,
author: useRouteQuery("author", "").value,
kind: useRouteQuery<ProblemQuery["kind"]>("kind", "").value,
})
const baseColumns: DataTableColumn<AdminProblemRow>[] = [
{ title: "ID", key: "id", width: 100 },
{ title: "显示编号", key: "_id", width: 100 },
{ title: "标题", key: "title", minWidth: 200 },
{
title: "标题",
key: "title",
minWidth: 200,
render: (row) =>
row.isSql
? h(NFlex, { size: 6, align: "center", wrap: false }, () => [
h("span", row.title),
h(ProblemTypeTag, { kind: "sql" }),
])
: row.title,
},
{
title: "难度",
key: "difficulty",
@@ -194,6 +209,8 @@ async function listProblems() {
query.keyword,
query.author,
props.contestID,
undefined,
query.kind,
)
total.value = res.total
problems.value = res.results
@@ -209,10 +226,17 @@ async function toggleVisible(problemID: number) {
})
}
function createContestProblem() {
const kindOptions = [
{ label: "全部题型", value: "" },
{ label: "编程题", value: "code" },
{ label: "SQL 题", value: "sql" },
]
function createProblem(kind: string) {
router.push({
name: "admin contest problem create",
params: { contestID: props.contestID },
name: isContestProblemList.value ? "admin contest problem create" : "admin problem create",
params: isContestProblemList.value ? { contestID: props.contestID } : {},
query: kind === "sql" ? { type: "sql" } : {},
})
}
@@ -230,7 +254,7 @@ watchDebounced(() => query.keyword, listProblems, {
})
// 监听其他查询条件变化
watch(() => [query.page, query.limit, query.author], listProblems)
watch(() => [query.page, query.limit, query.author, query.kind], listProblems)
</script>
<template>
@@ -246,12 +270,10 @@ watch(() => [query.page, query.limit, query.author], listProblems)
添加标签({{ selectedProblemIds.length }})
</n-button>
</template>
<n-button type="primary" @click="$router.push({ name: 'admin problem create' })">
新建题目
</n-button>
<CreateProblemMenu label="新建题目" primary @select="createProblem" />
</template>
<template v-else>
<n-button @click="createContestProblem">新建比赛题目</n-button>
<CreateProblemMenu label="新建比赛题目" @select="createProblem" />
<n-button type="primary" @click="selectProblems">从题库中选择</n-button>
</template>
</template>
@@ -263,6 +285,13 @@ watch(() => [query.page, query.limit, query.author], listProblems)
style="width: 220px"
@clear="clearQuery"
/>
<n-select
v-if="!props.contestID"
v-model:value="query.kind"
:options="kindOptions"
aria-label="题型"
style="width: 120px"
/>
<n-flex v-if="!props.contestID" align="center" :size="8">
<n-text depth="3">出题人</n-text>
<AuthorSelect v-model:value="query.author" all />