Files
OJ2/apps/web/src/admin/problem/detail.vue
yuetsh ed56a209ea
Some checks failed
Deploy / deploy (push) Has been cancelled
chore(格式): Prettier 统一到全仓,后端和契约一次性格式化
原来只有 `apps/web` 在 Prettier 下(配置在 `apps/web/.prettierrc.toml`、脚本在
web 的 package.json),后端和契约从来没格式化过 —— 手写在 100 列上下,`db/schema.ts`
还是 drizzle-kit pull 留下的 tab 缩进。两套口径分叉久了,跨端改一处就得记着「这边
什么风格」。

- 配置搬到根目录 `.prettierrc.toml`,内容不变(`semi=false`,其余全默认,
  printWidth 80 —— 和前端已有的格式一致,不另立一套宽度);
- 脚本统一成根目录 `bun run fmt`,覆盖 `apps/*/src`、`apps/web/tests` 和两个构建
  配置;web 自己那份 `fmt` 和重复的 prettier 依赖删掉;
- `.prettierignore` 挡掉两类不该碰的:drizzle-kit 生成的 `src/db/meta/` 结构快照
  (它是 db:generate 的比对输入,只该由 drizzle-kit 写)、unplugin 每次 dev 都会
  重写的 `auto-imports.d.ts` / `components.d.ts`;
- 全量跑了一遍。纯格式,无行为改动:api typecheck / check:routes / check:ast、
  前端 type-check 全过,起 api 打了接口确认正常。前端这 39 个文件的小改动是
  prettier 版本漂移(类型断言的换行口径变了),不是新配置带来的。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 08:27:34 -06:00

951 lines
27 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { PROBLEM_TAG_MAX_LENGTH } from "@oj2/contract"
import { getProblemTagList } from "shared/api"
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 download from "utils/download"
import { unique } from "utils/functions"
import type { BlankProblem, LANGUAGE, Tag, Testcase } from "utils/types"
import {
createContestProblem,
createProblem,
editProblem,
generateFlowchartFromPythonCode,
getProblem,
uploadTestcases,
} from "../api"
const CodeEditor = defineAsyncComponent(
() => import("shared/components/CodeEditor.vue"),
)
const MermaidEditor = defineAsyncComponent(
() => import("shared/components/MermaidEditor.vue"),
)
interface Props {
problemID?: string
contestID?: string
}
const message = useMessage()
const route = useRoute()
const router = useRouter()
const props = defineProps<Props>()
const title = computed(
() =>
({
"admin problem create": "新建题目",
"admin problem edit": "编辑题目",
"admin contest problem create": "新建比赛题目",
"admin contest problem edit": "编辑比赛题目",
})[route.name as string],
)
const isAIGenerating = ref(false)
const problem = useLocalStorage<BlankProblem>(STORAGE_KEY.ADMIN_PROBLEM, {
_id: "",
title: "",
description: "",
inputDescription: "",
outputDescription: "",
timeLimit: 1000,
memoryLimit: 64,
difficulty: "Low",
visible: false,
tags: [],
languages: ["Python3", "C"] as LANGUAGE[],
template: {} as { [key in LANGUAGE]?: string },
samples: [
{ input: "", output: "" },
{ input: "", output: "" },
{ input: "", output: "" },
],
testCaseId: "",
testCaseScore: [] as Testcase[],
hint: "",
source: "",
prompt: "",
answers: [] as { language: LANGUAGE; code: string }[],
contestId: null,
allowFlowchart: false,
showFlowchart: false,
mermaidCode: "",
flowchartHint: "",
astRules: null,
sqlConfig: null,
sqlDisplay: null,
})
// 从服务器来的tag列表
const tagList = shallowRef<Tag[]>([])
const tagListLoaded = ref(false)
const selectedTags = ref<string[]>([])
const newTags = ref<string[]>([])
const selectedTagSet = computed(() => new Set(selectedTags.value))
let syncingTagInputs = false
function normalizeTagNames(tags: unknown): string[] {
if (!Array.isArray(tags)) return []
return unique(
tags
.map((tag) => (typeof tag === "string" ? tag : tag?.name))
.filter((tag): tag is string => !!tag),
)
}
function syncProblemTags() {
problem.value.tags = unique([...selectedTags.value, ...newTags.value])
}
function syncTagInputsFromProblemTags(tags: unknown = problem.value.tags) {
const tagNames = normalizeTagNames(tags)
const existingTagNames = new Set(tagList.value.map((tag) => tag.name))
syncingTagInputs = true
if (!tagListLoaded.value) {
selectedTags.value = tagNames
newTags.value = []
} else {
selectedTags.value = tagNames.filter((tag) => existingTagNames.has(tag))
newTags.value = tagNames.filter((tag) => !existingTagNames.has(tag))
}
syncingTagInputs = false
syncProblemTags()
}
function toggleTag(name: string) {
const set = new Set(selectedTags.value)
if (set.has(name)) set.delete(name)
else set.add(name)
selectedTags.value = Array.from(set)
}
function validateNewTags(v: string[]) {
const existing = new Set(tagList.value.map((t) => t.name))
const blanks: string[] = []
for (const tag of unique(v)) {
// 输入框已经挂了 maxlength这里兜住粘贴等绕过的情况。只丢这一个标签、
// 不像重复那支那样 break —— 后端撞上来只会回一句 zod 的英文
if (tag.length > PROBLEM_TAG_MAX_LENGTH) {
message.error(`标签最多 ${PROBLEM_TAG_MAX_LENGTH} 个字:` + tag)
continue
}
if (existing.has(tag)) {
message.error("已经存在标签:" + tag)
break
}
blanks.push(tag)
}
newTags.value = blanks
}
// 这几个用的少,就不缓存本地了
const [needTemplate, toggleNeedTemplate] = useToggle(false)
const template = reactive(JSON.parse(JSON.stringify(CODE_TEMPLATES)))
const currentActiveTemplate = ref<LANGUAGE>("Python3")
const currentActiveAnswer = ref<LANGUAGE>("Python3")
// 给 TextEditor 用
const [ready, toggleReady] = useToggle(false)
// Mermaid 渲染状态
const mermaidRenderSuccess = ref(false)
const difficultyOptions: SelectOption[] = [
{ label: "简单", value: "Low" },
{ label: "中等", value: "Mid" },
{ label: "困难", value: "High" },
]
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
// 两个流程图开关是互斥的allowFlowchart 为真时后端不会把 mermaidCode 下发给
// 学生showFlowchart 就成了一个点进去什么都没有的 tab。UI 上已经把开关置灰,
// 这里再把存量数据里两个都开着的情况纠正掉。
watch(
() => problem.value?.allowFlowchart,
(allow) => {
if (allow) problem.value.showFlowchart = false
},
)
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.sqlConfig) {
problem.value.sqlConfig = { mode: "query", order_sensitive: false }
}
currentActiveAnswer.value = "SQL"
// 代码规则检查基于 Python/C 的 AST 解析,对 SQL 没有意义,清空避免脏数据
if (problem.value.astRules) {
problem.value.astRules = null
}
// 流程图依赖 Python 答案生成,对 SQL 没有意义
problem.value.allowFlowchart = false
problem.value.showFlowchart = false
} else if (problem.value.sqlConfig) {
problem.value.sqlConfig = null
}
},
{ immediate: true },
)
async function getProblemDetail() {
if (!props.problemID) {
// 草稿缓存和编辑页共用同一个 localStorage key编辑页会把服务器数据连 id
// 一起写进去,没保存就离开的话,这个 id 会跟着草稿漂到新建页 ——「下载测试点」
// 下的是那道旧题的包SQL 测试点编辑器也会去回显那道旧题的脚本。
// 新建页不存在 id进来先摘掉。
delete problem.value.id
syncTagInputsFromProblemTags()
toggleReady(true)
return
}
try {
const data = await getProblem(props.problemID)
problem.value.id = data.id
problem.value._id = data._id
problem.value.title = data.title
problem.value.description = data.description
problem.value.inputDescription = data.inputDescription
problem.value.outputDescription = data.outputDescription
problem.value.timeLimit = data.timeLimit
problem.value.memoryLimit = data.memoryLimit
problem.value.memoryLimit = data.memoryLimit
problem.value.difficulty = data.difficulty
problem.value.visible = data.visible
problem.value.tags = normalizeTagNames(data.tags)
problem.value.languages = data.languages
problem.value.template = data.template
problem.value.samples = data.samples
problem.value.samples = data.samples
problem.value.testCaseId = data.testCaseId
problem.value.testCaseScore = data.testCaseScore
problem.value.hint = data.hint ?? ""
problem.value.source = data.source
problem.value.prompt = data.prompt
// 流程图相关字段
problem.value.allowFlowchart = data.allowFlowchart
problem.value.showFlowchart = data.showFlowchart
problem.value.mermaidCode = data.mermaidCode ?? ""
problem.value.flowchartHint = data.flowchartHint ?? ""
problem.value.astRules = data.astRules ?? null
problem.value.sqlConfig = data.sqlConfig ?? null
if (data.answers && data.answers.length) {
problem.value.answers = data.answers
} else {
problem.value.answers = data.languages.map((lang: LANGUAGE) => ({
language: lang,
code: "",
}))
}
if (problem.value.contestId) {
problem.value.contestId = problem.value.contestId
}
// 下面是用来显示的:
// 代码模板 和 模板开关
problem.value.languages.forEach((lang) => {
if (data.template[lang]) {
template[lang] = data.template[lang]
toggleNeedTemplate(true)
}
})
// 标签
syncTagInputsFromProblemTags(problem.value.tags)
toggleReady(true)
} catch (error) {
message.error("获取题目失败")
router.push({ name: "admin problem list" })
}
}
async function getTagList() {
const res = await getProblemTagList()
tagList.value = res
tagListLoaded.value = true
syncTagInputsFromProblemTags()
}
function addSample() {
problem.value.samples.push({ input: "", output: "" })
}
function removeSample(index: number) {
problem.value.samples.splice(index, 1)
}
function resetTemplate(language: LANGUAGE) {
template[language] = CODE_TEMPLATES[language]
}
async function handleUploadTestcases({ file }: UploadCustomRequestOptions) {
try {
// 失败走 catch —— 原来这里还有一句 `if (res.error)`(拿 { error, data }
// 信封当返回值),被 @ts-ignore 压着,信封拆掉之后就是一段死代码了
const res = await uploadTestcases(file.file!, { sql: isSQLProblem.value })
// score 不在上传响应里,前端按测试点数量平分补上
const entries = res.info
const testcases: Testcase[] = entries.map((entry) => ({
input_name: entry.input_name,
output_name: entry.output_name,
// 取值与原来的 `.toFixed(0)` 逐字相同,只是不再包成字符串
score: Number((100 / entries.length).toFixed(0)),
}))
problem.value.testCaseScore = testcases
problem.value.testCaseId = res.id
} catch (err) {
message.error("上传测试用例失败")
}
}
function downloadTestcases() {
download(`problems/${problem.value.id}/test-cases`)
}
// Mermaid 渲染事件处理。这里必须原样接受 false ——
// 原来只在成功时置 true、永不复位先写对再改坏就能把语法错误的代码存进库
function onMermaidRenderState(ok: boolean) {
mermaidRenderSuccess.value = ok
}
// 题目是否有漏写的
async function validateProblem() {
let hasErrors = false
// 标题
if (!problem.value._id || !problem.value.title) {
message.error("编号或标题没有填写")
hasErrors = true
}
// 标签
else if (selectedTags.value.length === 0 && newTags.value.length === 0) {
message.error("标签没有填写")
hasErrors = true
}
// 题目
else if (
!problem.value.description ||
(!isSQLProblem.value &&
(!problem.value.inputDescription || !problem.value.outputDescription))
) {
message.error("题目或输入或输出没有填写")
hasErrors = true
}
// 样例
else if (!isSQLProblem.value && problem.value.samples.length == 0) {
message.error("样例没有填写")
hasErrors = true
}
// 样例是空的
else if (
!isSQLProblem.value &&
problem.value.samples.some(
(sample) => sample.output === "" || sample.input === "",
)
) {
message.error("空样例没有删干净")
hasErrors = true
}
// 测试用例
else if (problem.value.testCaseScore.length === 0) {
message.error("测试用例没有上传")
hasErrors = true
} else if (problem.value.languages.length === 0) {
message.error("编程语言没有选择")
hasErrors = true
}
// SQL 题验证
else if (isSQLProblem.value && !problem.value.sqlConfig?.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.showFlowchart || problem.value.allowFlowchart) {
if (!problem.value.mermaidCode || problem.value.mermaidCode.trim() === "") {
message.error("启用了流程图功能,但流程图代码为空")
hasErrors = true
} else if (!mermaidRenderSuccess.value) {
message.error("Mermaid 代码尚未成功渲染,请检查代码语法")
hasErrors = true
}
}
// 通过了
else {
hasErrors = false
}
return hasErrors
}
function getTemplate() {
if (!needTemplate.value) {
problem.value.template = {}
} else {
problem.value.languages.forEach((lang) => {
if (CODE_TEMPLATES[lang] !== template[lang]) {
problem.value.template[lang] = template[lang]
} else {
delete problem.value.template[lang]
}
})
}
}
function filterHint() {
// 编辑器会自动添加一段 HTML
if (problem.value.hint === "<p><br></p>") {
problem.value.hint = ""
}
}
function filterAnswers() {
problem.value.answers = problem.value.answers.filter(
(ans) => ans.code.trim() !== "",
)
}
function filterSamplesForSQL() {
// SQL 题不展示样例;后端 CreateSampleSerializer 也不接受空字符串样例
if (isSQLProblem.value) problem.value.samples = []
}
async function submit() {
const hasValidationErrors = await validateProblem()
if (hasValidationErrors) return
filterHint()
getTemplate()
filterAnswers()
filterSamplesForSQL()
syncProblemTags()
const api = {
"admin problem create": createProblem,
"admin problem edit": editProblem,
"admin contest problem create": createContestProblem,
"admin contest problem edit": editProblem,
}[route.name as string]
if (
route.name === "admin contest problem create" ||
route.name === "admin contest problem edit"
) {
problem.value.contestId = Number(props.contestID)
}
try {
await api!(problem.value)
problem.value = null
selectedTags.value = []
newTags.value = []
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"
) {
router.push({ name: "admin problem list" })
} else {
router.push({
name: "admin contest problem list",
params: { contestID: props.contestID },
})
}
} catch (err: any) {
if (err.error === "display-id-exists") {
message.error("显示编号重复了,请换一个显示编号")
} else {
message.error(err.data)
}
}
}
const showClear = computed(
() =>
route.name === "admin problem create" ||
route.name === "admin contest problem create",
)
function clear() {
problem.value = null
selectedTags.value = []
newTags.value = []
// 为了给所有状态初始化,刷新页面
location.reload()
}
async function generateMermaid() {
isAIGenerating.value = true
try {
const res = await generateFlowchartFromPythonCode(
problem.value.answers.filter((a) => a.language === "Python3")[0].code,
)
problem.value.mermaidCode = res.flowchart
message.warning("如果渲染不成功,请复制到外部 AI 网站检查语法")
} catch (err: any) {
// 没有 finally 的话AI 接口回 502 时按钮永远卡在 loading只能刷页面
message.error(err.data || "生成失败,请稍后再试")
} finally {
isAIGenerating.value = false
}
}
const showGeneratorModal = ref(false)
function handleTestcasesGenerated(
testCaseId: string,
testCaseScore: Testcase[],
) {
problem.value.testCaseId = testCaseId
problem.value.testCaseScore = testCaseScore
showGeneratorModal.value = false
}
onMounted(() => {
getTagList()
getProblemDetail()
})
watch([selectedTags, newTags], ([sel, newT]) => {
if (syncingTagInputs) return
problem.value.tags = unique([...sel, ...newT])
})
watch(
() => problem.value.languages,
(langs) => {
const answers = langs.map((lang) => {
const existing = problem.value.answers.find(
(ans) => ans.language === lang,
)
return existing || { language: lang, code: "" }
})
problem.value.answers = answers
},
{ immediate: true },
)
</script>
<template>
<n-flex>
<h2 class="title">{{ title }}</h2>
<n-button v-if="showClear" @click="clear">清空缓存</n-button>
</n-flex>
<n-form inline label-placement="left">
<n-form-item label="显示编号">
<n-input class="w-100" v-model:value="problem._id" />
</n-form-item>
<n-form-item label="题目">
<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-form-item>
<n-form-item label="可见">
<n-switch v-model:value="problem.visible" />
</n-form-item>
</n-form>
<n-form label-placement="left" :show-feedback="false">
<n-form-item label="标签">
<n-flex vertical style="width: 100%">
<n-flex size="small" style="flex-wrap: wrap">
<n-tag
v-for="tag in tagList"
:key="tag.id"
checkable
:checked="selectedTagSet.has(tag.name)"
@update:checked="toggleTag(tag.name)"
>
{{ tag.name }}
</n-tag>
</n-flex>
<n-dynamic-tags
v-model:value="newTags"
:input-props="{ maxlength: PROBLEM_TAG_MAX_LENGTH }"
@update:value="validateNewTags"
/>
</n-flex>
</n-form-item>
</n-form>
<TextEditor
v-if="ready"
v-model:value="problem.description"
title="题目的描述"
:min-height="300"
/>
<TextEditor
v-if="ready && !isSQLProblem"
v-model:value="problem.inputDescription"
title="输入的描述"
/>
<TextEditor
v-if="ready && !isSQLProblem"
v-model:value="problem.outputDescription"
title="输出的描述"
/>
<template v-if="!isSQLProblem">
<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)"
>
删除 {{ index + 1 }}
</n-button>
</n-flex>
<n-grid x-gap="20" cols="2">
<n-gi span="1">
<n-flex vertical>
<span>输入样例</span>
<n-input type="textarea" v-model:value="sample.input" />
</n-flex>
</n-gi>
<n-gi span="1">
<n-flex vertical>
<span>输出样例</span>
<n-input type="textarea" v-model:value="sample.output" />
</n-flex>
</n-gi>
</n-grid>
</div>
<n-button class="addSamples box" tertiary type="primary" @click="addSample">
添加用例
</n-button>
</template>
<TextEditor v-if="ready" v-model:value="problem.hint" title="提示(选填)" />
<n-form>
<n-form-item label="题目的来源(选填)">
<n-input
v-model:value="problem.source"
placeholder="比如来自某道题的改编等,或者网上的资料"
/>
</n-form-item>
<n-form-item label="本题的考察知识点(选填,用于 AI 分析)">
<n-input
v-model:value="problem.prompt"
placeholder="比如考察选择、循环、算法等知识点"
/>
</n-form-item>
</n-form>
<n-divider />
<h2 class="title">代码区域</h2>
<n-form inline label-placement="left">
<n-form-item label="编程语言">
<n-checkbox-group v-model:value="problem.languages">
<n-flex align="center">
<n-checkbox
v-for="(language, index) in languageOptions"
:key="index"
:value="language.value"
:label="language.label"
/>
</n-flex>
</n-checkbox-group>
</n-form-item>
<n-form-item v-if="!isSQLProblem">
<n-checkbox
v-model:checked="needTemplate"
label="预制代码(显示在编辑器中,帮助快速上手)"
/>
</n-form-item>
<n-form-item>
<n-button
v-if="needTemplate"
size="small"
tertiary
type="warning"
@click="resetTemplate(currentActiveTemplate)"
>
重置 {{ LANGUAGE_SHOW_VALUE[currentActiveTemplate] }} 的预制代码
</n-button>
</n-form-item>
</n-form>
<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-group>
</n-form-item>
<n-form-item label="严格比对行顺序">
<n-switch v-model:value="problem.sqlConfig.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="
isSQLProblem
? '标准答案(必填,判题依据:每个测试点会运行它生成期望结果)'
: '本题参考答案(选填,用于 AI 分析,不会泄露)'
"
>
<n-tabs
type="segment"
default-value="Python3"
v-model:value="currentActiveAnswer"
>
<n-tab-pane
v-for="(answer, index) in problem.answers"
:key="index"
:name="answer.language"
>
<CodeEditor
v-model:value="answer.code"
:language="answer.language"
:font-size="16"
height="300px"
/>
</n-tab-pane>
</n-tabs>
</n-form-item>
</n-form>
</n-gi>
<n-gi>
<n-form v-if="needTemplate">
<n-form-item label="编写预制代码">
<n-tabs
type="segment"
default-value="Python3"
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"
:font-size="16"
height="300px"
/>
</n-tab-pane>
</n-tabs>
</n-form-item>
</n-form>
</n-gi>
</n-grid>
<n-grid v-if="!isSQLProblem" :cols="2">
<n-gi :span="1">
<AstRulesEditor
v-model="problem.astRules!"
:languages="problem.languages"
/>
</n-gi>
</n-grid>
<n-divider />
<h2 class="title">测试用例区域</h2>
<n-flex v-if="!isSQLProblem" align="center" style="margin-bottom: 12px">
<div>
<n-button type="success" @click="showGeneratorModal = true">
直接生成
</n-button>
</div>
<div>
<n-upload
:show-file-list="false"
accept=".zip"
:custom-request="handleUploadTestcases"
>
<n-button type="info">手动上传</n-button>
</n-upload>
</div>
<n-tooltip placement="right" style="max-width: 320px; white-space: normal">
<template #trigger>
<n-button text>温馨提醒</n-button>
</template>
测试用例最好要有10个要考虑边界情况且不要跟测试样例一模一样
</n-tooltip>
</n-flex>
<SQLTestcaseEditor
v-if="isSQLProblem"
:answers="problem.answers"
:mode="problem.sqlConfig?.mode ?? 'query'"
:problem-id="problem.id"
@uploaded="handleTestcasesGenerated"
/>
<n-alert
class="box"
v-if="problem.testCaseScore.length"
:show-icon="false"
type="info"
>
<template #header>
<n-flex align="center">
<div>
测试组编号 {{ problem.testCaseId.slice(0, 12) }} 共有
{{ problem.testCaseScore.length }}
条测试用例
</div>
<n-button
v-if="problem.id"
tertiary
type="info"
size="small"
@click="downloadTestcases"
>
下载
</n-button>
</n-flex>
</template>
</n-alert>
<n-modal
v-model:show="showGeneratorModal"
preset="card"
title="测试用例生成器"
style="width: 80vw; max-width: 900px"
:mask-closable="false"
display-directive="show"
>
<TestcaseGenerator
:answers="problem.answers"
:samples="problem.samples"
@uploaded="handleTestcasesGenerated"
/>
</n-modal>
<template v-if="!isSQLProblem">
<n-divider />
<h2 class="title">流程图区域</h2>
<!-- 流程图相关设置 -->
<n-form inline label-placement="left" :show-feedback="false">
<n-form-item label="根据上面的【Python答案】智能生成 Mermaid 代码">
<n-button
type="primary"
size="small"
:disabled="
!problem.answers.filter((a) => a.language === 'Python3')[0]?.code
.length
"
:loading="isAIGenerating"
@click="generateMermaid"
>
AI 生成
</n-button>
</n-form-item>
<n-form-item label="允许提交流程图">
<n-switch v-model:value="problem.allowFlowchart" />
</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-text>
</n-flex>
</n-form-item>
</n-form>
<n-form>
<n-form-item>
<MermaidEditor
v-model="problem.mermaidCode"
@render-state="onMermaidRenderState"
/>
</n-form-item>
<n-form-item label="流程图提示信息(选填)">
<n-input
v-model:value="problem.flowchartHint"
placeholder="请输入流程图相关的提示信息,帮助学生理解题目要求"
/>
</n-form-item>
</n-form>
</template>
<n-flex style="margin: 16px 0 120px" align="center" justify="end">
<n-button type="primary" @click="submit">提交</n-button>
</n-flex>
</template>
<style scoped>
.title {
margin-top: 0;
}
.box {
margin-bottom: 20px;
}
.w-100 {
width: 100px;
}
.problemTitleInput {
width: 300px;
}
.addSamples {
width: 100%;
}
</style>