Files
OJ2/apps/web/src/oj/submission/list.vue
T
xuyueandClaude Opus 5.5 e7b95d211d fix(题目): 修掉题目页重构审查查出的问题——提交途中换题会把结果算到新题上、运行例子把对的菱形判错、点评完「下一题」不跳等
题目页重构之后按「功能对照 / 状态 / 结果页 / 布局与后端」四路审查了一遍,查出的问题一起修:

提交与换题
- 提交、画流程图途中换题:A 题格式化好的代码会盖掉 B 的草稿,A 判对了把 B 标成已解决、
  弹 B 的点评、题单里跳回题单。现在每个请求回来先核对题目,轮询和推送回来的也核对提交编号
- 画着流程图换到不能画的题,语言退到题目的第一门、还被记成偏好;现在回到学生习惯的那门
- 强制点评评完点「继续 / 下一题」,学生之前在看「题目」页签的话会停在原题
- 离开题目页之后判题、流程图的 WebSocket 一直连着,现在闲 15 分钟断开

运行例子
- 输出开头的空格被削掉,打印菱形这类题对的代码也显示「没对上」
- C 的编译警告拼进了「你的输出」(后台生成测试点也会写进 .out)
- Python 的语法错误(中文标点)被说成「下标有没有越界、除数是不是 0」
- 比赛里仍然把空格画成「·」、把不同的地方标红

入口与协作
- 比赛里不给「课堂统计」、不绑协作:房间和统计都按题号找,比赛题号和公开题撞号
- 「看他交过什么」按整名匹配,不再混进学号相近的同学
- 协作中点上下文条、相似题、下一题开新标签,协作不断
- 题单入口给管理员的「⋯」里留「本题提交」

结果与题面
- 「去自己输入拿 51 这些数试试」取到了题面 HTML 里的颜色值;「题目原话」露出 "
- 流程图 A 级的学生标题下还写着「还没交过」
- 「我的提交」上方补回「答案错误 × 3」这样的计数
- 流程图历次评分能点开看那一版的图;读分数失败不再说「还没交过流程图」;手机上能看流程图点评
- 「结果」页签有新结果没看时加粗上色,看过就恢复
- 空画布拖进第一个节点画布会跳一下;画布底下空着一条
- 比赛没开始写成「已结束」、倒计时超过一个月算错;分隔条拖到头工具栏被挤坏;题单里的题目页有备案页脚
- 流程图历次分数只读需要的几列(一个学生一道题 23 次提交:96 kB → 2 kB)
- 比赛密码、比赛没开始等几条学生看得到的英文报错换成中文

设计文档照实际做法更新了第 5.5、5.6、6、8、9、10、11 节。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 00:07:16 +08:00

755 lines
24 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 { errorCode } from "utils/api"
import { NButton, NFlex, NTag, NText } from "naive-ui"
import { useRouteQuery } from "@vueuse/router"
import {
adminRejudge,
getFlowchartSubmissions,
getSubmissions,
getTodaySubmissionCount,
retryFlowchartSubmission,
} from "oj/api"
import { parseTime } from "utils/functions"
import type { Grade as GradeValue } from "utils/types"
import type { FlowchartSubmissionListItem, LANGUAGE, SubmissionListItem } from "utils/types"
import Pagination from "shared/components/Pagination.vue"
import SubmissionResultTag from "shared/components/SubmissionResultTag.vue"
import { useBreakpoints } from "shared/composables/breakpoints"
import { usePagination } from "shared/composables/pagination"
import { useUserStore } from "shared/store/user"
import { LANGUAGE_SHOW_VALUE } from "utils/constants"
import { FlowchartSubmissionStatus } from "utils/types"
// 流程图提交的四种状态,列表里原来一列都没有 ——
// 老师分不出「还在评」和「评失败了」,两种都只是分数栏空着
const FLOWCHART_STATUS_TAG: Record<
number,
{ text: string; type: "default" | "info" | "success" | "error" }
> = {
[FlowchartSubmissionStatus.PENDING]: { text: "排队中", type: "default" },
[FlowchartSubmissionStatus.PROCESSING]: { text: "评分中", type: "info" },
[FlowchartSubmissionStatus.COMPLETED]: { text: "已完成", type: "success" },
[FlowchartSubmissionStatus.FAILED]: { text: "评分失败", type: "error" },
}
import ButtonWithSearch from "./components/ButtonWithSearch.vue"
import SubmissionLink from "./components/SubmissionLink.vue"
import Grade from "./components/Grade.vue"
import FlowchartLink from "./components/FlowchartLink.vue"
// 下面四个组件只在默认关闭的 n-modal 里用,其中两个统计面板还只有老师看得见。
// 静态 import 会把它们拖进本路由的关键路径——光 chart.js 就 197KB,进页面前必须先下完。
// 改成异步后本路由增量下载从 675KB / 59 个文件降到 360KB 出头。
const StatisticsPanel = defineAsyncComponent(() => import("shared/components/StatisticsPanel.vue"))
const FlowchartStatisticsPanel = defineAsyncComponent(
() => import("shared/components/FlowchartStatisticsPanel.vue"),
)
const TodayStatistics = defineAsyncComponent(() => import("./components/TodayStatistics.vue"))
const SubmissionDetail = defineAsyncComponent(() => import("./detail.vue"))
const FlowchartScoreDetail = defineAsyncComponent(
() => import("./components/FlowchartScoreDetail.vue"),
)
interface SubmissionQuery {
username: string
result: string
myself: "0" | "1"
problem: string
language: LANGUAGE | ""
today: "0" | "1"
}
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const message = useMessage()
const { isMobile, isDesktop } = useBreakpoints()
const submissions = ref<SubmissionListItem[]>([])
const flowcharts = ref<FlowchartSubmissionListItem[]>([])
const total = ref(0)
const todayCount = ref(0)
// 没有它的话,等接口这段时间表格就是一片空白,连转圈都没有
const loading = ref(false)
/**
* 转圈圈延迟出现。翻页、切筛选大多在一百毫秒内就回来了,直接切数据比「闪一下
* 转圈再切」看着稳;只有真的慢到 300ms 以上才值得给个反馈。
*
* 注意是**单向**延迟:只推迟「显示」,一旦请求结束立刻收掉,不留尾巴。
*
* 为什么不用 n-spin 的 delay:n-data-table 压根不走 n-spin,它渲染的是内部的
* _internal/loading,直接由 loading 控制、没有 delay 参数。要用内置的就得把表格
* 包进 <n-spin :show :delay> 再去掉 :loading —— 那会换掉加载样式,还会丢掉
* loading 时锁住排序/分页交互(DataTable 里那个 `disabled: this.loading`)。
* 下面这几行就是 n-spin 内部同样的写法(watchEffect + setTimeout + 清理)。
*/
const LOADING_DELAY = 300
const showLoading = ref(false)
let loadingTimer: ReturnType<typeof setTimeout> | undefined
watch(loading, (value) => {
clearTimeout(loadingTimer)
if (!value) {
showLoading.value = false
return
}
loadingTimer = setTimeout(() => (showLoading.value = true), LOADING_DELAY)
})
onUnmounted(() => clearTimeout(loadingTimer))
// 使用分页 composable
const { query, clearQuery } = usePagination<SubmissionQuery>({
username: useRouteQuery("username", "").value,
result: useRouteQuery("result", "").value,
myself: useRouteQuery("myself", "0").value,
problem: useRouteQuery("problem", "").value,
language: useRouteQuery("language", "").value,
today: "0",
})
/**
* 从题目页协作条「看他交过什么」点进来时带着 `exactUsername=1`:用户名按整名匹配。
* 默认是「包含」,而学号互相包含是常态(帮 ks24a1 时会混进 ks24a10–ks24a19)。
* 只对带进来的那个用户名生效,老师在输入框里改了名字就回到原来的包含匹配
*/
const exactUsernameFor =
useRouteQuery<string>("exactUsername", "").value === "1" ? query.username : ""
const submissionID = ref("")
const problemDisplayID = ref("")
const [statisticPanel, toggleStatisticPanel] = useToggle(false)
// 「今日提交数」旁边那颗「统计」按钮的弹框
const [todayPanel, toggleTodayPanel] = useToggle(false)
const [codePanel, toggleCodePanel] = useToggle(false)
const [scoreDetailPanel, toggleScoreDetailPanel] = useToggle(false)
const selectedFlowchartId = ref("")
const selectedFlowchart = computed(() => {
return flowcharts.value.find((f) => f.id === selectedFlowchartId.value)
})
const resultOptions: SelectOption[] = [
{ label: "全部", value: "" },
{ label: "答案正确", value: "0" },
{ label: "语法未通过", value: "10" },
{ label: "答案错误", value: "-1" },
{ label: "编译失败", value: "-2" },
{ label: "运行时错误", value: "4" },
]
const gradeOptions: SelectOption[] = [
{ label: "全部", value: "" },
{ label: "S级", value: "S" },
{ label: "A级", value: "A" },
{ label: "B级", value: "B" },
{ label: "C级", value: "C" },
]
const languageOptions: SelectOption[] = [
{ label: "流程图", value: "Flowchart" },
{ label: "全部语言", value: "" },
{ label: "Python", value: "Python" },
{ label: "C语言", value: "C" },
{ label: "C++", value: "C++" },
]
async function listSubmissions() {
if (query.page < 1) query.page = 1
const offset = query.limit * (query.page - 1)
loading.value = true
try {
if (query.language === "Flowchart") {
const res = await getFlowchartSubmissions({
username: query.username,
problemDisplayId: query.problem,
myself: query.myself,
offset,
limit: query.limit,
today: query.today,
grade: query.result,
})
total.value = res.total
flowcharts.value = res.results
} else {
const res = await getSubmissions({
...query,
offset,
problemDisplayId: query.problem,
contestId: (route.params.contestID as string) ?? "",
language: query.language,
today: query.today,
...(exactUsernameFor && query.username === exactUsernameFor
? { exactUsername: "1" as const }
: {}),
})
submissions.value = res.results
total.value = res.total
}
} finally {
loading.value = false
}
consumePendingJump()
}
async function getTodayCount() {
const res = await getTodaySubmissionCount(query.language)
todayCount.value = res
}
onMounted(() => {
listSubmissions()
if (route.name === "submissions") {
getTodayCount()
}
})
function search(username: string, problem: string) {
query.username = username
query.problem = problem
}
function clear() {
clearQuery()
}
async function rejudge(submissionID: string) {
await adminRejudge(submissionID)
message.success("重新判分成功")
listSubmissions()
}
async function retryFlowchart(submissionId: string) {
// 后端会拒掉「还在评分中」的提交(409 retry-not-allowed),也可能撞上限流。
// 不兜住的话拦截器静默 reject,老师点下去完全没反应。
// 按错误码分支而不是 match 文案(见 utils/api.ts 的约定)——后端文案是英文的,
// 直接弹给老师看不合适
const retryTips: Record<string, string> = {
"retry-not-allowed": "这条还在评分中,等出了结果再重新评分",
"too-many-submissions": "操作太频繁了,缓一下再试",
"flowchart-not-found": "提交不存在,或者没有权限",
}
try {
await retryFlowchartSubmission(submissionId)
} catch (err) {
message.error(retryTips[errorCode(err) ?? ""] ?? "重新评分失败")
return
}
message.success("重新评分已提交")
listSubmissions()
}
function problemClicked(row: SubmissionListItem | FlowchartSubmissionListItem) {
if (route.name === "contest submissions") {
const path = router.resolve({
name: "contest problem",
params: {
problemID: row.problemDisplayId,
},
})
window.open(path.href, "_blank")
} else {
window.open("/problem/" + row.problemDisplayId, "_blank")
}
}
// 今日统计弹框里点题目。那颗按钮只在 route.name === "submissions" 上出现
// (今日提交数本身就只在那一页拉),所以不用管比赛里的题目路由
function openProblem(displayId: string) {
window.open("/problem/" + displayId, "_blank")
}
function showCodePanel(id: string, problem: string) {
toggleCodePanel(true)
submissionID.value = id
problemDisplayID.value = problem
}
/**
* 代码详情的键盘翻阅。老师看一个班的提交时原来是「点开 → 看 → 关掉 → 再点下一行」,
* 这里让弹框开着就能上下切换,走到本页头尾自动翻页续上。
*
* 只在**能看代码**的行之间走 —— showLink 是后端按题单规则算出来的,
* 跳到一条看不了的上面只会得到一个空弹框。
*/
const viewableSubmissions = computed(() => submissions.value.filter((row) => row.showLink))
const currentCodeIndex = computed(() =>
viewableSubmissions.value.findIndex((row) => row.id === submissionID.value),
)
const maxPage = computed(() => Math.ceil(total.value / query.limit))
// 翻页是异步的,所以先记下「落地后停在哪一头」,等新一页的数据回来再开
let pendingJump: "first" | "last" | null = null
function consumePendingJump() {
const jump = pendingJump
pendingJump = null
if (!jump || !codePanel.value) return
const list = viewableSubmissions.value
const row = jump === "first" ? list[0] : list[list.length - 1]
// 整页都是看不了代码的(题单没做出来那批),此时保持弹框里原来那条不动
if (!row) {
message.info("这一页没有可以查看的代码")
return
}
showCodePanel(row.id, row.problemDisplayId)
}
function moveCodePanel(step: 1 | -1) {
const index = currentCodeIndex.value
if (index === -1) return
const next = viewableSubmissions.value[index + step]
if (next) {
showCodePanel(next.id, next.problemDisplayId)
return
}
const page = query.page + step
if (page < 1 || page > maxPage.value) return
pendingJump = step > 0 ? "first" : "last"
query.page = page // 监听器会去 listSubmissions,回来后 consumePendingJump 接手
}
// 上下(和左右)翻阅代码详情。Esc 关弹框是 n-modal 自带的,不用管。
// 弹框没开就什么都不做,页面正常滚动
onKeyStroke(["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"], (e: KeyboardEvent) => {
if (!codePanel.value) return
// 焦点在输入框里(弹框底下那几个筛选框)时不抢方向键
const target = e.target as HTMLElement | null
if (
target &&
(target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable)
) {
return
}
e.preventDefault()
moveCodePanel(e.key === "ArrowUp" || e.key === "ArrowLeft" ? -1 : 1)
})
function showScoreDetail(id: string) {
selectedFlowchartId.value = id
toggleScoreDetailPanel(true)
}
function getGradeType(grade?: string) {
if (!grade) return "default"
if (grade === "S") return "primary"
if (grade === "A") return "info"
if (grade === "B") return "warning"
return "error"
}
// 监听用户名和题号变化(防抖)
watchDebounced(() => [query.username, query.problem], listSubmissions, {
debounce: 500,
maxWait: 1000,
})
// 监听其他查询条件变化
watch(
() => [query.page, query.limit, query.myself, query.result, query.language, query.today],
listSubmissions,
)
// 切换语言时重置过滤条件,刷新今日提交数
watch(
() => query.language,
() => {
query.result = ""
if (route.name === "submissions") getTodayCount()
},
)
// 登录状态变化后刷新提交列表,更新提交编号列的可点击状态。
// 今日提交数不看登录态,onMounted 那次就够了,这里不用再拉一遍。
watch(
() => userStore.isAuthed,
() => listSubmissions(),
)
const columns = computed(() => {
const res: DataTableColumn<SubmissionListItem>[] = [
{
title: "提交时间",
key: "create_time",
minWidth: 200,
render: (row) => parseTime(row.createTime, "YYYY-MM-DD HH:mm:ss"),
},
{
title: "提交编号",
key: "id",
minWidth: 200,
render: (row) =>
h(SubmissionLink, {
submission: row,
onShowCode: () => showCodePanel(row.id, row.problemDisplayId),
}),
},
{
title: "状态",
key: "status",
minWidth: 140,
render: (row) => h(SubmissionResultTag, { result: row.result }),
},
{
title: "题目",
key: "problem",
minWidth: 360,
render: (row) => {
const problem = h(
ButtonWithSearch,
{
type: "题目",
onClick: () => problemClicked(row),
onSearch: () => (query.problem = row.problemDisplayId),
},
() => `${row.problemDisplayId} ${row.problemTitle}`,
)
// 从题单入口做出来的提交才有这个标记(后端只在提交时记来源),
// 同一道题从普通题库刷的不会带。老提交里只有当年首次 AC 那条有
const problemSet = row.problemSet
if (!problemSet) return problem
return h(NFlex, { align: "center", size: 8, wrap: false }, () => [
problem,
h(
NTag,
{
size: "small",
round: true,
type: "info",
bordered: false,
style: { cursor: "pointer", flexShrink: 0 },
onClick: () => window.open("/problemset/" + problemSet.id, "_blank"),
},
() => "题单 " + problemSet.title,
),
])
},
},
{
title: "语言",
key: "language",
minWidth: 120,
render: (row) => LANGUAGE_SHOW_VALUE[row.language],
},
{
title: "用户",
key: "username",
minWidth: 200,
render: (row) =>
h(
ButtonWithSearch,
{
type: "用户",
username: row.username,
onClick: () => window.open("/user?name=" + row.username, "_blank"),
onSearch: () => (query.username = row.username),
onFilterClass: (classname: string) => (query.username = classname),
},
() => row.username,
),
},
]
if (!route.params.contestID && userStore.isTeacherOrAbove) {
res.push({
title: "选项",
key: "rejudge",
render: (row) =>
h(
NButton,
{
quaternary: true,
size: "small",
type: "primary",
onClick: () => rejudge(row.id),
},
() => "重新判题",
),
})
}
return res
})
const flowchartColumns = computed(() => {
const res: DataTableColumn<FlowchartSubmissionListItem>[] = [
{
title: "提交时间",
key: "create_time",
render: (row) => parseTime(row.createTime, "YYYY-MM-DD HH:mm:ss"),
},
{
title: "提交编号",
key: "id",
render: (row) =>
h(FlowchartLink, {
flowchart: row,
onShowDetail: (id: string) => showScoreDetail(id),
}),
},
{
title: "题目",
key: "problem_title",
render: (row) =>
h(
ButtonWithSearch,
{
type: "题目",
onClick: () => problemClicked(row),
onSearch: () => (query.problem = row.problemDisplayId),
},
() => `${row.problemDisplayId} ${row.problemTitle}`,
),
},
{
title: "状态",
key: "status",
render: (row) => {
const tag = FLOWCHART_STATUS_TAG[row.status]
return h(
NTag,
{ size: "small", round: true, type: tag?.type ?? "default" },
() => tag?.text ?? "未知",
)
},
},
{
title: "评分",
key: "ai_score",
// 只有评完的才有分数。没评完也渲染 Grade 的话会显示成 0 分,
// 看着像「评了但得了 0 分」
render: (row) =>
row.status === FlowchartSubmissionStatus.COMPLETED
? h(Grade, {
score: row.aiScore ?? 0,
grade: (row.aiGrade ?? "") as GradeValue,
})
: h(NText, { depth: 3 }, () => "—"),
},
{
title: "用户",
key: "username",
minWidth: 200,
render: (row) =>
h(
ButtonWithSearch,
{
type: "用户",
username: row.username,
onClick: () => window.open("/user?name=" + row.username, "_blank"),
onSearch: () => (query.username = row.username),
onFilterClass: (classname: string) => (query.username = classname),
},
() => row.username,
),
},
]
if (!route.params.contestID && userStore.isTeacherOrAbove) {
res.push({
title: "选项",
key: "retry",
// 后端只接受已完成 / 已失败的重判(其余返回 409),这里同步置灰,
// 免得老师点了才发现不行
render: (row) =>
h(
NButton,
{
quaternary: true,
size: "small",
type: "primary",
disabled:
row.status !== FlowchartSubmissionStatus.COMPLETED &&
row.status !== FlowchartSubmissionStatus.FAILED,
onClick: () => retryFlowchart(row.id),
},
() => "重新判题",
),
})
}
return res
})
</script>
<template>
<n-flex vertical size="large">
<n-space>
<n-form :show-feedback="false" inline label-placement="left">
<n-form-item v-if="isDesktop && userStore.isAuthed" label="只看自己">
<n-switch v-model:value="query.myself" checked-value="1" unchecked-value="0" />
</n-form-item>
<n-form-item label="语言" v-if="route.name !== 'contest submissions'">
<n-select class="select" v-model:value="query.language" :options="languageOptions" />
</n-form-item>
<n-form-item :label="query.language === 'Flowchart' ? '等级' : '状态'">
<n-select
class="select"
v-model:value="query.result"
:options="query.language === 'Flowchart' ? gradeOptions : resultOptions"
/>
</n-form-item>
</n-form>
<n-form :show-feedback="false" inline label-placement="left">
<n-form-item>
<n-input
:disabled="query.myself === '1'"
style="width: 140px"
clearable
v-model:value="query.username"
placeholder="用户"
/>
</n-form-item>
<n-form-item>
<n-input
style="width: 120px"
clearable
v-model:value="query.problem"
placeholder="题号"
/>
</n-form-item>
</n-form>
<n-form :show-feedback="false" inline label-placement="left">
<n-form-item v-if="isMobile && userStore.isAuthed" label="只看自己">
<n-switch v-model:value="query.myself" checked-value="1" unchecked-value="0" />
</n-form-item>
<n-form-item>
<n-button @click="search(query.username, query.problem)"> 搜索 </n-button>
</n-form-item>
<n-form-item>
<n-button @click="clear" quaternary>重置</n-button>
</n-form-item>
<n-form-item v-if="userStore.isTeacherOrAbove && route.name === 'submissions'">
<n-button quaternary type="warning" @click="toggleStatisticPanel(true)">
数据统计
</n-button>
</n-form-item>
</n-form>
<n-flex v-if="todayCount > 0" align="center" :size="8">
<n-tag
checkable
:checked="query.today === '1'"
type="success"
size="large"
@update:checked="(v: boolean) => (query.today = v ? '1' : '0')"
>
<n-gradient-text v-if="query.today !== '1'" type="success">
今日提交数:{{ todayCount }}
</n-gradient-text>
<template v-else>今日提交数:{{ todayCount }}</template>
</n-tag>
<!--
筛到今天之后才出现。流程图那档不给 —— 这个统计只算代码提交
(流程图提交在另一张表、只有 AI 评级没有判题状态),点开会是一份
对不上标签数字的统计。
-->
<n-button
v-if="query.today === '1' && query.language !== 'Flowchart'"
quaternary
type="success"
@click="toggleTodayPanel(true)"
>
统计
</n-button>
</n-flex>
</n-space>
<n-data-table
v-if="query.language === 'Flowchart'"
:bordered="false"
:columns="flowchartColumns"
:data="flowcharts"
:loading="showLoading"
/>
<n-data-table
v-else
:bordered="false"
:columns="columns"
:data="submissions"
:loading="showLoading"
/>
</n-flex>
<Pagination :total="total" v-model:limit="query.limit" v-model:page="query.page" />
<n-modal
v-if="userStore.isTeacherOrAbove"
v-model:show="statisticPanel"
preset="card"
:style="{ maxWidth: isDesktop && '800px', maxHeight: '80vh' }"
:content-style="{ overflow: 'auto' }"
:title="query.language === 'Flowchart' ? '流程图提交的统计' : '提交记录的统计'"
>
<FlowchartStatisticsPanel
v-if="query.language === 'Flowchart'"
:problem="query.problem"
:username="query.username"
/>
<StatisticsPanel v-else :problem="query.problem" :username="query.username" />
</n-modal>
<n-modal
v-model:show="todayPanel"
preset="card"
:style="{ maxWidth: isDesktop && '700px', maxHeight: '80vh' }"
:content-style="{ overflow: 'auto' }"
title="今日提交统计"
>
<TodayStatistics @open-problem="(id: string) => openProblem(id)" />
</n-modal>
<n-modal
v-model:show="codePanel"
preset="card"
:style="{ maxWidth: isDesktop && '70vw', maxHeight: '80vh' }"
:content-style="{ overflow: 'auto' }"
>
<template #header>
<n-flex align="center" :size="12">
<n-text>代码详情</n-text>
<n-text v-if="isDesktop && viewableSubmissions.length > 1" depth="3">
<span class="shortcut-hint">
本页第 {{ currentCodeIndex + 1 }} / {{ viewableSubmissions.length }}
条 · ↑ ↓ ← → 切换上下一条 · Esc 关闭
</span>
</n-text>
</n-flex>
</template>
<!--
key 换掉才会重新拉代码 —— detail.vue 只在 onMounted 里取一次,
键盘切换时不换 key 的话弹框里还是上一条的代码
-->
<SubmissionDetail
:key="submissionID"
:problemID="problemDisplayID"
:submissionID="submissionID"
hideList
@copied="toggleCodePanel(false)"
/>
</n-modal>
<n-modal
v-model:show="scoreDetailPanel"
preset="card"
:style="{ maxWidth: isDesktop && '1000px', maxHeight: '80vh' }"
:content-style="{ overflow: 'auto' }"
>
<template #header>
<n-flex align="center">
<n-text>流程图评分详情</n-text>
<n-text v-if="selectedFlowchart" :type="getGradeType(selectedFlowchart.aiGrade ?? '')">
{{ selectedFlowchart.aiScore }}分 {{ selectedFlowchart.aiGrade }}级
</n-text>
</n-flex>
</template>
<FlowchartScoreDetail :submissionId="selectedFlowchartId" />
</n-modal>
</template>
<style scoped>
.select {
width: 120px;
}
.code {
font-size: 20px;
overflow: auto;
}
.shortcut-hint {
font-size: 13px;
font-weight: normal;
}
.flowchart-iframe {
width: 100%;
height: 100%;
border: none;
display: block;
}
</style>