Some checks failed
Deploy / deploy (push) Has been cancelled
点求助列表里的一条 = 跳到 /problem/:id,在页面自带的那个编辑器里和学生同步。
原来是弹一个 CollabModal(弹框里再挂一个 CodeMirror + 一份只读题面):
- 按一下 Esc 弹框就关、协作跟着结束(`show` 的 setter 里 leave 是唯一的关闭语义,
Esc 和点遮罩都走它),上课太容易误触;
- 弹框里那份题面不能跑测试、不能提交,老师还是得另开标签页对照。
现在结束协作只有两条明确的路:工具栏的「结束协作」按钮、离开这一页。教师端也少了
一个 CodeMirror 实例 —— setBinaryHandler 那个单例槽位只剩 SyncCodeEditor 一个使用者。
改造中撞出两个真问题,根因是同一个:`/problem/1002` → `/problem/1001` 是同一条路由
换 params,**组件被复用、不卸载**。
- 老师在别的题上接单跳过来,`@ready` 不会再触发,而 room 早在导航前就 open 了 ——
老师停在一个空编辑器上干等。watch 的源从 room 一个变成 [room, roomIsHere] 两个。
- 反过来老师协作中切去别的题,onUnmounted 不触发 → 不发 leave,学生一直挂着
「老师正在帮你」而老师的字一个也过不去。加 `bound` 标记区分「房间从我这儿挪走了」
(补发 leave)和「房间在别处开起来了」(跟我无关,接单那一瞬间就是这种)。
`leave` 分两种语义(协议加 reason):
| 前端 | 求助记录 | 对面看到 |
|---|---|---|
| leave("done") 点「结束协作」 | 删除 | 老师已结束这次帮忙 |
| leave("left") 离开这道题的页面 | 教师走→退回排队;学生走→删除 | 老师暂时离开,已重新排队 |
"left" 复用的是老师掉线那条路(requeueAfterTeacherGone),两件事语义相同。原来两者
都按 done 处理 —— 老师点一下「提交信息」,学生就得重新举手,而他看到的是
「协作已结束」,会以为被处理完了。发起方收到的 reason 单独一份(self_left/done),
不然学生自己切走了却看到「老师已结束这次帮忙」。
其余为这套交互补的闸:
- 学生排队期间离开那道题 → 自动取消求助。那条求助说的是「我卡在这道题」,人走了
就不成立;原来它会一直挂在队列里,老师接进来时学生的编辑器不在这道题上、根本
不会绑,老师对着空编辑器敲字,两边都没提示。
- 协作中**教师的语言选择跟着 room.language 走**(写 codeStore,不只是改高亮),
结束后还原。只改高亮的话老师会拿自己那档语言提交学生的代码(学生写 C、老师选的
是 Python,当场 CE),工具栏还可能显示「提交流程图」。学生端一个字不动 ——
服务端的 room_language 只发给教师,学生本地那份停在建房那一刻。
- 协作中不给「重置代码」:v-model 一写回就顺着 Yjs 同步过去,等于一键清空学生的作业。
- 协作中「提交信息」走新标签 —— 那是教师工具栏上唯一会跳路由的按钮。
- 接单时 resetScreenMode():分屏在「题目」「自测」两档时右侧编辑器根本没挂出来,
停在同一道题上接单会落进一个没有编辑器的页面(跳到别的题时 detail.vue 的 init()
会重置,同页不会)。
- 老师已在一个房间里时前端也拦住接下一单(服务端本来就拦),不拦的话前端已经跳到
新题目上,等于把手上那场协作断掉。
- 文案:协议层那四条英文(Invalid problemId 之类)会被原样弹成 toast,改成中文;
「请先退出当前协作」统一成「结束」;room_closed 的提示按 reason × 角色展开。
验证:本机起 api + web,三个账号两两组合实跑 —— 跨题接单 / 同页接单、双向编辑、
连按 Esc、结束协作、老师跳走后再接、学生跳走、学生排队中切题、学生协作中切语言、
分屏在「题目」档接单、协作中重复接单,以及上面每一句提示的实际文案。
api typecheck / check:routes、前端 type-check / build、fmt 全过。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
85 lines
3.1 KiB
Vue
85 lines
3.1 KiB
Vue
<script setup lang="ts">
|
||
import type { CollabRequestItem } from "shared/composables/websocket"
|
||
import { useCollabStore } from "shared/store/collab"
|
||
import HelpRequestList from "./HelpRequestList.vue"
|
||
|
||
/**
|
||
* 课堂求助的全局界面:一次性提示、新求助 toast、求助列表。
|
||
*
|
||
* 教师端的协作**没有弹框**:接单会跳到那道题的页面,在页面自带的编辑器里协作
|
||
* (见 HelpRequestList 的 handleAccept、ProblemEditor 的 collabHere)。
|
||
* 原来这里还异步挂一个 CollabModal,那个弹框按一下 Esc 就关、协作跟着结束。
|
||
*
|
||
* 挂在 App.vue 而不是顶栏或 default.vue 布局里。这些东西跟着**连接**走,
|
||
* 而连接是全局常驻的(App.vue 按登录态开关)—— 挂在顶栏里的时候,老师一进
|
||
* /admin 就换成了 admin.vue 布局,顶栏连同这几个消费者一起卸载:求助照收,
|
||
* 提示、角标、协作界面全都不出现,正好错过 collab.ts 里写的那句「老师可能
|
||
* 正在后台改题时收到求助」。放在这里才真的全局。
|
||
*
|
||
* 位置要求:n-message-provider 的后代(useMessage 需要)。
|
||
*/
|
||
const collabStore = useCollabStore()
|
||
const message = useMessage()
|
||
|
||
/**
|
||
* 一次性提示统一在这里消费。
|
||
*
|
||
* 学生排着队切去看提交记录,老师这时候取消了他的求助,那条「老师已取消你的
|
||
* 求助」挂在题目页上就永远没人消费 —— 教师端的 error 提示(比如「请先退出
|
||
* 当前协作」)同理。
|
||
*/
|
||
watch(
|
||
() => collabStore.noticeSeq,
|
||
() => {
|
||
const text = collabStore.consumeNotice()
|
||
if (text) message.info(text)
|
||
},
|
||
)
|
||
|
||
/**
|
||
* 新求助进来只有角标默默 +1,上课走动的时候根本注意不到,补一条 toast。
|
||
*
|
||
* 只在数字**变大**时弹:老师自己接单、拒绝、别的老师接走都会让它变小,那些
|
||
* 不该打扰人。断线重连后服务端会重推一份全量列表,队里还有人的话这里会再弹
|
||
* 一次 —— 那正好是「你刚断过线,这些人还等着」,留着。
|
||
*/
|
||
watch(
|
||
() => collabStore.pendingCount,
|
||
(count, previous) => {
|
||
if (count <= previous) return
|
||
let latest: CollabRequestItem | null = null
|
||
for (const item of collabStore.requests) {
|
||
if (item.status !== "pending") continue
|
||
if (!latest || item.createdAt > latest.createdAt) latest = item
|
||
}
|
||
const text = latest
|
||
? `${latest.studentName} 求助:${latest.problemTitle}`
|
||
: "有新的求助"
|
||
// 内容传 render 函数(naive 的 content 支持),这样整条 toast 可点:
|
||
// 点一下直接开求助列表,省得再去点名字、再点菜单。
|
||
const notice = message.info(
|
||
() =>
|
||
h(
|
||
"span",
|
||
{
|
||
style: { cursor: "pointer" },
|
||
onClick: () => {
|
||
collabStore.helpPanelOpen = true
|
||
notice.destroy()
|
||
},
|
||
},
|
||
`${text} · 点击处理`,
|
||
),
|
||
{ duration: 5000 },
|
||
)
|
||
},
|
||
)
|
||
</script>
|
||
|
||
<template>
|
||
<HelpRequestList
|
||
v-if="collabStore.isTeacher"
|
||
v-model:show="collabStore.helpPanelOpen"
|
||
/>
|
||
</template>
|