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:
@@ -185,8 +185,8 @@ const goSubmissions = () => {
|
||||
}
|
||||
|
||||
const goEdit = () => {
|
||||
const url = problem.value!.contest
|
||||
? `/admin/contest/${problem.value!.contest}/problem/edit/${problem.value!.id}`
|
||||
const url = problem.value!.contestId
|
||||
? `/admin/contest/${problem.value!.contestId}/problem/edit/${problem.value!.id}`
|
||||
: `/admin/problem/edit/${problem.value!.id}`
|
||||
window.open(router.resolve(url).href, "_blank")
|
||||
}
|
||||
|
||||
@@ -32,8 +32,8 @@ const { problem } = storeToRefs(problemStore)
|
||||
const problemSetId = computed(() => route.params.problemSetId)
|
||||
|
||||
// SQL 题:隐藏输入/输出/例子,改为渲染数据表与期望结果
|
||||
const isSQL = computed(() => !!problem.value?.sql_config)
|
||||
const sqlDisplay = computed(() => problem.value?.sql_display ?? null)
|
||||
const isSQL = computed(() => !!problem.value?.sqlConfig)
|
||||
const sqlDisplay = computed(() => problem.value?.sqlDisplay ?? null)
|
||||
const sqlExpectedQuery = computed(() => {
|
||||
const exp = sqlDisplay.value?.expected
|
||||
return exp && "columns" in exp ? exp : null
|
||||
@@ -71,7 +71,7 @@ watch(
|
||||
|
||||
// AC 或失败次数 >= 3 时加载推荐
|
||||
watch(
|
||||
() => [problem.value?._id, problem.value?.my_status, problemStore.failCount],
|
||||
() => [problem.value?._id, problem.value?.myStatus, problemStore.failCount],
|
||||
([, status, failCount]) => {
|
||||
if (status === 0 || (failCount as number) >= 3) {
|
||||
loadSimilarProblems()
|
||||
@@ -82,9 +82,9 @@ watch(
|
||||
|
||||
const hasTriedButNotPassed = computed(() => {
|
||||
return (
|
||||
problem.value?.my_status !== undefined &&
|
||||
problem.value?.my_status !== null &&
|
||||
problem.value?.my_status !== 0
|
||||
problem.value?.myStatus !== undefined &&
|
||||
problem.value?.myStatus !== null &&
|
||||
problem.value?.myStatus !== 0
|
||||
)
|
||||
})
|
||||
|
||||
@@ -177,8 +177,8 @@ function ruleTagType(engine: string): "error" | "success" | "info" {
|
||||
}
|
||||
|
||||
const astRulesForDisplay = computed(() => {
|
||||
if (!problem.value?.ast_rules) return []
|
||||
return Object.entries(problem.value.ast_rules).filter(
|
||||
if (!problem.value?.astRules) return []
|
||||
return Object.entries(problem.value.astRules).filter(
|
||||
([, rules]) => rules.length > 0,
|
||||
)
|
||||
})
|
||||
@@ -249,7 +249,7 @@ function type(status: ProblemStatus) {
|
||||
<!-- 已通过 -->
|
||||
<n-alert
|
||||
class="status-alert"
|
||||
v-if="problem.my_status === 0"
|
||||
v-if="problem.myStatus === 0"
|
||||
type="success"
|
||||
title="🎉 本 题 已 经 被 你 解 决 啦"
|
||||
>
|
||||
@@ -291,7 +291,7 @@ function type(status: ProblemStatus) {
|
||||
</p>
|
||||
<MdPreview
|
||||
preview-theme="vuepress"
|
||||
:model-value="problem.input_description"
|
||||
:model-value="problem.inputDescription"
|
||||
:theme="isDark ? 'dark' : 'light'"
|
||||
/>
|
||||
|
||||
@@ -303,7 +303,7 @@ function type(status: ProblemStatus) {
|
||||
</p>
|
||||
<MdPreview
|
||||
preview-theme="vuepress"
|
||||
:model-value="problem.output_description"
|
||||
:model-value="problem.outputDescription"
|
||||
:theme="isDark ? 'dark' : 'light'"
|
||||
/>
|
||||
</template>
|
||||
@@ -338,7 +338,7 @@ function type(status: ProblemStatus) {
|
||||
:total-rows="sqlExpectedQuery.total_rows"
|
||||
:truncated="sqlExpectedQuery.truncated"
|
||||
/>
|
||||
<p v-if="!problem.sql_config?.order_sensitive" class="sqlNote">
|
||||
<p v-if="!problem.sqlConfig?.order_sensitive" class="sqlNote">
|
||||
结果顺序不限
|
||||
</p>
|
||||
</template>
|
||||
|
||||
@@ -11,13 +11,13 @@ const { renderError, renderFlowchart } = useMermaid()
|
||||
const renderProblemFlowchart = async () => {
|
||||
await renderFlowchart(
|
||||
mermaidContainer.value,
|
||||
problem.value?.mermaid_code ?? "",
|
||||
problem.value?.mermaidCode ?? "",
|
||||
)
|
||||
}
|
||||
|
||||
onMounted(renderProblemFlowchart)
|
||||
|
||||
watch(() => problem.value?.mermaid_code, renderProblemFlowchart)
|
||||
watch(() => problem.value?.mermaidCode, renderProblemFlowchart)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -30,7 +30,7 @@ const beatRate = ref("0")
|
||||
const yearlyACData = ref<YearlyACData[]>([])
|
||||
|
||||
const data = computed(() => {
|
||||
const status = problem.value!.statistic_info
|
||||
const status = problem.value!.statisticInfo
|
||||
const labels = []
|
||||
for (let i in status) {
|
||||
if (status[i] !== 0) {
|
||||
@@ -50,14 +50,14 @@ const numbers = computed(() => {
|
||||
return [
|
||||
{
|
||||
icon: "streamline-ultimate-color:checklist",
|
||||
title: problem.value?.submission_number ?? 0,
|
||||
title: problem.value?.submissionNumber ?? 0,
|
||||
content: "总提交",
|
||||
int: true,
|
||||
suffix: "",
|
||||
},
|
||||
{
|
||||
icon: "streamline-emojis:woman-raising-hand-2",
|
||||
title: problem.value?.accepted_number ?? 0,
|
||||
title: problem.value?.acceptedNumber ?? 0,
|
||||
content: "通过数",
|
||||
int: true,
|
||||
suffix: "",
|
||||
@@ -65,8 +65,8 @@ const numbers = computed(() => {
|
||||
{
|
||||
icon: "fluent-emoji:chart-increasing",
|
||||
title: getACRateNumber(
|
||||
problem.value?.accepted_number ?? 0,
|
||||
problem.value?.submission_number ?? 0,
|
||||
problem.value?.acceptedNumber ?? 0,
|
||||
problem.value?.submissionNumber ?? 0,
|
||||
),
|
||||
content: "通过率",
|
||||
int: false,
|
||||
@@ -115,10 +115,10 @@ onMounted(() => {
|
||||
{{ problem._id }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="出题人">
|
||||
{{ problem.created_by.username }}
|
||||
{{ problem.createdBy.username }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="创建时间">
|
||||
{{ parseTime(problem.create_time) }}
|
||||
{{ parseTime(problem.createTime) }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="难度">
|
||||
<n-tag :type="getTagColor(problem.difficulty)">
|
||||
@@ -150,7 +150,7 @@ onMounted(() => {
|
||||
</n-card>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
<div class="pie" v-if="problem && problem.submission_number > 0">
|
||||
<div class="pie" v-if="problem && problem.submissionNumber > 0">
|
||||
<Pie :data="data" :options="options" />
|
||||
</div>
|
||||
<ProblemYearlyChart :data="yearlyACData" />
|
||||
|
||||
@@ -10,17 +10,17 @@ defineProps<{
|
||||
<n-flex align="center">
|
||||
<span>{{ problem.title }}</span>
|
||||
<Icon
|
||||
v-if="problem.allow_flowchart"
|
||||
v-if="problem.allowFlowchart"
|
||||
width="18"
|
||||
icon="vscode-icons:file-type-drawio"
|
||||
/>
|
||||
<Icon
|
||||
v-else-if="problem.show_flowchart"
|
||||
v-else-if="problem.showFlowchart"
|
||||
width="18"
|
||||
icon="vscode-icons:file-type-graphql"
|
||||
/>
|
||||
<Icon
|
||||
v-if="problem.has_ast_rules"
|
||||
v-if="problem.hasAstRules"
|
||||
width="18"
|
||||
icon="vscode-icons:file-type-light-todo"
|
||||
/>
|
||||
|
||||
@@ -80,7 +80,7 @@ const wheelItems = REACTIONS.map((item, index) => ({
|
||||
style: getWheelItemStyle(index),
|
||||
}))
|
||||
|
||||
const solved = computed(() => problem.value?.my_status === 0)
|
||||
const solved = computed(() => problem.value?.myStatus === 0)
|
||||
const locked = computed(() => mine.value !== null)
|
||||
const canInteract = computed(
|
||||
() =>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useUserStore } from "shared/store/user"
|
||||
import { JUDGE_STATUS, LANGUAGE_SHOW_VALUE } from "utils/constants"
|
||||
import { parseTime } from "utils/functions"
|
||||
import { renderTableTitle } from "utils/renders"
|
||||
import type { Submission } from "utils/types"
|
||||
import type { SubmissionListItem } from "utils/types"
|
||||
import SubmissionDetail from "oj/submission/detail.vue"
|
||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||
|
||||
@@ -29,19 +29,19 @@ function showCodePanel(id: string, problem: string) {
|
||||
toggleCodePanel(true)
|
||||
}
|
||||
|
||||
const columns: DataTableColumn<Submission>[] = [
|
||||
const columns: DataTableColumn<SubmissionListItem>[] = [
|
||||
{
|
||||
title: renderTableTitle("提交时间", "fluent-emoji:seven-oclock"),
|
||||
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: renderTableTitle("编号", "fluent-emoji-flat:input-numbers"),
|
||||
key: "id",
|
||||
minWidth: 160,
|
||||
render: (row) => {
|
||||
if (!row.show_link)
|
||||
if (!row.showLink)
|
||||
return h(NFlex, { align: "center" }, () => [
|
||||
h("span", row.id.slice(0, 12)),
|
||||
h(
|
||||
@@ -90,7 +90,7 @@ const class_ac_count = ref(0)
|
||||
const all_ac_count = ref(0)
|
||||
const loading = ref(false)
|
||||
|
||||
const submissions = ref<Submission[]>([])
|
||||
const submissions = ref<SubmissionListItem[]>([])
|
||||
const total = ref(0)
|
||||
const query = reactive({
|
||||
limit: 10,
|
||||
@@ -126,8 +126,8 @@ async function listSubmissions() {
|
||||
...query,
|
||||
myself: "1",
|
||||
offset,
|
||||
problem_id: (route.params.problemID as string) ?? "",
|
||||
contest_id: (route.params.contestID as string) ?? "",
|
||||
problemId: (route.params.problemID as string) ?? "",
|
||||
contestId: (route.params.contestID as string) ?? "",
|
||||
})
|
||||
submissions.value = res.data.results
|
||||
total.value = res.data.total
|
||||
@@ -138,10 +138,10 @@ async function getRankOfThisProblem() {
|
||||
const res = await getRankOfProblem((route.params.problemID as string) ?? "")
|
||||
loading.value = false
|
||||
|
||||
class_name.value = res.data.class_name
|
||||
class_name.value = res.data.className
|
||||
rank.value = res.data.rank
|
||||
class_ac_count.value = res.data.class_ac_count
|
||||
all_ac_count.value = res.data.all_ac_count
|
||||
class_ac_count.value = res.data.classAcCount
|
||||
all_ac_count.value = res.data.allAcCount
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -35,7 +35,7 @@ const chartData = computed(() => ({
|
||||
datasets: [
|
||||
{
|
||||
label: "AC 率",
|
||||
data: props.data.map((d) => d.ac_rate),
|
||||
data: props.data.map((d) => d.acRate),
|
||||
fill: true,
|
||||
tension: 0.3,
|
||||
backgroundColor: "rgba(99, 179, 237, 0.2)",
|
||||
@@ -58,7 +58,7 @@ const chartOptions = computed(() => ({
|
||||
callbacks: {
|
||||
label: (context: any) => {
|
||||
const d = props.data[context.dataIndex]
|
||||
return [`AC 率: ${d.ac_rate}%`, `通过: ${d.accepted} / ${d.total}`]
|
||||
return [`AC 率: ${d.acRate}%`, `通过: ${d.accepted} / ${d.total}`]
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -47,9 +47,9 @@ const msg = computed(() => {
|
||||
|
||||
if (
|
||||
result !== SubmissionStatus.ast_check_failed &&
|
||||
props.submission.statistic_info?.err_info
|
||||
props.submission.statisticInfo?.err_info
|
||||
) {
|
||||
msg += props.submission.statistic_info.err_info
|
||||
msg += props.submission.statisticInfo.err_info
|
||||
}
|
||||
|
||||
return msg
|
||||
@@ -161,15 +161,13 @@ const columns: DataTableColumn<Submission["info"]["data"][number]>[] = [
|
||||
<n-flex
|
||||
vertical
|
||||
v-if="
|
||||
msg ||
|
||||
infoTable.length ||
|
||||
submission.statistic_info?.ast_results?.length
|
||||
msg || infoTable.length || submission.statisticInfo?.ast_results?.length
|
||||
"
|
||||
>
|
||||
<n-card v-if="submission.statistic_info?.ast_results?.length" embedded>
|
||||
<n-card v-if="submission.statisticInfo?.ast_results?.length" embedded>
|
||||
<n-flex vertical :size="8">
|
||||
<n-flex
|
||||
v-for="(rule, i) in submission.statistic_info.ast_results"
|
||||
v-for="(rule, i) in submission.statisticInfo.ast_results"
|
||||
:key="i"
|
||||
align="center"
|
||||
:size="6"
|
||||
|
||||
@@ -130,22 +130,22 @@ async function submit() {
|
||||
|
||||
// 1. 构建提交数据
|
||||
const data: SubmitCodePayload = {
|
||||
problem_id: problem.value!.id,
|
||||
problemId: problem.value!.id,
|
||||
language: codeStore.code.language,
|
||||
code: codeStore.code.value,
|
||||
}
|
||||
if (contestID) {
|
||||
data.contest_id = parseInt(contestID)
|
||||
data.contestId = parseInt(contestID)
|
||||
}
|
||||
// 2. 提交代码到后端
|
||||
isSubmittingRequest.value = true
|
||||
try {
|
||||
const res = await submitCode(data)
|
||||
console.log(`[Submit] 代码已提交: ID=${res.data.submission_id}`)
|
||||
console.log(`[Submit] 代码已提交: ID=${res.data.submissionId}`)
|
||||
|
||||
// 3. 启动冷却 + 监控
|
||||
startCooldown()
|
||||
startMonitoring(res.data.submission_id)
|
||||
startMonitoring(res.data.submissionId)
|
||||
showResult.value = true
|
||||
} finally {
|
||||
isSubmittingRequest.value = false
|
||||
@@ -183,7 +183,7 @@ watch(
|
||||
return
|
||||
|
||||
// 1. 刷新题目状态
|
||||
problem.value!.my_status = 0
|
||||
problem.value!.myStatus = 0
|
||||
|
||||
// 2. 创建ProblemSetSubmission记录,更新题单进度
|
||||
if (problemSetId) {
|
||||
|
||||
@@ -135,16 +135,16 @@ async function submitFlowchartData() {
|
||||
|
||||
try {
|
||||
const response = await submitFlowchart({
|
||||
problem_id: problem.value!.id,
|
||||
mermaid_code: mermaidCode,
|
||||
flowchart_data: {
|
||||
problemId: problem.value!.id,
|
||||
mermaidCode,
|
||||
flowchartData: {
|
||||
compressed: true,
|
||||
data: compressed,
|
||||
},
|
||||
})
|
||||
|
||||
// 获取提交ID并订阅更新
|
||||
const submissionId = response.data.submission_id
|
||||
const submissionId = response.data.submissionId
|
||||
|
||||
if (submissionId) {
|
||||
subscribeToSubmission(submissionId)
|
||||
@@ -183,18 +183,34 @@ async function getSubmission(submissionPage = 0) {
|
||||
)
|
||||
submissionCount.value = data.count
|
||||
const submission = data.submission
|
||||
myFlowchartZippedStr.value = submission.flowchart_data.data
|
||||
myMermaidCode.value = submission.mermaid_code || ""
|
||||
// 翻到没有提交的页时后端返回 null(契约里 submission 是 nullable)——
|
||||
// 原来的 any 让这里看起来非空,真翻到那一页会直接抛
|
||||
if (!submission) {
|
||||
myFlowchartZippedStr.value = ""
|
||||
myMermaidCode.value = ""
|
||||
modalRating.value = { score: 0, grade: "" }
|
||||
evaluation.value = {
|
||||
score: 0,
|
||||
grade: "",
|
||||
feedback: "",
|
||||
suggestions: "",
|
||||
criteria_details: {},
|
||||
}
|
||||
return
|
||||
}
|
||||
myFlowchartZippedStr.value = String(submission.flowchartData.data ?? "")
|
||||
myMermaidCode.value = submission.mermaidCode || ""
|
||||
modalRating.value = {
|
||||
score: submission.ai_score,
|
||||
grade: submission.ai_grade,
|
||||
score: submission.aiScore ?? 0,
|
||||
grade: (submission.aiGrade ?? "") as Rating["grade"],
|
||||
}
|
||||
evaluation.value = {
|
||||
score: submission.ai_score,
|
||||
grade: submission.ai_grade,
|
||||
feedback: submission.ai_feedback,
|
||||
suggestions: submission.ai_suggestions,
|
||||
criteria_details: submission.ai_criteria_details,
|
||||
score: submission.aiScore ?? 0,
|
||||
grade: (submission.aiGrade ?? "") as Rating["grade"],
|
||||
feedback: submission.aiFeedback ?? "",
|
||||
suggestions: submission.aiSuggestions ?? "",
|
||||
criteria_details:
|
||||
submission.aiCriteriaDetails as Evaluation["criteria_details"],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ const { isMobile, isDesktop } = useBreakpoints()
|
||||
|
||||
const tabOptions = computed(() => {
|
||||
const options: string[] = ["content"]
|
||||
if (problem.value?.show_flowchart) {
|
||||
if (problem.value?.showFlowchart) {
|
||||
options.push("flowchart")
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ async function init() {
|
||||
problem.value = res.data
|
||||
} catch (err: any) {
|
||||
problem.value = null
|
||||
if (err.data === "Contest has not started yet.") {
|
||||
if (err.error === "contest-not-started") {
|
||||
errMsg.value = "比赛还没有开始"
|
||||
}
|
||||
}
|
||||
@@ -159,7 +159,7 @@ watch(
|
||||
<ProblemContent />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane
|
||||
v-if="problem.show_flowchart && problem.mermaid_code"
|
||||
v-if="problem.showFlowchart && problem.mermaidCode"
|
||||
name="flowchart"
|
||||
tab="流程图表"
|
||||
>
|
||||
@@ -211,7 +211,7 @@ watch(
|
||||
<ProblemContent />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane
|
||||
v-if="problem.show_flowchart && problem.mermaid_code"
|
||||
v-if="problem.showFlowchart && problem.mermaidCode"
|
||||
name="flowchart"
|
||||
tab="流程图表"
|
||||
>
|
||||
@@ -251,7 +251,7 @@ watch(
|
||||
<n-tab-pane name="content" tab="描述">
|
||||
<ProblemContent />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane v-if="problem.show_flowchart" name="flowchart" tab="流程">
|
||||
<n-tab-pane v-if="problem.showFlowchart" name="flowchart" tab="流程">
|
||||
<ProblemFlowchart />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="editor" tab="代码">
|
||||
|
||||
Reference in New Issue
Block a user