refactor(前端): 拆掉 camelCase→snake_case 转换层,契约成为唯一真相
utils/legacy.ts 是迁移期的临时层:新后端一律 camelCase,而组件读的还是
旧 Django 的 snake_case,于是在 api 层做一次递归键名重写。它自己的注释就
写了「迁移完成后这一层应当整体拆掉」。现在拆了。
代价不只是那 96 处包装:每个响应都要递归遍历整个对象重写一遍键名,而且
utils/types.ts 和 packages/contract 是两份真相 —— 手抄的那份还抄歪了好几处。
做法是按域推进,每域都用 vue-tsc 相对基线做差,确认零新增错误后再往下走。
前端的类型现在一律以契约为准,只在必要处窄化(比如 languages/template 的键
窄化成 LANGUAGE),删掉的重复定义包括 WebsiteConfig、LoginSummary、
AchievementSummary、ProblemSet、Contest、User、Profile、AdminTag、
StuckProblem 等等,其中 ClassComparison 有两个组件各手抄了一份。
## 顺带修掉的真 bug
- 管理端公告列表的「可见」开关每次都 400:列表响应被契约 omit 掉了 content,
而更新接口要求 content 必填,toggleVisible 把列表行原样回传。而且是乐观
翻转、不 await 不 catch,管理员看到开关动了、实际没存也没有提示。
改成先 GET 整条再 PUT,加失败提示。
- 删有提交的题时只显示笼统的「删除失败」:前端还在 match 旧 Django 的英文
文案,而后端返回的是 problem-has-submissions + 中文。连同另外 8 处同类
匹配一起改成判错误码 —— 文案是后端随时能改的,match 文案改一个字就静默失效。
- SubmissionStatus.time_limit_exceeded 写成 `1 | 2`,TS 按位或算成 3,和
memory_limit_exceeded 撞了同一个值。后端 judge/status.ts 里这是分开的
两个码,按后端拆成 cpu_/real_ 两项。当前没有代码读这两个成员,但
CLAUDE.md 明确要求判题状态码三处同步。
- 流程图历史翻到没有提交的那一页会直接抛:契约里 submission 是 nullable,
被 any 掩盖成看起来非空。补了 null 分支。
## 契约里被逼出来的三处不诚实
- grade 写成 z.string(),但 averageGrade() 在没有可用数据时返回空串,
前端三张图表拿它查 Record<Grade,...> 会查出 undefined。按实际收紧成
z.enum([...,""]),四个查表点都补了「无评级」分支。
- difficulty 写成 z.string()。核对过生产库 dump:956 道题只有
Low/Mid/High 三个值(761/149/46)。收紧成枚举。
- topReaction 写成 z.string(),既对不上前端渲染的 {type,count},也对不上
旧后端 get_top_reactions 下发的形状。改成正确形状并注明当前恒传 null。
## 明确保留 snake_case 的 54 处
判题沙箱原始输出(cpu_time/exit_code/output_md5/compile_output)、
statistic_info 内容(err_info/time_cost/ast_results)、submission_info
JSONB(is_ac/ac_time/error_number,回滚时旧后端还要读)、SQL 判题引擎的
total_rows/order_sensitive/changed_tables、WebSocket 的 submission_id、
以及数据库选项键 enable_maxkb。每一处都在类型定义旁写了为什么不能改。
language 没有跟着收紧契约 —— 它是配置项、随时可能加语言,收紧会让新语言
在后端 parse 时直接抛。改在 api 边界一处窄化。
## 另外
- utils/http.ts 整个模块已是死代码(四处引用全是 import type),删除。
- profile 的 blog/github/school/major/language 五个字段全链路空转,没有
任何组件读,从契约到类型一并摘除(数据库列不动)。
- admin/account.ts 往 user_profile 塞的 totalScore 是 OI 模式遗留,表里
没这一列。Drizzle 按表定义拼列名会把它静默丢弃,所以没出过错,是死代码。
验证:vue-tsc 143 → 54 条且无新增,apps/api tsc、check:routes、web build
全通过;各域响应形状逐条打接口核对过。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,36 +1,28 @@
|
||||
<script setup lang="ts">
|
||||
import { getStuckProblems } from "admin/api"
|
||||
|
||||
interface StuckProblem {
|
||||
problem_id: string
|
||||
problem_title: string
|
||||
total: number
|
||||
failed: number
|
||||
failed_users: number
|
||||
ac_rate: number
|
||||
}
|
||||
import type { StuckProblem } from "utils/types"
|
||||
|
||||
const loading = ref(true)
|
||||
const data = ref<StuckProblem[]>([])
|
||||
|
||||
const columns: DataTableColumn<StuckProblem>[] = [
|
||||
{ title: "题目 ID", key: "problem_id", width: 100 },
|
||||
{ title: "题目名称", key: "problem_title", minWidth: 200 },
|
||||
{ title: "题目 ID", key: "problemId", width: 100 },
|
||||
{ title: "题目名称", key: "problemTitle", minWidth: 200 },
|
||||
{ title: "总提交", key: "total", width: 100, sorter: "default" },
|
||||
{ title: "失败次数", key: "failed", width: 100, sorter: "default" },
|
||||
{
|
||||
title: "卡住学生数",
|
||||
key: "failed_users",
|
||||
key: "failedUsers",
|
||||
width: 120,
|
||||
sorter: "default",
|
||||
defaultSortOrder: "descend",
|
||||
},
|
||||
{
|
||||
title: "AC 率",
|
||||
key: "ac_rate",
|
||||
key: "acRate",
|
||||
width: 100,
|
||||
sorter: "default",
|
||||
render: (row) => `${row.ac_rate}%`,
|
||||
render: (row) => `${row.acRate}%`,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import type { AcTrend } from "utils/types"
|
||||
import { Line } from "vue-chartjs"
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
@@ -22,18 +23,7 @@ ChartJS.register(
|
||||
Tooltip,
|
||||
)
|
||||
|
||||
interface YearlyEntry {
|
||||
year: number
|
||||
total: number
|
||||
accepted: number
|
||||
ac_rate: number
|
||||
}
|
||||
|
||||
interface ProblemTrend {
|
||||
problem_id: string
|
||||
problem_title: string
|
||||
yearly: YearlyEntry[]
|
||||
}
|
||||
type ProblemTrend = AcTrend
|
||||
|
||||
const currentYear = new Date().getFullYear()
|
||||
const yearOptions = Array.from({ length: currentYear - 2022 + 1 }, (_, i) => ({
|
||||
@@ -79,7 +69,7 @@ function getChartData(problem: ProblemTrend) {
|
||||
datasets: [
|
||||
{
|
||||
label: "AC 率",
|
||||
data: problem.yearly.map((y) => y.ac_rate),
|
||||
data: problem.yearly.map((y) => y.acRate),
|
||||
fill: true,
|
||||
tension: 0.3,
|
||||
backgroundColor: "rgba(99, 179, 237, 0.2)",
|
||||
@@ -98,14 +88,14 @@ function getChartOptions(problem: ProblemTrend) {
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: `${problem.problem_id} · ${problem.problem_title}`,
|
||||
text: `${problem.problemId} · ${problem.problemTitle}`,
|
||||
font: { size: 14 },
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (ctx: any) => {
|
||||
const entry = problem.yearly[ctx.dataIndex]
|
||||
return `AC 率: ${entry.ac_rate}% (${entry.accepted}/${entry.total})`
|
||||
return `AC 率: ${entry.acRate}% (${entry.accepted}/${entry.total})`
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -127,9 +117,9 @@ async function fetchData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getTopACTrend({
|
||||
since_year: sinceYear.value,
|
||||
until_year: untilYear.value,
|
||||
min_per_year: minPerYear.value,
|
||||
sinceYear: sinceYear.value,
|
||||
untilYear: untilYear.value,
|
||||
minPerYear: minPerYear.value,
|
||||
})
|
||||
data.value = res.data
|
||||
} finally {
|
||||
@@ -174,7 +164,7 @@ onMounted(fetchData)
|
||||
暂无数据
|
||||
</div>
|
||||
<div v-else class="grid">
|
||||
<div v-for="problem in data" :key="problem.problem_id" class="chart-card">
|
||||
<div v-for="problem in data" :key="problem.problemId" class="chart-card">
|
||||
<Line
|
||||
:data="getChartData(problem)"
|
||||
:options="getChartOptions(problem)"
|
||||
|
||||
@@ -34,7 +34,7 @@ async function handleDeleteProblem() {
|
||||
message.success("删除成功")
|
||||
emit("updated")
|
||||
} catch (err: any) {
|
||||
if (err.data === "Can't delete the problem as it has submissions") {
|
||||
if (err.error === "problem-has-submissions") {
|
||||
message.error("这道题有提交之后,就不能被删除")
|
||||
} else {
|
||||
message.error("删除失败")
|
||||
@@ -82,9 +82,9 @@ async function handleMakePublic() {
|
||||
showMakePublicModal.value = false
|
||||
emit("updated") // 刷新列表
|
||||
} catch (err: any) {
|
||||
if (err.data === "Duplicate display ID") {
|
||||
if (err.error === "display-id-exists") {
|
||||
message.error("该题目编号已存在,请使用其他编号")
|
||||
} else if (err.data === "Already be a public problem") {
|
||||
} else if (err.error === "already-public") {
|
||||
message.error("该题目已经是公开题目")
|
||||
} else {
|
||||
message.error("转换失败:" + (err.data || "未知错误"))
|
||||
|
||||
@@ -23,9 +23,9 @@ async function addProblem() {
|
||||
)
|
||||
emit("added")
|
||||
} catch (err: any) {
|
||||
if (err.data === "Duplicate display id in this contest") {
|
||||
if (err.error === "display-id-exists") {
|
||||
message.error("显示编号重复了,请重新写一个")
|
||||
} else if (err.data === "Contest has ended") {
|
||||
} else if (err.error === "contest-ended") {
|
||||
message.error("这场比赛已经结束了,不能添加题目")
|
||||
} else {
|
||||
message.error(err.data)
|
||||
|
||||
@@ -60,7 +60,7 @@ async function submit() {
|
||||
)
|
||||
const verb = props.action === "add" ? "添加" : "移除"
|
||||
message.success(
|
||||
`已为 ${res.data.problem_count} 道题${verb} ${res.data.tag_count} 个标签`,
|
||||
`已为 ${res.data.problemCount} 道题${verb} ${res.data.tagCount} 个标签`,
|
||||
)
|
||||
close()
|
||||
emit("done")
|
||||
@@ -96,7 +96,7 @@ watch(
|
||||
:checked="selectedSet.has(tag.name)"
|
||||
@update:checked="toggleTag(tag.name)"
|
||||
>
|
||||
{{ tag.name }}({{ tag.problem_count }})
|
||||
{{ tag.name }}({{ tag.problemCount }})
|
||||
</n-tag>
|
||||
</n-flex>
|
||||
<n-dynamic-tags v-if="action === 'add'" v-model:value="newTags" />
|
||||
|
||||
@@ -125,7 +125,7 @@ async function generate() {
|
||||
blanks.map(async (s) => {
|
||||
try {
|
||||
const res = await generateSQLTestcase({
|
||||
ref_sql: refSQL.value,
|
||||
refSql: refSQL.value,
|
||||
mode: props.mode,
|
||||
})
|
||||
s.sql = res.data.sql
|
||||
@@ -154,8 +154,8 @@ async function preview() {
|
||||
s.stale = false
|
||||
try {
|
||||
const res = await previewSQLTestcase({
|
||||
init_sql: s.sql,
|
||||
ref_sql: refSQL.value,
|
||||
initSql: s.sql,
|
||||
refSql: refSQL.value,
|
||||
mode: props.mode,
|
||||
})
|
||||
s.display = res.data
|
||||
|
||||
@@ -11,13 +11,7 @@ import {
|
||||
} from "utils/constants"
|
||||
import download from "utils/download"
|
||||
import { unique } from "utils/functions"
|
||||
import type {
|
||||
BlankProblem,
|
||||
LANGUAGE,
|
||||
SQLConfig,
|
||||
Tag,
|
||||
Testcase,
|
||||
} from "utils/types"
|
||||
import type { BlankProblem, LANGUAGE, Tag, Testcase } from "utils/types"
|
||||
import {
|
||||
createContestProblem,
|
||||
createProblem,
|
||||
@@ -62,13 +56,13 @@ const problem = useLocalStorage<BlankProblem>(STORAGE_KEY.ADMIN_PROBLEM, {
|
||||
_id: "",
|
||||
title: "",
|
||||
description: "",
|
||||
input_description: "",
|
||||
output_description: "",
|
||||
time_limit: 1000,
|
||||
memory_limit: 64,
|
||||
difficulty: "Low" as "Low" | "Mid" | "High",
|
||||
inputDescription: "",
|
||||
outputDescription: "",
|
||||
timeLimit: 1000,
|
||||
memoryLimit: 64,
|
||||
difficulty: "Low",
|
||||
visible: false,
|
||||
share_submission: false,
|
||||
shareSubmission: false,
|
||||
tags: [],
|
||||
languages: ["Python3", "C"] as LANGUAGE[],
|
||||
template: {} as { [key in LANGUAGE]?: string },
|
||||
@@ -77,20 +71,20 @@ const problem = useLocalStorage<BlankProblem>(STORAGE_KEY.ADMIN_PROBLEM, {
|
||||
{ input: "", output: "" },
|
||||
{ input: "", output: "" },
|
||||
],
|
||||
test_case_id: "",
|
||||
test_case_score: [] as Testcase[],
|
||||
testCaseId: "",
|
||||
testCaseScore: [] as Testcase[],
|
||||
hint: "",
|
||||
source: "",
|
||||
prompt: "",
|
||||
answers: [] as { language: LANGUAGE; code: string }[],
|
||||
contest_id: "",
|
||||
allow_flowchart: false,
|
||||
mermaid_code: "",
|
||||
flowchart_data: {},
|
||||
flowchart_hint: "",
|
||||
show_flowchart: false,
|
||||
ast_rules: null as { [key: string]: any[] } | null,
|
||||
sql_config: null as SQLConfig | null,
|
||||
contestId: null,
|
||||
allowFlowchart: false,
|
||||
showFlowchart: false,
|
||||
mermaidCode: "",
|
||||
flowchartHint: "",
|
||||
astRules: null,
|
||||
sqlConfig: null,
|
||||
sqlDisplay: null,
|
||||
})
|
||||
|
||||
// 从服务器来的tag列表
|
||||
@@ -189,19 +183,19 @@ watch(
|
||||
return
|
||||
}
|
||||
needTemplate.value = false
|
||||
if (!problem.value.sql_config) {
|
||||
problem.value.sql_config = { mode: "query", order_sensitive: false }
|
||||
if (!problem.value.sqlConfig) {
|
||||
problem.value.sqlConfig = { mode: "query", order_sensitive: false }
|
||||
}
|
||||
currentActiveAnswer.value = "SQL"
|
||||
// 代码规则检查基于 Python/C 的 AST 解析,对 SQL 没有意义,清空避免脏数据
|
||||
if (problem.value.ast_rules) {
|
||||
problem.value.ast_rules = null
|
||||
if (problem.value.astRules) {
|
||||
problem.value.astRules = null
|
||||
}
|
||||
// 流程图依赖 Python 答案生成,对 SQL 没有意义
|
||||
problem.value.allow_flowchart = false
|
||||
problem.value.show_flowchart = false
|
||||
} else if (problem.value.sql_config) {
|
||||
problem.value.sql_config = null
|
||||
problem.value.allowFlowchart = false
|
||||
problem.value.showFlowchart = false
|
||||
} else if (problem.value.sqlConfig) {
|
||||
problem.value.sqlConfig = null
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
@@ -219,32 +213,31 @@ async function getProblemDetail() {
|
||||
problem.value._id = data._id
|
||||
problem.value.title = data.title
|
||||
problem.value.description = data.description
|
||||
problem.value.input_description = data.input_description
|
||||
problem.value.output_description = data.output_description
|
||||
problem.value.time_limit = data.time_limit
|
||||
problem.value.memory_limit = data.memory_limit
|
||||
problem.value.memory_limit = data.memory_limit
|
||||
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.share_submission = data.share_submission
|
||||
problem.value.shareSubmission = data.shareSubmission
|
||||
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.test_case_id = data.test_case_id
|
||||
problem.value.test_case_score = data.test_case_score
|
||||
problem.value.hint = data.hint
|
||||
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.allow_flowchart = data.allow_flowchart
|
||||
problem.value.show_flowchart = data.show_flowchart
|
||||
problem.value.mermaid_code = data.mermaid_code ?? ""
|
||||
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
|
||||
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 {
|
||||
@@ -253,8 +246,8 @@ async function getProblemDetail() {
|
||||
code: "",
|
||||
}))
|
||||
}
|
||||
if (problem.value.contest_id) {
|
||||
problem.value.contest_id = problem.value.contest_id
|
||||
if (problem.value.contestId) {
|
||||
problem.value.contestId = problem.value.contestId
|
||||
}
|
||||
|
||||
// 下面是用来显示的:
|
||||
@@ -305,8 +298,8 @@ async function handleUploadTestcases({ file }: UploadCustomRequestOptions) {
|
||||
for (let file of testcases) {
|
||||
file.score = (100 / testcases.length).toFixed(0)
|
||||
}
|
||||
problem.value.test_case_score = testcases
|
||||
problem.value.test_case_id = res.data.id
|
||||
problem.value.testCaseScore = testcases
|
||||
problem.value.testCaseId = res.data.id
|
||||
} catch (err) {
|
||||
message.error("上传测试用例失败")
|
||||
}
|
||||
@@ -338,7 +331,7 @@ async function validateProblem() {
|
||||
else if (
|
||||
!problem.value.description ||
|
||||
(!isSQLProblem.value &&
|
||||
(!problem.value.input_description || !problem.value.output_description))
|
||||
(!problem.value.inputDescription || !problem.value.outputDescription))
|
||||
) {
|
||||
message.error("题目或输入或输出没有填写")
|
||||
hasErrors = true
|
||||
@@ -359,7 +352,7 @@ async function validateProblem() {
|
||||
hasErrors = true
|
||||
}
|
||||
// 测试用例
|
||||
else if (problem.value.test_case_score.length === 0) {
|
||||
else if (problem.value.testCaseScore.length === 0) {
|
||||
message.error("测试用例没有上传")
|
||||
hasErrors = true
|
||||
} else if (problem.value.languages.length === 0) {
|
||||
@@ -367,7 +360,7 @@ async function validateProblem() {
|
||||
hasErrors = true
|
||||
}
|
||||
// SQL 题验证
|
||||
else if (isSQLProblem.value && !problem.value.sql_config?.mode) {
|
||||
else if (isSQLProblem.value && !problem.value.sqlConfig?.mode) {
|
||||
message.error("SQL 题需要选择题型(查询题/增删改题)")
|
||||
hasErrors = true
|
||||
} else if (
|
||||
@@ -380,11 +373,8 @@ async function validateProblem() {
|
||||
hasErrors = true
|
||||
}
|
||||
// 流程图验证
|
||||
else if (problem.value.show_flowchart || problem.value.allow_flowchart) {
|
||||
if (
|
||||
!problem.value.mermaid_code ||
|
||||
problem.value.mermaid_code.trim() === ""
|
||||
) {
|
||||
else if (problem.value.showFlowchart || problem.value.allowFlowchart) {
|
||||
if (!problem.value.mermaidCode || problem.value.mermaidCode.trim() === "") {
|
||||
message.error("启用了流程图功能,但流程图代码为空")
|
||||
hasErrors = true
|
||||
} else if (!mermaidRenderSuccess.value) {
|
||||
@@ -449,7 +439,7 @@ async function submit() {
|
||||
route.name === "admin contest problem create" ||
|
||||
route.name === "admin contest problem edit"
|
||||
) {
|
||||
problem.value.contest_id = props.contestID
|
||||
problem.value.contestId = Number(props.contestID)
|
||||
}
|
||||
try {
|
||||
await api!(problem.value)
|
||||
@@ -474,7 +464,7 @@ async function submit() {
|
||||
})
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.data === "Display ID already exists") {
|
||||
if (err.error === "display-id-exists") {
|
||||
message.error("显示编号重复了,请换一个显示编号")
|
||||
} else {
|
||||
message.error(err.data)
|
||||
@@ -503,7 +493,7 @@ async function generateMermaid() {
|
||||
)
|
||||
isAIGenerating.value = false
|
||||
message.warning("如果渲染不成功,请复制到外部 AI 网站检查语法")
|
||||
problem.value.mermaid_code = res.data.flowchart
|
||||
problem.value.mermaidCode = res.data.flowchart
|
||||
}
|
||||
|
||||
const showGeneratorModal = ref(false)
|
||||
@@ -512,8 +502,8 @@ function handleTestcasesGenerated(
|
||||
testCaseId: string,
|
||||
testCaseScore: Testcase[],
|
||||
) {
|
||||
problem.value.test_case_id = testCaseId
|
||||
problem.value.test_case_score = testCaseScore
|
||||
problem.value.testCaseId = testCaseId
|
||||
problem.value.testCaseScore = testCaseScore
|
||||
showGeneratorModal.value = false
|
||||
}
|
||||
|
||||
@@ -593,12 +583,12 @@ watch(
|
||||
/>
|
||||
<TextEditor
|
||||
v-if="ready && !isSQLProblem"
|
||||
v-model:value="problem.input_description"
|
||||
v-model:value="problem.inputDescription"
|
||||
title="输入的描述"
|
||||
/>
|
||||
<TextEditor
|
||||
v-if="ready && !isSQLProblem"
|
||||
v-model:value="problem.output_description"
|
||||
v-model:value="problem.outputDescription"
|
||||
title="输出的描述"
|
||||
/>
|
||||
<template v-if="!isSQLProblem">
|
||||
@@ -686,12 +676,12 @@ watch(
|
||||
</n-form>
|
||||
|
||||
<n-form
|
||||
v-if="isSQLProblem && problem.sql_config"
|
||||
v-if="isSQLProblem && problem.sqlConfig"
|
||||
inline
|
||||
label-placement="left"
|
||||
>
|
||||
<n-form-item label="SQL 题型">
|
||||
<n-radio-group v-model:value="problem.sql_config.mode">
|
||||
<n-radio-group v-model:value="problem.sqlConfig.mode">
|
||||
<n-radio-button value="query">查询题(比对查询结果)</n-radio-button>
|
||||
<n-radio-button value="modify">
|
||||
增删改题(比对执行后的表数据)
|
||||
@@ -699,7 +689,7 @@ watch(
|
||||
</n-radio-group>
|
||||
</n-form-item>
|
||||
<n-form-item label="严格比对行顺序">
|
||||
<n-switch v-model:value="problem.sql_config.order_sensitive" />
|
||||
<n-switch v-model:value="problem.sqlConfig.order_sensitive" />
|
||||
<n-text depth="3" style="margin-left: 12px">
|
||||
题目要求 ORDER BY 时开启;关闭则按无序集合比对
|
||||
</n-text>
|
||||
@@ -766,7 +756,7 @@ watch(
|
||||
<n-grid v-if="!isSQLProblem" :cols="2">
|
||||
<n-gi :span="1">
|
||||
<AstRulesEditor
|
||||
v-model="problem.ast_rules!"
|
||||
v-model="problem.astRules!"
|
||||
:languages="problem.languages"
|
||||
/>
|
||||
</n-gi>
|
||||
@@ -802,22 +792,22 @@ watch(
|
||||
<SQLTestcaseEditor
|
||||
v-if="isSQLProblem"
|
||||
:answers="problem.answers"
|
||||
:mode="problem.sql_config?.mode ?? 'query'"
|
||||
:mode="problem.sqlConfig?.mode ?? 'query'"
|
||||
:problem-id="problem.id"
|
||||
@uploaded="handleTestcasesGenerated"
|
||||
/>
|
||||
|
||||
<n-alert
|
||||
class="box"
|
||||
v-if="problem.test_case_score.length"
|
||||
v-if="problem.testCaseScore.length"
|
||||
:show-icon="false"
|
||||
type="info"
|
||||
>
|
||||
<template #header>
|
||||
<n-flex align="center">
|
||||
<div>
|
||||
测试组编号 {{ problem.test_case_id.slice(0, 12) }} 共有
|
||||
{{ problem.test_case_score.length }}
|
||||
测试组编号 {{ problem.testCaseId.slice(0, 12) }} 共有
|
||||
{{ problem.testCaseScore.length }}
|
||||
条测试用例
|
||||
</div>
|
||||
<n-button
|
||||
@@ -870,23 +860,23 @@ watch(
|
||||
</n-button>
|
||||
</n-form-item>
|
||||
<n-form-item label="允许提交流程图">
|
||||
<n-switch v-model:value="problem.allow_flowchart" />
|
||||
<n-switch v-model:value="problem.allowFlowchart" />
|
||||
</n-form-item>
|
||||
<n-form-item label="显示标准流程图">
|
||||
<n-switch v-model:value="problem.show_flowchart" />
|
||||
<n-switch v-model:value="problem.showFlowchart" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
|
||||
<n-form>
|
||||
<n-form-item>
|
||||
<MermaidEditor
|
||||
v-model="problem.mermaid_code"
|
||||
v-model="problem.mermaidCode"
|
||||
@render-success="onMermaidRenderSuccess"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="流程图提示信息(选填)">
|
||||
<n-input
|
||||
v-model:value="problem.flowchart_hint"
|
||||
v-model:value="problem.flowchartHint"
|
||||
placeholder="请输入流程图相关的提示信息,帮助学生理解题目要求"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
@@ -113,20 +113,20 @@ const baseColumns: DataTableColumn<AdminProblemFiltered>[] = [
|
||||
width: 80,
|
||||
render: (row) =>
|
||||
h(NFlex, { size: 4, align: "center" }, () => [
|
||||
row.allow_flowchart
|
||||
row.allowFlowchart
|
||||
? h(Icon, {
|
||||
width: 18,
|
||||
icon: "vscode-icons:file-type-drawio",
|
||||
title: "绘图",
|
||||
})
|
||||
: row.show_flowchart
|
||||
: row.showFlowchart
|
||||
? h(Icon, {
|
||||
width: 18,
|
||||
icon: "vscode-icons:file-type-graphql",
|
||||
title: "流程图",
|
||||
})
|
||||
: null,
|
||||
row.has_ast_rules
|
||||
row.hasAstRules
|
||||
? h(Icon, {
|
||||
width: 18,
|
||||
icon: "vscode-icons:file-type-light-todo",
|
||||
@@ -140,7 +140,7 @@ const baseColumns: DataTableColumn<AdminProblemFiltered>[] = [
|
||||
key: "top_reaction",
|
||||
width: 60,
|
||||
render: (row) => {
|
||||
const top = row.top_reaction
|
||||
const top = row.topReaction
|
||||
if (!top) return null
|
||||
const reaction = REACTIONS.find((it) => it.key === top.type)
|
||||
if (!reaction) return null
|
||||
@@ -155,7 +155,7 @@ const baseColumns: DataTableColumn<AdminProblemFiltered>[] = [
|
||||
title: "创建时间",
|
||||
key: "create_time",
|
||||
width: 200,
|
||||
render: (row) => parseTime(row.create_time, "YYYY-MM-DD HH:mm:ss"),
|
||||
render: (row) => parseTime(row.createTime, "YYYY-MM-DD HH:mm:ss"),
|
||||
},
|
||||
{
|
||||
title: "可见",
|
||||
|
||||
@@ -57,7 +57,7 @@ const columns: DataTableColumn<AdminTag>[] = [
|
||||
h(
|
||||
NButton,
|
||||
{ text: true, type: "primary", onClick: () => openTagProblems(row) },
|
||||
() => String(row.problem_count),
|
||||
() => String(row.problemCount),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -123,7 +123,7 @@ async function saveTag(tag: AdminTag) {
|
||||
const res = await renameTag(tag.id, name)
|
||||
if (res.data.merged) {
|
||||
message.success(
|
||||
`已合并到「${res.data.name}」,影响 ${res.data.affected_count} 道题`,
|
||||
`已合并到「${res.data.name}」,影响 ${res.data.affectedCount} 道题`,
|
||||
)
|
||||
} else {
|
||||
message.success("已重命名")
|
||||
@@ -135,7 +135,7 @@ async function saveTag(tag: AdminTag) {
|
||||
function confirmDelete(tag: AdminTag) {
|
||||
dialog.warning({
|
||||
title: "删除标签",
|
||||
content: `确定删除标签「${tag.name}」吗?当前有 ${tag.problem_count} 道题在使用它,删除后这些题目会失去该标签。`,
|
||||
content: `确定删除标签「${tag.name}」吗?当前有 ${tag.problemCount} 道题在使用它,删除后这些题目会失去该标签。`,
|
||||
positiveText: "删除",
|
||||
negativeText: "取消",
|
||||
onPositiveClick: async () => {
|
||||
|
||||
Reference in New Issue
Block a user