From 1fcd12f6f4ed809b8c82432a1b8ccf81ba1c9ded Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Tue, 13 Oct 2026 13:04:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E4=B8=BE=E6=89=8B=E4=B8=8E=E5=8D=8F?= =?UTF-8?q?=E4=BD=9C):=20=E9=A1=B6=E6=A0=8F=E3=80=8CN=20=E4=BA=BA=E4=B8=BE?= =?UTF-8?q?=E6=89=8B=E3=80=8D=E7=82=B9=E5=BC=80=E6=8C=89=E9=A2=98=E5=88=86?= =?UTF-8?q?=E7=BB=84=E5=B8=A6=E9=94=99=E6=B3=95=EF=BC=9B=E7=9C=8B=E6=9D=BF?= =?UTF-8?q?=E5=A4=9A=E4=B8=80=E5=88=97=E3=80=8C=E5=9C=A8=E5=86=99=E3=80=8D?= =?UTF-8?q?=E7=82=B9=E4=BA=86=E5=B0=B1=E8=83=BD=E7=9C=8B=E4=BB=96=E7=9A=84?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=EF=BC=9B=E5=B8=AE=E5=AE=8C=E4=BA=86=E5=9B=9E?= =?UTF-8?q?=E7=9C=8B=E6=9D=BF=E3=80=81=E8=BF=98=E6=9C=89=E4=BA=BA=E5=B0=B1?= =?UTF-8?q?=E3=80=8C=E4=B8=8B=E4=B8=80=E4=B8=AA=E3=80=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 学生那边按钮写「xuyue 在帮你 / 在看你的代码」。顺带修了老师接单后编辑器里留着自己的 草稿或模板、学生的代码插在前面、两边从此不一致的问题。 Co-Authored-By: Claude Opus 5.5 --- apps/web/src/oj/classroom/board.vue | 118 ++++++- .../src/oj/problem/components/CollabBar.vue | 90 ++++- .../oj/problem/components/EditorToolbar.vue | 31 +- apps/web/src/oj/problem/detail.vue | 28 +- apps/web/src/shared/components/CollabHost.vue | 25 +- apps/web/src/shared/components/HelpButton.vue | 32 +- .../src/shared/components/HelpRequestList.vue | 311 +++++++++++------- .../src/shared/components/SyncCodeEditor.vue | 12 + apps/web/src/shared/composables/collabDoc.ts | 12 +- .../web/src/shared/composables/teacherHelp.ts | 34 ++ apps/web/src/shared/composables/websocket.ts | 5 + apps/web/src/shared/store/collab.ts | 78 ++++- .../2026-08-28-collab-help-request-design.md | 23 ++ 13 files changed, 621 insertions(+), 178 deletions(-) create mode 100644 apps/web/src/shared/composables/teacherHelp.ts diff --git a/apps/web/src/oj/classroom/board.vue b/apps/web/src/oj/classroom/board.vue index 0ea3a57..8d76edb 100644 --- a/apps/web/src/oj/classroom/board.vue +++ b/apps/web/src/oj/classroom/board.vue @@ -6,6 +6,7 @@ import { useTone } from "oj/submission/composables/tone" import { groupClassOptions } from "oj/submission/utils" import UserName from "shared/components/UserName.vue" import { useLeaveStudents } from "shared/composables/leaveStudents" +import { useTeacherHelp } from "shared/composables/teacherHelp" import { useCollabStore } from "shared/store/collab" import { useConfigStore } from "shared/store/config" import type { LessonLanguage } from "@oj2/contract" @@ -40,9 +41,18 @@ const message = useMessage() const theme = useThemeVars() const tone = useTone() const router = useRouter() +const route = useRoute() +const { help, peek } = useTeacherHelp() const board = ref(null) -const className = ref(null) +/** + * 带着 ?className= 进来的(协作条「帮完了」回来、举手面板「讲这道题」)就看那个班, + * 否则第一次由后端猜 + */ +const queryText = (value: unknown) => (typeof value === "string" && value ? value : null) +const className = ref(queryText(route.query.className)) +/** ?problem= 带来的题号:加载完选中那道题,下面展开它的错法(「讲这道题」) */ +let wantedProblem = queryText(route.query.problem) const classOptions = computed(() => { const list = configStore.config?.classList ?? [] @@ -56,6 +66,12 @@ async function load() { board.value = data // 第一次由后端猜,之后就固定在这个班上,别每次刷新都换 className.value = data.className + if (wantedProblem) { + const wanted = wantedProblem.toLowerCase() + const hit = data.problems.find((item) => item.problemDisplayId.toLowerCase() === wanted) + if (hit) selectedId.value = hit.problemId + wantedProblem = null + } } catch (error) { message.error(errorMessage(error)) } @@ -69,6 +85,26 @@ function changeClass(value: string) { } onMounted(load) +// 已经停在看板上时又从举手面板点了「讲这道题」:同一条路由只换 query,组件不重建 +watch( + () => route.query, + (query) => { + wantedProblem = queryText(query.problem) + const next = queryText(query.className) + if (next && next !== className.value) { + className.value = next + editing.value = false + } + load() + }, +) + +/** 举手多久了,每秒走一格 */ +const now = useNow({ scheduler: (cb) => useIntervalFn(cb, 1000) }) +function waitedText(createdAt: number) { + const seconds = Math.max(0, Math.floor((now.value.getTime() - createdAt) / 1000)) + return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, "0")}` +} const { pause } = useIntervalFn(load, REFRESH_MS) onUnmounted(pause) @@ -637,6 +673,7 @@ function shortTitle(title: string, size: number) { 学生 + 在写