From 0c2ff68e9be34711b2a0d06f3ff0ceaf25ef2fa9 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Mon, 28 Sep 2026 18:28:53 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=8D=8F=E4=BD=9C):=20=E8=80=81=E5=B8=88?= =?UTF-8?q?=E5=8D=8F=E4=BD=9C=E6=97=B6=E5=8F=B3=E6=A0=8F=E9=A1=B6=E4=B8=8A?= =?UTF-8?q?=E4=B8=80=E6=9D=A1=E5=8D=8F=E4=BD=9C=E6=9D=A1=EF=BC=8C=E7=BB=93?= =?UTF-8?q?=E6=9E=9C=E9=A1=B5=E7=AD=BE=E5=85=88=E6=91=86=E5=AD=A6=E7=94=9F?= =?UTF-8?q?=E6=9C=80=E8=BF=91=E4=B8=80=E6=AC=A1=E4=BA=A4=E7=9A=84=EF=BC=8C?= =?UTF-8?q?=E6=8A=BD=E5=B1=89=E5=8F=98=E6=88=90=E3=80=8C=E4=BB=96=E7=9A=84?= =?UTF-8?q?=E6=8F=90=E4=BA=A4=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 协作条:「正在帮 谁 · 语言 · 看他交过什么 ↗ · 结束协作」,从工具栏里挪出来,一眼看得出 编辑器里是学生的代码。「看他交过什么」带上学生用户名、开新标签(原来只带题号,还得再筛一遍) - 老师接单后「结果」页签原来是空的:现在摆学生在这道题上最近一次交的结果,可刷新; 不给 AI 提示(那是按登录的人自己的失败次数解锁的) - 抽屉「我的提交」在协作中变成「他的提交」,列的是学生的提交,只能复制、不能放回编辑器 (放回去会经协作通道直接盖掉学生正在写的代码),也不显示老师自己的排名 - 学生排队求助中、或者老师正在帮他时换题:先确认一句。换题会断开协作 / 撤掉举手, 原来一点「下一题」就把正在帮他的老师踢出去了 - 求助按钮文案:「举手求助」→「已举手 · 前面 2 人」→「某某 老师在帮你」 - 提交列表加 exactUsername=1:用户名精确匹配。原来只有「包含」,ks24a1 会把 ks24a10 的提交 也捞进来 Co-Authored-By: Claude Opus 5.5 --- apps/api/src/routes/submission-statistics.ts | 13 ++++ apps/api/src/routes/submission.ts | 13 +++- .../src/oj/problem/components/CollabBar.vue | 77 +++++++++++++++++++ .../oj/problem/components/EditorToolbar.vue | 20 +---- .../src/oj/problem/components/PeerLatest.vue | 75 ++++++++++++++++++ .../oj/problem/components/ProblemDrawer.vue | 19 +++-- .../oj/problem/components/ProblemEditor.vue | 10 ++- .../problem/components/ProblemSubmission.vue | 27 ++++--- .../src/oj/problem/components/ResultPane.vue | 6 ++ .../problem/components/SubmissionResult.vue | 10 ++- .../oj/problem/composables/teacherCollab.ts | 19 +++++ apps/web/src/oj/problem/detail.vue | 41 +++++++++- apps/web/src/utils/types.ts | 2 + 13 files changed, 287 insertions(+), 45 deletions(-) create mode 100644 apps/web/src/oj/problem/components/CollabBar.vue create mode 100644 apps/web/src/oj/problem/components/PeerLatest.vue create mode 100644 apps/web/src/oj/problem/composables/teacherCollab.ts diff --git a/apps/api/src/routes/submission-statistics.ts b/apps/api/src/routes/submission-statistics.ts index 7621d7f..52ae43d 100644 --- a/apps/api/src/routes/submission-statistics.ts +++ b/apps/api/src/routes/submission-statistics.ts @@ -296,6 +296,19 @@ async function astOnlyByUser(where: SQL | undefined, userIds: number[]) { * 快照实测 count 65ms → 0.6ms。`ks2` 这种匹配上千个账号的宽前缀退回扫表,30~50ms, * 和原来持平。 */ +/** + * 用户名**精确**匹配。usernameFilter 是「包含」,学号互相包含是常态(ks24a1 包含在 ks24a10 里), + * 协作中老师看「这个学生」的提交时不能把别人的也捞进来。查无此人留恒假条件 + */ +export async function exactUsernameFilter(username: string) { + const [row] = await db + .select({ id: schema.user.id }) + .from(schema.user) + .where(eq(schema.user.username, username)) + .limit(1) + return row ? eq(schema.submission.userId, row.id) : sql`false` +} + export async function usernameFilter(username: string) { const like = `%${username}%` const users = await db diff --git a/apps/api/src/routes/submission.ts b/apps/api/src/routes/submission.ts index aef4091..56a545e 100644 --- a/apps/api/src/routes/submission.ts +++ b/apps/api/src/routes/submission.ts @@ -32,7 +32,12 @@ import { problemSetJoinTimes } from "../services/problemset" import { consumeToken } from "../services/throttling" import { dayStart } from "../time" import { asFilterValue, asRecord, isAdminRole, queryInteger } from "./helpers" -import { problemFilter, submissionStatisticsRoutes, usernameFilter } from "./submission-statistics" +import { + exactUsernameFilter, + problemFilter, + submissionStatisticsRoutes, + usernameFilter, +} from "./submission-statistics" export const submissionRoutes = new Hono() @@ -489,7 +494,11 @@ submissionRoutes.get("/submissions", optionalAuth, async (c) => { filters.push( ...(await Promise.all([ displayId ? problemFilter(displayId, null) : undefined, - username ? usernameFilter(username) : undefined, + username + ? c.req.query("exactUsername") === "1" + ? exactUsernameFilter(username) + : usernameFilter(username) + : undefined, ])), ) if (myself) filters.push(eq(schema.submission.userId, myself.id)) diff --git a/apps/web/src/oj/problem/components/CollabBar.vue b/apps/web/src/oj/problem/components/CollabBar.vue new file mode 100644 index 0000000..3330b0b --- /dev/null +++ b/apps/web/src/oj/problem/components/CollabBar.vue @@ -0,0 +1,77 @@ + + + + + diff --git a/apps/web/src/oj/problem/components/EditorToolbar.vue b/apps/web/src/oj/problem/components/EditorToolbar.vue index 694ffc6..dccf4b8 100644 --- a/apps/web/src/oj/problem/components/EditorToolbar.vue +++ b/apps/web/src/oj/problem/components/EditorToolbar.vue @@ -108,14 +108,14 @@ const showCollabBar = computed(() => collabHere.value && userStore.isTeacherOrAb const helpButtonText = computed(() => { if (collabStore.helpStatus === "active") { const name = collabStore.teacherName - return name ? `${name} 老师帮你中` : "老师正在帮你" + return name ? `${name} 老师在帮你` : "老师正在帮你" } if (collabStore.helpStatus === "pending") { return collabStore.queueAhead > 0 - ? `已求助 · 前面 ${collabStore.queueAhead} 人` - : "已求助 · 待接入" + ? `已举手 · 前面 ${collabStore.queueAhead} 人` + : "已举手 · 老师马上来" } - return "求助" + return "举手求助" }) const helpButtonType = computed(() => { @@ -301,18 +301,6 @@ const goEdit = () => { 更多 - - +import { storeToRefs } from "pinia" +import { getSubmission, getSubmissions } from "oj/api" +import { useProblemStore } from "oj/store/problem" +import { useCollabStore } from "shared/store/collab" +import { parseTime } from "utils/functions" +import type { Submission } from "utils/types" + +/** + * 老师接单之后落在题目页,「结果」页签原来是空的 —— 学生多半是交了几次没过才举的手, + * 老师最先想知道的就是「他交上去是什么结果」。这里摆出学生最近一次交的(设计文档第 9 节)。 + * 管理员角色本来就能看任何人的提交,不需要新接口。 + */ +const SubmissionResult = defineAsyncComponent(() => import("./SubmissionResult.vue")) + +const collabStore = useCollabStore() +const { problem } = storeToRefs(useProblemStore()) + +const loading = ref(false) +const failed = ref(false) +const latest = ref(null) +const checked = ref(false) + +const peer = computed(() => collabStore.room?.peerName ?? "") + +async function load() { + if (!peer.value || !problem.value) return + loading.value = true + failed.value = false + try { + const list = await getSubmissions({ + username: peer.value, + exactUsername: "1", + problemDisplayId: problem.value._id, + limit: 1, + offset: 0, + }) + const row = list.results[0] + latest.value = row ? await getSubmission(row.id) : null + checked.value = true + } catch { + failed.value = true + } finally { + loading.value = false + } +} + +watch([peer, () => problem.value?._id], load, { immediate: true }) + + + + + diff --git a/apps/web/src/oj/problem/components/ProblemDrawer.vue b/apps/web/src/oj/problem/components/ProblemDrawer.vue index eb2a7a4..abb95f2 100644 --- a/apps/web/src/oj/problem/components/ProblemDrawer.vue +++ b/apps/web/src/oj/problem/components/ProblemDrawer.vue @@ -1,13 +1,13 @@