新增 SQL 题型前端支持
- LANGUAGE 类型与六张语言映射表补 SQL;编辑器 SQLite 方言高亮 - 出题表单:SQL 语言选项、题型/行序配置卡片、数据脚本压缩包上传、标准答案必填校验 - 修复做题页语言回退硬编码 Python3 的问题(SQL 题会提交被拒) - SQL 题隐藏自测猫/自测屏/复制到自测猫(外部运行器不支持 SQL) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
3
package-lock.json
generated
3
package-lock.json
generated
@@ -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": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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" },
|
||||
})
|
||||
|
||||
@@ -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.sql、2.sql…(每个文件是一个测试点的建表+插入数据脚本,
|
||||
判题时对每个测试点分别运行标准答案和学生 SQL 比对结果)。 建议至少 2-3
|
||||
个数据不同的测试点,防止学生硬编码答案
|
||||
</template>
|
||||
<template v-else>
|
||||
【测试用例】最好要有10个,要考虑边界情况,且不要跟【测试样例】一模一样
|
||||
</template>
|
||||
</n-tooltip>
|
||||
</n-flex>
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
|
||||
@@ -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] || []
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user