feat(web): 协作编辑走 collab 通道

Yjs sync/awareness 协议直接跑在 /ws/collab 上,服务端哑转发。
内容源是学生:学生端先把编辑器内容写进 ytext 再挂 yCollab,
教师端 seedContent 恒为 null —— 这条根治了老实现里谁的代码活下来看运气的问题。

订正 brief 里的一处疏漏:SyncCodeEditor.vue 挂在每个用户的题目页上,教师也不例外
(ProblemEditor.vue 只按语言是不是 Flowchart 分支,不看角色)。教师接单同样会让
collabStore.room 非空,若不按角色收窄,教师自己停在某道题页面上接单时,这个组件
会把教师自己的编辑器内容当成种子插入文档,还会跟 CollabModal 抢
setBinaryHandler 这个单例槽位。改为 `room && !collabStore.isTeacher` 才起协作,
教师端的协作只归 CollabModal 管。

另外两处修正:
- editorView 用 shallowRef 而非 ref —— CodeMirror 的 EditorView 是带 getter 的类
  实例,ref() 的深度 UnwrapRef 会把它拆成丢了原型方法的假类型,vue-tsc 报错。
- Header.vue 挂 CollabModal 放进根 n-flex 内部而不是同级 —— 组件一旦变成多根
  fragment,default.vue 里 `<Header class="header" />` 的 class 就没有单一根节点
  可以落地,header 行会丢掉居中样式(实测触发了 Vue 的
  Extraneous non-props attributes 警告)。n-modal 默认 teleport 到 body,塞在
  这里不影响其实际渲染位置。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K1d8B3f4SXJwDvUY625eQd
This commit is contained in:
2026-08-28 07:43:08 -06:00
co-authored by Claude Opus 5
parent bfe4468fdb
commit 86857dedf0
6 changed files with 309 additions and 2 deletions
@@ -6,6 +6,7 @@ import { useLearnProgress } from "shared/composables/learnProgress"
import { useAuthModalStore } from "shared/store/authModal"
import { useScreenModeStore } from "shared/store/screenMode"
import { logout } from "../api"
import CollabModal from "./CollabModal.vue"
import HelpRequestList from "./HelpRequestList.vue"
import { useConfigStore } from "../store/config"
import { useUserStore } from "../store/user"
@@ -363,6 +364,14 @@ function handleMenuSelect(key: string) {
</template>
</n-button>
</n-flex>
<!--
挂在根 n-flex 内部而不是同级:Header.vue 一旦变成多根 fragment
default.vue 里 `<Header class="header" />` 那个 class 就没有任何单一
根节点可以落地(Vue 会报 "Extraneous non-props attributes" 警告并把它
整个丢弃),header 行随之丢掉 `max-width: 2000px` 那条居中样式。
n-modal 默认 teleport 到 body,塞在这里不影响它的实际渲染位置。
-->
<CollabModal v-if="userStore.isTeacherOrAbove" />
</n-flex>
</template>