新增 SQL 题型前端支持

- LANGUAGE 类型与六张语言映射表补 SQL;编辑器 SQLite 方言高亮
- 出题表单:SQL 语言选项、题型/行序配置卡片、数据脚本压缩包上传、标准答案必填校验
- 修复做题页语言回退硬编码 Python3 的问题(SQL 题会提交被拒)
- SQL 题隐藏自测猫/自测屏/复制到自测猫(外部运行器不支持 SQL)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 17:23:23 -06:00
parent 5867bbceed
commit 38500671af
11 changed files with 138 additions and 12 deletions

3
package-lock.json generated
View File

@@ -11,6 +11,7 @@
"@codemirror/autocomplete": "^6.20.3",
"@codemirror/lang-cpp": "^6.0.3",
"@codemirror/lang-python": "^6.2.1",
"@codemirror/lang-sql": "^6.10.0",
"@vue-flow/background": "^1.3.2",
"@vue-flow/controls": "^1.1.3",
"@vue-flow/core": "^1.48.2",
@@ -424,7 +425,7 @@
},
"node_modules/@codemirror/lang-sql": {
"version": "6.10.0",
"resolved": "https://registry.npmmirror.com/@codemirror/lang-sql/-/lang-sql-6.10.0.tgz",
"resolved": "https://registry.npmjs.org/@codemirror/lang-sql/-/lang-sql-6.10.0.tgz",
"integrity": "sha512-6ayPkEd/yRw0XKBx5uAiToSgGECo/GY2NoJIHXIIQh1EVwLuKoU8BP/qK0qH5NLXAbtJRLuT73hx7P9X34iO4w==",
"license": "MIT",
"dependencies": {

View File

@@ -13,6 +13,7 @@
"@codemirror/autocomplete": "^6.20.3",
"@codemirror/lang-cpp": "^6.0.3",
"@codemirror/lang-python": "^6.2.1",
"@codemirror/lang-sql": "^6.10.0",
"@vue-flow/background": "^1.3.2",
"@vue-flow/controls": "^1.1.3",
"@vue-flow/core": "^1.48.2",

View File

@@ -133,10 +133,13 @@ export async function uploadImage(file: File): Promise<string> {
return res.success ? res.file_path : ""
}
// 上传测试用例
export function uploadTestcases(file: File) {
// 上传测试用例SQL 题的压缩包是 1.sql..N.sql每个文件一个测试点的建表+数据脚本)
export function uploadTestcases(file: File, options: { sql?: boolean } = {}) {
const form = new window.FormData()
form.append("file", file)
if (options.sql) {
form.append("sql", "1")
}
return http.post<TestcaseUploadedReturns>("admin/test_case", form, {
headers: { "content-type": "multipart/form-data" },
})

View File

@@ -10,7 +10,13 @@ import {
} from "utils/constants"
import download from "utils/download"
import { unique } from "utils/functions"
import type { BlankProblem, LANGUAGE, Tag, Testcase } from "utils/types"
import type {
BlankProblem,
LANGUAGE,
SQLConfig,
Tag,
Testcase,
} from "utils/types"
import {
createContestProblem,
createProblem,
@@ -89,6 +95,7 @@ const problem = useLocalStorage<BlankProblem>(STORAGE_KEY.ADMIN_PROBLEM, {
flowchart_hint: "",
show_flowchart: false,
ast_rules: null as { [key: string]: any[] } | null,
sql_config: null as SQLConfig | null,
})
// 从服务器来的tag列表
@@ -171,8 +178,33 @@ const languageOptions = [
{ label: LANGUAGE_SHOW_VALUE["Python3"], value: "Python3" },
{ label: LANGUAGE_SHOW_VALUE["C"], value: "C" },
{ label: LANGUAGE_SHOW_VALUE["C++"], value: "C++" },
{ label: LANGUAGE_SHOW_VALUE["SQL"], value: "SQL" },
]
const isSQLProblem = computed(() => !!problem.value?.languages.includes("SQL"))
// SQL 题联动SQL 必须是唯一语言(后端强校验),不需要预制代码,自动初始化 sql_config
watch(
() => problem.value?.languages,
(langs) => {
if (!langs) return
if (langs.includes("SQL")) {
if (langs.length > 1) {
problem.value.languages = ["SQL"]
return
}
needTemplate.value = false
if (!problem.value.sql_config) {
problem.value.sql_config = { mode: "query", order_sensitive: false }
}
currentActiveAnswer.value = "SQL"
} else if (problem.value.sql_config) {
problem.value.sql_config = null
}
},
{ immediate: true },
)
async function getProblemDetail() {
if (!props.problemID) {
syncTagInputsFromProblemTags()
@@ -211,6 +243,7 @@ async function getProblemDetail() {
problem.value.flowchart_hint = data.flowchart_hint ?? ""
problem.value.flowchart_data = data.flowchart_data
problem.value.ast_rules = data.ast_rules ?? null
problem.value.sql_config = data.sql_config ?? null
if (data.answers && data.answers.length) {
problem.value.answers = data.answers
} else {
@@ -262,7 +295,7 @@ function resetTemplate(language: LANGUAGE) {
async function handleUploadTestcases({ file }: UploadCustomRequestOptions) {
try {
const res = await uploadTestcases(file.file!)
const res = await uploadTestcases(file.file!, { sql: isSQLProblem.value })
// @ts-ignore
if (res.error) {
message.error("上传测试用例失败")
@@ -332,6 +365,19 @@ async function validateProblem() {
message.error("编程语言没有选择")
hasErrors = true
}
// SQL 题验证
else if (isSQLProblem.value && !problem.value.sql_config?.mode) {
message.error("SQL 题需要选择题型(查询题/增删改题)")
hasErrors = true
} else if (
isSQLProblem.value &&
!problem.value.answers.find(
(ans) => ans.language === "SQL" && ans.code.trim() !== "",
)
) {
message.error("SQL 题必须填写标准答案(判题时用它生成期望结果)")
hasErrors = true
}
// 流程图验证
else if (problem.value.show_flowchart || problem.value.allow_flowchart) {
if (
@@ -611,7 +657,7 @@ watch(
</n-flex>
</n-checkbox-group>
</n-form-item>
<n-form-item>
<n-form-item v-if="!isSQLProblem">
<n-checkbox
v-model:checked="needTemplate"
label="预制代码(显示在编辑器中,帮助快速上手)"
@@ -630,10 +676,37 @@ watch(
</n-form-item>
</n-form>
<n-form
v-if="isSQLProblem && problem.sql_config"
inline
label-placement="left"
>
<n-form-item label="SQL 题型">
<n-radio-group v-model:value="problem.sql_config.mode">
<n-radio-button value="query">查询题比对查询结果</n-radio-button>
<n-radio-button value="modify">
增删改题比对执行后的表数据
</n-radio-button>
</n-radio-group>
</n-form-item>
<n-form-item label="严格比对行顺序">
<n-switch v-model:value="problem.sql_config.order_sensitive" />
<n-text depth="3" style="margin-left: 12px">
题目要求 ORDER BY 时开启关闭则按无序集合比对
</n-text>
</n-form-item>
</n-form>
<n-grid :cols="2" x-gap="20">
<n-gi>
<n-form>
<n-form-item label="本题参考答案(选填,用于 AI 分析,不会泄露)">
<n-form-item
:label="
isSQLProblem
? '标准答案(必填,判题依据:每个测试点会运行它生成期望结果)'
: '本题参考答案(选填,用于 AI 分析,不会泄露)'
"
>
<n-tabs
type="segment"
default-value="Python3"
@@ -695,7 +768,7 @@ watch(
<h2 class="title">测试用例区域</h2>
<n-flex align="center" style="margin-bottom: 12px">
<div>
<div v-if="!isSQLProblem">
<n-button type="success" @click="showGeneratorModal = true">
直接生成
</n-button>
@@ -706,14 +779,23 @@ watch(
accept=".zip"
:custom-request="handleUploadTestcases"
>
<n-button type="info">手动上传</n-button>
<n-button type="info">
{{ isSQLProblem ? "上传数据脚本压缩包" : "(老)手动上传" }}
</n-button>
</n-upload>
</div>
<n-tooltip placement="right">
<template #trigger>
<n-button text>温馨提醒</n-button>
</template>
<template v-if="isSQLProblem">
压缩包内放 1.sql2.sql每个文件是一个测试点的建表+插入数据脚本
判题时对每个测试点分别运行标准答案和学生 SQL 比对结果 建议至少 2-3
个数据不同的测试点防止学生硬编码答案
</template>
<template v-else>
测试用例最好要有10个要考虑边界情况且不要跟测试样例一模一样
</template>
</n-tooltip>
</n-flex>

View File

@@ -140,7 +140,8 @@ defineExpose({
onMounted(() => {
if (!languages.value.includes(codeStore.code.language)) {
codeStore.code.language = "Python3"
// 回退到题目支持的第一种语言(如 SQL 题只有 "SQL",硬编码 Python3 会被后端拒绝)
codeStore.code.language = languages.value[0] ?? "Python3"
}
})
</script>
@@ -175,6 +176,7 @@ onMounted(() => {
<template v-if="codeStore.code.language !== 'Flowchart'">
<IconButton
v-if="codeStore.code.language !== 'SQL'"
icon="streamline-ultimate-color:business-lucky-cat"
tip="自测猫"
@click="goTestCat"

View File

@@ -130,6 +130,16 @@ onBeforeUnmount(() => {
watch(isMobile, (value) => {
if (value) screenModeStore.resetScreenMode()
})
// SQL 题不支持"自测"模式(外部代码运行器无法执行 SQL切到该屏时自动跳到下一模式
watch(
() => screenModeStore.isCodeOnlyMode,
(codeOnly) => {
if (codeOnly && problem.value?.languages.includes("SQL")) {
screenModeStore.switchScreenMode()
}
},
)
</script>
<template>

View File

@@ -127,7 +127,13 @@ onMounted(init)
</n-flex>
</n-alert>
<n-flex :vertical="isDesktop" justify="center">
<n-button secondary @click="copyToCat">复制到自测猫</n-button>
<n-button
v-if="submission.language !== 'SQL'"
secondary
@click="copyToCat"
>
复制到自测猫
</n-button>
<n-button secondary @click="copyToProblem">复制回到题目</n-button>
</n-flex>
</n-flex>

View File

@@ -1,6 +1,7 @@
<script lang="ts" setup>
import { cpp } from "@codemirror/lang-cpp"
import { python } from "@codemirror/lang-python"
import { sql, SQLite } from "@codemirror/lang-sql"
import { bracketMatching } from "@codemirror/language"
import { EditorView } from "@codemirror/view"
import { Codemirror } from "vue-codemirror"
@@ -57,6 +58,8 @@ const emit = defineEmits<{
const { isDesktop } = useBreakpoints()
const langExtension = computed((): Extension => {
if (language === "SQL")
return sql({ dialect: SQLite, upperCaseKeywords: true })
return ["Python2", "Python3"].includes(language) ? python() : cpp()
})

View File

@@ -27,6 +27,9 @@ export function enhanceCompletion(language: LANGUAGE): CompletionSource {
const word = context.matchBefore(/\w+/)
if (!word && !context.explicit) return null
// SQL 没有中文注释提示,关键字补全由 @codemirror/lang-sql 提供
if (language === "SQL") return null
const trulyLanguage = language.startsWith("Python") ? "python" : "c"
const completions: Completion[] = (
chineseAnnotations[trulyLanguage] || []

View File

@@ -175,6 +175,7 @@ export const SOURCES = {
JavaScript: "",
Golang: "",
Flowchart: "",
SQL: "",
} as const
export const LANGUAGE_ID = {
@@ -186,6 +187,7 @@ export const LANGUAGE_ID = {
JavaScript: 0,
Golang: 0,
Flowchart: 0,
SQL: 0,
} as const
export const LANGUAGE_FORMAT_VALUE = {
@@ -197,6 +199,7 @@ export const LANGUAGE_FORMAT_VALUE = {
JavaScript: "javascript",
Golang: "go",
Flowchart: "flowchart",
SQL: "sql",
} as const
export const LANGUAGE_SHOW_VALUE = {
@@ -208,6 +211,7 @@ export const LANGUAGE_SHOW_VALUE = {
Python3: "Python",
JavaScript: "JS",
Golang: "Go",
SQL: "SQL",
} as const
export const ICON_SET = {
@@ -219,6 +223,7 @@ export const ICON_SET = {
Java: "devicon:java",
JavaScript: "devicon:javascript",
Golang: "devicon:go",
SQL: "devicon:sqlite",
} as const
const cTemplate = `//TEMPLATE BEGIN
@@ -256,6 +261,7 @@ export const CODE_TEMPLATES = {
JavaScript: blankTemplate,
Golang: blankTemplate,
Flowchart: blankTemplate,
SQL: blankTemplate,
} as const
export enum ScreenMode {

View File

@@ -65,6 +65,12 @@ export type LANGUAGE =
| "JavaScript"
| "Golang"
| "Flowchart"
| "SQL"
export interface SQLConfig {
mode: "query" | "modify"
order_sensitive: boolean
}
export type LANGUAGE_SHOW_LABEL =
(typeof LANGUAGE_SHOW_VALUE)[keyof typeof LANGUAGE_SHOW_VALUE]
@@ -164,6 +170,9 @@ export interface Problem {
}[]
} | null
has_ast_rules?: boolean
// SQL 题配置(非 SQL 题为 null
sql_config?: SQLConfig | null
}
export type AdminProblem = Problem & AlterProblem