From ad858ed864cdb3a156242ab06a08601c0bd6e54e Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Wed, 16 Sep 2026 19:26:51 -0600 Subject: [PATCH] =?UTF-8?q?feat(=E6=8F=90=E4=BA=A4=E5=88=97=E8=A1=A8):=20?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=E8=AF=A6=E6=83=85=E5=BC=B9=E6=A1=86=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=96=B9=E5=90=91=E9=94=AE=E7=BF=BB=E9=98=85=EF=BC=8C?= =?UTF-8?q?=E5=88=B0=E5=A4=B4=E8=87=AA=E5=8A=A8=E7=BF=BB=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 老师看一个班的提交原来得「点开 → 看 → 关掉 → 点下一行」,现在弹框开着就能 用 ↑↓(←→ 等价)切上一条/下一条,Esc 关闭。 - 只在 showLink 为真的行之间走,跳到看不了的行只会得到空弹框 - 走到本页头尾自动翻页续上:向下落到下一页第一条,向上落到上一页最后一条。 翻页是异步的,先记 pendingJump,等 listSubmissions() 回来再开; 整页都看不了代码时保持原来那条不动,给一句提示 - 弹框没开不接管方向键,焦点在输入框里也不抢 - SubmissionDetail 加 :key="submissionID" —— 它只在 onMounted 拉一次代码, 不换 key 切过去还是上一条的代码 Co-Authored-By: Claude Opus 5 --- apps/web/src/oj/submission/list.vue | 90 ++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/apps/web/src/oj/submission/list.vue b/apps/web/src/oj/submission/list.vue index 046c40b..a75e4a0 100644 --- a/apps/web/src/oj/submission/list.vue +++ b/apps/web/src/oj/submission/list.vue @@ -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="代码详情" > + + { overflow: auto; } +.shortcut-hint { + font-size: 13px; + font-weight: normal; +} + .flowchart-iframe { width: 100%; height: 100%;