feat: SQL 题展示数据表与期望结果
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
60
src/oj/problem/components/SQLDataTable.vue
Normal file
60
src/oj/problem/components/SQLDataTable.vue
Normal 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>
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user