feat(提交列表): 代码详情弹框支持方向键翻阅,到头自动翻页
Some checks failed
Deploy / deploy (push) Has been cancelled

老师看一个班的提交原来得「点开 → 看 → 关掉 → 点下一行」,现在弹框开着就能
用 ↑↓(←→ 等价)切上一条/下一条,Esc 关闭。

- 只在 showLink 为真的行之间走,跳到看不了的行只会得到空弹框
- 走到本页头尾自动翻页续上:向下落到下一页第一条,向上落到上一页最后一条。
  翻页是异步的,先记 pendingJump,等 listSubmissions() 回来再开;
  整页都看不了代码时保持原来那条不动,给一句提示
- 弹框没开不接管方向键,焦点在输入框里也不抢
- SubmissionDetail 加 :key="submissionID" —— 它只在 onMounted 拉一次代码,
  不换 key 切过去还是上一条的代码

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-16 19:26:51 -06:00
parent 88b695d34f
commit ad858ed864

View File

@@ -183,6 +183,7 @@ async function listSubmissions() {
} finally {
loading.value = false
}
consumePendingJump()
}
async function getTodayCount() {
@@ -258,6 +259,73 @@ function showCodePanel(id: string, problem: string) {
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.problem)
}
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.problem)
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)
@@ -675,9 +743,24 @@ const flowchartColumns = computed(() => {
preset="card"
:style="{ maxWidth: isDesktop && '70vw', maxHeight: '80vh' }"
:content-style="{ overflow: 'auto' }"
title="代码详情"
>
<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
@@ -714,6 +797,11 @@ const flowchartColumns = computed(() => {
overflow: auto;
}
.shortcut-hint {
font-size: 13px;
font-weight: normal;
}
.flowchart-iframe {
width: 100%;
height: 100%;