fix(collab): 动态 import 竞态、切走页面的语义、演示模式的身份错位
- 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016DHxhKNxXfG89JnVzHbvgj
This commit is contained in:
+10
-4
@@ -26,11 +26,17 @@ useConfigUpdate()
|
|||||||
useMaxKB()
|
useMaxKB()
|
||||||
|
|
||||||
// 课堂求助通道。和 /ws/config 一样是全局常驻的:老师可能正在后台改题时
|
// 课堂求助通道。和 /ws/config 一样是全局常驻的:老师可能正在后台改题时
|
||||||
// 收到求助,学生也要在排队期间一直挂着,所以不放在题目页里起落
|
// 收到求助,学生也要在排队期间一直挂着,所以不放在题目页里起落。
|
||||||
|
//
|
||||||
|
// 演示模式下整个关掉。它是超管把界面伪装成学生用来投屏的,而服务端只认库里的
|
||||||
|
// 真实身份:连着的话这个人会被算进「在线老师」,学生因此拿到 pending 而不是
|
||||||
|
// no_teacher,排队等一个顶栏里根本没有求助列表的人;反过来他自己看到的求助
|
||||||
|
// 按钮点下去,服务端回的是「教师不能发起求助」。两头都不对,索性对演示模式
|
||||||
|
// 关闭这个功能(Form.vue 的按钮同步隐藏)。
|
||||||
watch(
|
watch(
|
||||||
() => userStore.isAuthed,
|
() => userStore.isAuthed && !userStore.demoMode,
|
||||||
(isAuthed) => {
|
(available) => {
|
||||||
if (isAuthed) collabStore.connect()
|
if (available) collabStore.connect()
|
||||||
else collabStore.disconnect()
|
else collabStore.disconnect()
|
||||||
},
|
},
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ const showHelpButton = computed(
|
|||||||
isDesktop.value &&
|
isDesktop.value &&
|
||||||
userStore.isAuthed &&
|
userStore.isAuthed &&
|
||||||
!userStore.isTeacherOrAbove &&
|
!userStore.isTeacherOrAbove &&
|
||||||
|
// 演示模式下协作通道是断开的(见 App.vue),按钮点了也没人收
|
||||||
|
!userStore.demoMode &&
|
||||||
codeStore.code.language !== "Flowchart" &&
|
codeStore.code.language !== "Flowchart" &&
|
||||||
!isContestMode.value,
|
!isContestMode.value,
|
||||||
)
|
)
|
||||||
@@ -74,14 +76,6 @@ const toggleHelp = () => {
|
|||||||
collabStore.requestHelp(problem.value!._id)
|
collabStore.requestHelp(problem.value!._id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 服务端的一次性提示(没有老师在线、老师取消了求助)
|
|
||||||
watch(
|
|
||||||
() => collabStore.notice,
|
|
||||||
(text) => {
|
|
||||||
if (text) message.info(collabStore.consumeNotice())
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const showGoSubmissionButton = computed(() => {
|
const showGoSubmissionButton = computed(() => {
|
||||||
if (isContestMode.value) return true
|
if (isContestMode.value) return true
|
||||||
else if (userStore.isAdminRole) return true
|
else if (userStore.isAdminRole) return true
|
||||||
|
|||||||
@@ -68,8 +68,17 @@ interface EditorReadyPayload {
|
|||||||
// 项目里旧的 sync.ts 用的是裸变量,同一个道理,这里换成 shallowRef 规避。
|
// 项目里旧的 sync.ts 用的是裸变量,同一个道理,这里换成 shallowRef 规避。
|
||||||
const editorView = shallowRef<EditorView | null>(null)
|
const editorView = shallowRef<EditorView | null>(null)
|
||||||
|
|
||||||
|
const bind = (view: EditorView) => {
|
||||||
|
if (collabStore.isTeacher || !collabStore.room) return
|
||||||
|
// 学生端:当前编辑器内容就是内容源
|
||||||
|
start({ editorView: view, seedContent: view.state.doc.toString() })
|
||||||
|
}
|
||||||
|
|
||||||
const handleEditorReady = (payload: EditorReadyPayload) => {
|
const handleEditorReady = (payload: EditorReadyPayload) => {
|
||||||
editorView.value = payload.view
|
editorView.value = payload.view
|
||||||
|
// 也从这里起:学生排队时切去看提交记录、老师在这期间接了单,
|
||||||
|
// 等他切回来时 room 早就非空了,只靠下面的 watch 是等不到的
|
||||||
|
bind(payload.view)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 房间开了才建文档。学生点求助时什么都不做 —— 老师没来之前不该动他的编辑器。
|
// 房间开了才建文档。学生点求助时什么都不做 —— 老师没来之前不该动他的编辑器。
|
||||||
@@ -82,19 +91,19 @@ const handleEditorReady = (payload: EditorReadyPayload) => {
|
|||||||
watch(
|
watch(
|
||||||
() => collabStore.room,
|
() => collabStore.room,
|
||||||
(room) => {
|
(room) => {
|
||||||
if (room && !collabStore.isTeacher && editorView.value) {
|
if (room && !collabStore.isTeacher && editorView.value) bind(editorView.value)
|
||||||
// 学生端:当前编辑器内容就是内容源
|
else stop()
|
||||||
start({
|
|
||||||
editorView: editorView.value,
|
|
||||||
seedContent: editorView.value.state.doc.toString(),
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
stop()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
onUnmounted(stop)
|
onUnmounted(() => {
|
||||||
|
stop()
|
||||||
|
// 这个组件卸载意味着编辑器没了(切走页面、或者把语言切成流程图),
|
||||||
|
// CRDT 会话没法接着用:回来时只能新建 Y.Doc,再拿编辑器内容当种子就会和
|
||||||
|
// 老师那份合并成重复文本。所以不是"悄悄把绑定拆了",而是明确结束协作 ——
|
||||||
|
// 否则老师那边模态框照开、字照敲,一个也到不了学生那里
|
||||||
|
if (collabStore.room && !collabStore.isTeacher) collabStore.leave()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -32,8 +32,21 @@ export function useCollabDoc() {
|
|||||||
let view: EditorView | null = null
|
let view: EditorView | null = null
|
||||||
let detachDocUpdate: (() => void) | null = null
|
let detachDocUpdate: (() => void) | null = null
|
||||||
let detachAwarenessUpdate: (() => void) | null = null
|
let detachAwarenessUpdate: (() => void) | null = null
|
||||||
|
/**
|
||||||
|
* 会话代号。每次 start / stop 都推进一格。
|
||||||
|
*
|
||||||
|
* start 要 await 六个动态 import,房间可能在这期间就关了(机房首次加载
|
||||||
|
* y* 那几个 chunk 正是最慢的时候)。stop() 先跑完的话它面对的是 doc === null,
|
||||||
|
* 什么也拆不到;等 import 回来 start 的后半段照样建文档、装 binaryHandler、
|
||||||
|
* 往编辑器里挂 yCollab、还发一轮 SyncStep1 —— 给一个已经不存在的房间。
|
||||||
|
*/
|
||||||
|
let generation = 0
|
||||||
|
|
||||||
async function start({ editorView, seedContent }: StartOptions) {
|
async function start({ editorView, seedContent }: StartOptions) {
|
||||||
|
// 已经有一份在跑就先收掉。重复 start 只可能来自时序竞争,叠加没有意义
|
||||||
|
if (doc) stop()
|
||||||
|
const myGeneration = ++generation
|
||||||
|
|
||||||
const [Y, awarenessProtocol, syncProtocol, encoding, decoding, { yCollab }] =
|
const [Y, awarenessProtocol, syncProtocol, encoding, decoding, { yCollab }] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
import("yjs"),
|
import("yjs"),
|
||||||
@@ -44,6 +57,9 @@ export function useCollabDoc() {
|
|||||||
import("y-codemirror.next"),
|
import("y-codemirror.next"),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
// 等 chunk 的这段时间里房间关了(或者又开了新的一轮),整个放弃
|
||||||
|
if (myGeneration !== generation) return
|
||||||
|
|
||||||
view = editorView
|
view = editorView
|
||||||
doc = new Y.Doc()
|
doc = new Y.Doc()
|
||||||
const ytext = doc.getText("codemirror")
|
const ytext = doc.getText("codemirror")
|
||||||
@@ -131,6 +147,7 @@ export function useCollabDoc() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function stop() {
|
function stop() {
|
||||||
|
generation += 1
|
||||||
collabStore.setBinaryHandler(null)
|
collabStore.setBinaryHandler(null)
|
||||||
detachDocUpdate?.()
|
detachDocUpdate?.()
|
||||||
detachAwarenessUpdate?.()
|
detachAwarenessUpdate?.()
|
||||||
|
|||||||
Reference in New Issue
Block a user