From 4abaf9c7e41595e093a227b78175567944952666 Mon Sep 17 00:00:00 2001 From: yuetsh <517252939@qq.com> Date: Sun, 30 Aug 2026 09:38:04 -0600 Subject: [PATCH] =?UTF-8?q?fix(collab):=20=E5=8A=A8=E6=80=81=20import=20?= =?UTF-8?q?=E7=AB=9E=E6=80=81=E3=80=81=E5=88=87=E8=B5=B0=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E7=9A=84=E8=AF=AD=E4=B9=89=E3=80=81=E6=BC=94=E7=A4=BA=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E7=9A=84=E8=BA=AB=E4=BB=BD=E9=94=99=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - collabDoc.start 要 await 六个动态 import,房间可能在这期间就关了(机房首次 加载 y* 那几个 chunk 正是最慢的时候)。stop() 先跑完面对的是 doc === null, 什么也拆不到;import 回来之后 start 的后半段照样建文档、装 binaryHandler、 挂 yCollab、发 SyncStep1——给一个不存在的房间。加会话代号,过期就整个放弃。 - SyncCodeEditor 卸载(切走题目页、把语言切成流程图)原来只是悄悄 stop(), 房间和 active 都还留着,老师那边模态框照开、字照敲,一个也到不了;watch 没有 immediate,学生切回来也不会重建。改成明确 leave() 结束协作。 另外 @ready 也作为起点之一:学生排队时切走、老师这期间接了单,切回来能接上。 - 演示模式下 isTeacherOrAbove 被强制 false,服务端却按库里的 adminType 照样 把他算作在线老师:学生因此拿到 pending 而不是 no_teacher,排队等一个顶栏里 根本没有求助列表的人;他自己看到的求助按钮点下去,服务端回「教师不能发起求助」。 两头都不对,索性对演示模式整个关掉这个功能。 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016DHxhKNxXfG89JnVzHbvgj --- apps/web/src/App.vue | 14 ++++++--- apps/web/src/oj/problem/components/Form.vue | 10 ++----- .../src/shared/components/SyncCodeEditor.vue | 29 ++++++++++++------- apps/web/src/shared/composables/collabDoc.ts | 17 +++++++++++ 4 files changed, 48 insertions(+), 22 deletions(-) diff --git a/apps/web/src/App.vue b/apps/web/src/App.vue index 31f36d7..83eeda6 100644 --- a/apps/web/src/App.vue +++ b/apps/web/src/App.vue @@ -26,11 +26,17 @@ useConfigUpdate() useMaxKB() // 课堂求助通道。和 /ws/config 一样是全局常驻的:老师可能正在后台改题时 -// 收到求助,学生也要在排队期间一直挂着,所以不放在题目页里起落 +// 收到求助,学生也要在排队期间一直挂着,所以不放在题目页里起落。 +// +// 演示模式下整个关掉。它是超管把界面伪装成学生用来投屏的,而服务端只认库里的 +// 真实身份:连着的话这个人会被算进「在线老师」,学生因此拿到 pending 而不是 +// no_teacher,排队等一个顶栏里根本没有求助列表的人;反过来他自己看到的求助 +// 按钮点下去,服务端回的是「教师不能发起求助」。两头都不对,索性对演示模式 +// 关闭这个功能(Form.vue 的按钮同步隐藏)。 watch( - () => userStore.isAuthed, - (isAuthed) => { - if (isAuthed) collabStore.connect() + () => userStore.isAuthed && !userStore.demoMode, + (available) => { + if (available) collabStore.connect() else collabStore.disconnect() }, { immediate: true }, diff --git a/apps/web/src/oj/problem/components/Form.vue b/apps/web/src/oj/problem/components/Form.vue index 73e1dbe..bb62a4c 100644 --- a/apps/web/src/oj/problem/components/Form.vue +++ b/apps/web/src/oj/problem/components/Form.vue @@ -58,6 +58,8 @@ const showHelpButton = computed( isDesktop.value && userStore.isAuthed && !userStore.isTeacherOrAbove && + // 演示模式下协作通道是断开的(见 App.vue),按钮点了也没人收 + !userStore.demoMode && codeStore.code.language !== "Flowchart" && !isContestMode.value, ) @@ -74,14 +76,6 @@ const toggleHelp = () => { collabStore.requestHelp(problem.value!._id) } -// 服务端的一次性提示(没有老师在线、老师取消了求助) -watch( - () => collabStore.notice, - (text) => { - if (text) message.info(collabStore.consumeNotice()) - }, -) - const showGoSubmissionButton = computed(() => { if (isContestMode.value) return true else if (userStore.isAdminRole) return true diff --git a/apps/web/src/shared/components/SyncCodeEditor.vue b/apps/web/src/shared/components/SyncCodeEditor.vue index 090503b..989a9fd 100644 --- a/apps/web/src/shared/components/SyncCodeEditor.vue +++ b/apps/web/src/shared/components/SyncCodeEditor.vue @@ -68,8 +68,17 @@ interface EditorReadyPayload { // 项目里旧的 sync.ts 用的是裸变量,同一个道理,这里换成 shallowRef 规避。 const editorView = shallowRef(null) +const bind = (view: EditorView) => { + if (collabStore.isTeacher || !collabStore.room) return + // 学生端:当前编辑器内容就是内容源 + start({ editorView: view, seedContent: view.state.doc.toString() }) +} + const handleEditorReady = (payload: EditorReadyPayload) => { editorView.value = payload.view + // 也从这里起:学生排队时切去看提交记录、老师在这期间接了单, + // 等他切回来时 room 早就非空了,只靠下面的 watch 是等不到的 + bind(payload.view) } // 房间开了才建文档。学生点求助时什么都不做 —— 老师没来之前不该动他的编辑器。 @@ -82,19 +91,19 @@ const handleEditorReady = (payload: EditorReadyPayload) => { watch( () => collabStore.room, (room) => { - if (room && !collabStore.isTeacher && editorView.value) { - // 学生端:当前编辑器内容就是内容源 - start({ - editorView: editorView.value, - seedContent: editorView.value.state.doc.toString(), - }) - } else { - stop() - } + if (room && !collabStore.isTeacher && editorView.value) bind(editorView.value) + else stop() }, ) -onUnmounted(stop) +onUnmounted(() => { + stop() + // 这个组件卸载意味着编辑器没了(切走页面、或者把语言切成流程图), + // CRDT 会话没法接着用:回来时只能新建 Y.Doc,再拿编辑器内容当种子就会和 + // 老师那份合并成重复文本。所以不是"悄悄把绑定拆了",而是明确结束协作 —— + // 否则老师那边模态框照开、字照敲,一个也到不了学生那里 + if (collabStore.room && !collabStore.isTeacher) collabStore.leave() +})