feat: SQL 题展示数据表与期望结果

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 01:26:16 -06:00
parent 14321cc222
commit aa212427ba
3 changed files with 234 additions and 89 deletions

View File

@@ -12,6 +12,7 @@ import { useDark } from "@vueuse/core"
import { MdPreview } from "md-editor-v3" import { MdPreview } from "md-editor-v3"
import "md-editor-v3/lib/preview.css" import "md-editor-v3/lib/preview.css"
import { getSimilarProblems } from "oj/api" import { getSimilarProblems } from "oj/api"
import SQLDataTable from "./SQLDataTable.vue"
type Sample = Problem["samples"][number] & { type Sample = Problem["samples"][number] & {
id: number id: number
@@ -30,6 +31,18 @@ const { problem } = storeToRefs(problemStore)
const problemSetId = computed(() => route.params.problemSetId) const problemSetId = computed(() => route.params.problemSetId)
// SQL 题:隐藏输入/输出/例子,改为渲染数据表与期望结果
const isSQL = computed(() => !!problem.value?.sql_config)
const sqlDisplay = computed(() => problem.value?.sql_display ?? null)
const sqlExpectedQuery = computed(() => {
const exp = sqlDisplay.value?.expected
return exp && "columns" in exp ? exp : null
})
const sqlChangedTables = computed(() => {
const exp = sqlDisplay.value?.expected
return exp && "changed_tables" in exp ? exp.changed_tables : []
})
const router = useRouter() const router = useRouter()
// 相似题目推荐 // 相似题目推荐
@@ -269,29 +282,79 @@ function type(status: ProblemStatus) {
:theme="isDark ? 'dark' : 'light'" :theme="isDark ? 'dark' : 'light'"
/> />
<p class="title" :style="style"> <template v-if="!isSQL">
<n-flex align="center"> <p class="title" :style="style">
<Icon icon="streamline-ultimate-color:envelope-back-front"></Icon> <n-flex align="center">
输入 <Icon icon="streamline-ultimate-color:envelope-back-front"></Icon>
</n-flex> 输入
</p> </n-flex>
<MdPreview </p>
preview-theme="vuepress" <MdPreview
:model-value="problem.input_description" preview-theme="vuepress"
:theme="isDark ? 'dark' : 'light'" :model-value="problem.input_description"
/> :theme="isDark ? 'dark' : 'light'"
/>
<p class="title" :style="style"> <p class="title" :style="style">
<n-flex align="center"> <n-flex align="center">
<Icon icon="streamline-ultimate-color:mailbox-post"></Icon> <Icon icon="streamline-ultimate-color:mailbox-post"></Icon>
输出 输出
</n-flex> </n-flex>
</p> </p>
<MdPreview <MdPreview
preview-theme="vuepress" preview-theme="vuepress"
:model-value="problem.output_description" :model-value="problem.output_description"
:theme="isDark ? 'dark' : 'light'" :theme="isDark ? 'dark' : 'light'"
/> />
</template>
<template v-if="isSQL && sqlDisplay">
<p class="title" :style="style">
<n-flex align="center">
<Icon icon="devicon:sqlite"></Icon>
数据表
</n-flex>
</p>
<div v-for="t in sqlDisplay.tables" :key="t.name">
<p class="sqlTableName">{{ t.name }}</p>
<SQLDataTable
:columns="t.columns"
:rows="t.rows"
:total-rows="t.total_rows"
:truncated="t.truncated"
/>
</div>
<p class="title" :style="style">
<n-flex align="center">
<Icon icon="streamline-ultimate-color:check-button"></Icon>
期望结果
</n-flex>
</p>
<template v-if="sqlExpectedQuery">
<SQLDataTable
:columns="sqlExpectedQuery.columns.map((name) => ({ name }))"
:rows="sqlExpectedQuery.rows"
:total-rows="sqlExpectedQuery.total_rows"
:truncated="sqlExpectedQuery.truncated"
/>
<p v-if="!problem.sql_config?.order_sensitive" class="sqlNote">
结果顺序不限
</p>
</template>
<div v-for="t in sqlChangedTables" :key="t.name">
<p class="sqlTableName">
{{ t.dropped ? `${t.name} 表已被删除` : `执行后的 ${t.name}` }}
</p>
<SQLDataTable
v-if="!t.dropped"
:columns="t.columns"
:rows="t.rows"
:total-rows="t.total_rows"
:truncated="t.truncated"
/>
</div>
</template>
<div v-if="problem.hint"> <div v-if="problem.hint">
<p class="title" :style="style"> <p class="title" :style="style">
@@ -334,50 +397,52 @@ function type(status: ProblemStatus) {
</div> </div>
</div> </div>
<div v-for="(sample, index) of samples" :key="index"> <template v-if="!isSQL">
<n-flex align="center"> <div v-for="(sample, index) of samples" :key="index">
<p class="title" :style="style"> <n-flex align="center">
<n-flex align="center"> <p class="title" :style="style">
<Icon icon="streamline-emojis:microscope"></Icon> <n-flex align="center">
例子 {{ index + 1 }} <Icon icon="streamline-emojis:microscope"></Icon>
</n-flex> 例子 {{ index + 1 }}
</p> </n-flex>
<n-button </p>
size="small" <n-button
:type="type(sample.status)" size="small"
@click="test(sample, index)" :type="type(sample.status)"
@click="test(sample, index)"
>
{{ label(sample.status, sample.loading) }}
</n-button>
</n-flex>
<n-descriptions
bordered
:column="2"
label-style="width: 50%; min-width: 100px"
> >
{{ label(sample.status, sample.loading) }} <n-descriptions-item>
</n-button> <template #label>
</n-flex> <n-flex>
<n-descriptions <span>输入</span>
bordered <Copy :value="sample.input" />
:column="2" </n-flex>
label-style="width: 50%; min-width: 100px" </template>
> <div class="testcase">{{ sample.input }}</div>
<n-descriptions-item> </n-descriptions-item>
<template #label> <n-descriptions-item>
<n-flex> <template #label>
<span>输入</span> <n-flex>
<Copy :value="sample.input" /> <span>输出</span>
</n-flex> <Copy :value="sample.output" />
</template> </n-flex>
<div class="testcase">{{ sample.input }}</div> </template>
</n-descriptions-item> <div class="testcase">{{ sample.output }}</div>
<n-descriptions-item> </n-descriptions-item>
<template #label> <n-descriptions-item label="运行结果" v-if="sample.msg">
<n-flex> <div class="testcase">{{ sample.msg }}</div>
<span>输出</span> </n-descriptions-item>
<Copy :value="sample.output" /> </n-descriptions>
</n-flex> </div>
</template> </template>
<div class="testcase">{{ sample.output }}</div>
</n-descriptions-item>
<n-descriptions-item label="运行结果" v-if="sample.msg">
<div class="testcase">{{ sample.msg }}</div>
</n-descriptions-item>
</n-descriptions>
</div>
<div v-if="problem.source"> <div v-if="problem.source">
<p class="title" :style="style"> <p class="title" :style="style">
@@ -470,4 +535,16 @@ function type(status: ProblemStatus) {
font-size: 13px; font-size: 13px;
opacity: 0.65; opacity: 0.65;
} }
.sqlTableName {
font-weight: 600;
margin: 8px 0 4px;
font-family: "Monaco";
}
.sqlNote {
font-size: 13px;
opacity: 0.65;
margin: 0 0 8px;
}
</style> </style>

View File

@@ -0,0 +1,60 @@
<script setup lang="ts">
import type { SQLDisplayColumn } from "utils/types"
defineProps<{
columns: SQLDisplayColumn[]
rows: (string | number | null)[][]
totalRows?: number
truncated?: boolean
}>()
</script>
<template>
<n-table class="sqlTable" size="small" :single-line="false">
<thead>
<tr>
<th v-for="(col, i) in columns" :key="i">
{{ col.name }}
<span v-if="col.type" class="colType">{{ col.type }}</span>
</th>
</tr>
</thead>
<tbody>
<tr v-if="rows.length === 0">
<td :colspan="columns.length" class="nullCell">空表</td>
</tr>
<tr v-for="(row, i) in rows" :key="i">
<td v-for="(v, j) in row" :key="j" :class="{ nullCell: v === null }">
{{ v === null ? "NULL" : v }}
</td>
</tr>
</tbody>
</n-table>
<p v-if="truncated" class="truncNote">
{{ totalRows }} 仅展示前 {{ rows.length }}
</p>
</template>
<style scoped>
.sqlTable {
margin-bottom: 8px;
}
.colType {
font-size: 12px;
opacity: 0.55;
margin-left: 4px;
font-weight: normal;
}
.nullCell {
opacity: 0.45;
font-style: italic;
}
.truncNote {
font-size: 13px;
opacity: 0.65;
margin: 0 0 8px;
}
</style>

View File

@@ -34,10 +34,7 @@ export interface Profile {
} }
export type UserAdminType = export type UserAdminType =
| "Regular User" "Regular User" | "Student Admin" | "Teacher Admin" | "Super Admin"
| "Student Admin"
| "Teacher Admin"
| "Super Admin"
export interface User { export interface User {
id: number id: number
@@ -72,23 +69,37 @@ export interface SQLConfig {
order_sensitive: boolean order_sensitive: boolean
} }
export interface SQLDisplayColumn {
name: string
type?: string
}
export interface SQLDisplayTable {
name: string
columns: SQLDisplayColumn[]
rows: (string | number | null)[][]
total_rows: number
truncated: boolean
dropped?: boolean
}
export interface SQLDisplay {
tables: SQLDisplayTable[]
expected:
| {
columns: string[]
rows: (string | number | null)[][]
total_rows: number
truncated: boolean
}
| { changed_tables: SQLDisplayTable[] }
}
export type LANGUAGE_SHOW_LABEL = export type LANGUAGE_SHOW_LABEL =
(typeof LANGUAGE_SHOW_VALUE)[keyof typeof LANGUAGE_SHOW_VALUE] (typeof LANGUAGE_SHOW_VALUE)[keyof typeof LANGUAGE_SHOW_VALUE]
export type SUBMISSION_RESULT = export type SUBMISSION_RESULT =
| -2 -2 | -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10
| -1
| 0
| 1
| 2
| 3
| 4
| 5
| 6
| 7
| 8
| 9
| 10
export type ProblemStatus = "passed" | "failed" | "not_test" export type ProblemStatus = "passed" | "failed" | "not_test"
@@ -173,6 +184,9 @@ export interface Problem {
// SQL 题配置(非 SQL 题为 null // SQL 题配置(非 SQL 题为 null
sql_config?: SQLConfig | null sql_config?: SQLConfig | null
// SQL 题展示数据(后端保存题目时自动生成)
sql_display?: SQLDisplay | null
} }
export type AdminProblem = Problem & AlterProblem export type AdminProblem = Problem & AlterProblem
@@ -664,13 +678,7 @@ export interface ExerciseGroupData {
} }
export type ExerciseType = export type ExerciseType =
| "mcq" "mcq" | "sort" | "fill" | "match" | "predict" | "debug" | "group"
| "sort"
| "fill"
| "match"
| "predict"
| "debug"
| "group"
export interface Exercise { export interface Exercise {
id: number id: number