feat(web): 题目页的「开启同步」换成「求助」

学生发起求助,状态与排队位置显示在按钮旁。
教师端入口不在题目页,所以对教师隐藏这个按钮。

顺带修正 handleHelpCancel:去掉 request.socket !== ws 的校验。
学生取消自己的求助,不管从他哪个标签页发起都合法——
getRequest(ws.data.userId) 已经把范围锁在这一个用户上了,
不是跨用户操作,不需要再比对是不是同一条连接。这层校验此前
会让「第二个标签页点取消」被服务端静默丢弃,前端却已经乐观
地把按钮变回「求助」,是一处 UI 说谎;现在两边状态一致。
handleCollabClose 里排队分支的 socket 归属校验不受影响,
那里的关闭事件是顺带触发的,仍然需要认出是不是本人这条连接。

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 06:33:25 -06:00
parent ab8fcc2d42
commit 6f873be367
4 changed files with 46 additions and 165 deletions

View File

@@ -3,7 +3,6 @@ import { cpp } from "@codemirror/lang-cpp"
import { python } from "@codemirror/lang-python"
import { sql, SQLite } from "@codemirror/lang-sql"
import { bracketMatching } from "@codemirror/language"
import { EditorView } from "@codemirror/view"
import { Codemirror } from "vue-codemirror"
import {
autocompletion,
@@ -15,20 +14,11 @@ import type { LANGUAGE } from "utils/types"
import { oneDark } from "../themes/oneDark"
import { smoothy } from "../themes/smoothy"
import { styleTheme } from "shared/extensions/baseTheme"
import { useCodeSync, SYNC_ERROR_CODES } from "../composables/sync"
import { useBreakpoints } from "../composables/breakpoints"
import { enhanceCompletion } from "shared/extensions/autocompletion"
const isDark = useDark()
interface EditorReadyPayload {
view: EditorView
state: any
container: HTMLElement
}
interface Props {
sync: boolean
problem: string
language?: LANGUAGE
fontSize?: number
@@ -38,8 +28,6 @@ interface Props {
}
const {
sync,
problem,
language = "Python3",
fontSize = 20,
height = "100%",
@@ -48,15 +36,6 @@ const {
} = defineProps<Props>()
const code = defineModel<string>("value")
const emit = defineEmits<{
syncClosed: []
syncStatusChange: [
status: { otherUser?: { name: string; isSuperAdmin: boolean } },
]
}>()
const { isDesktop } = useBreakpoints()
const langExtension = computed((): Extension => {
if (language === "SQL")
return sql({ dialect: SQLite, upperCaseKeywords: true })
@@ -72,71 +51,8 @@ const extensions = computed(() => [
autocompletion({
override: [enhanceCompletion(language), completeAnyWord],
}),
getInitialExtension(),
])
const { startSync, stopSync, getInitialExtension } = useCodeSync()
const editorView = ref<EditorView | null>(null)
let cleanupSync: (() => void) | null = null
const cleanupSyncResources = () => {
if (cleanupSync) {
cleanupSync()
cleanupSync = null
}
stopSync()
}
const initSync = async () => {
if (!editorView.value || !problem || !isDesktop.value) return
cleanupSyncResources()
cleanupSync = await startSync({
problemId: problem,
editorView: editorView.value as EditorView,
onStatusChange: (status) => {
// 处理需要断开同步的情况
if (
(status.errorCode === SYNC_ERROR_CODES.SUPER_ADMIN_LEFT ||
status.errorCode === SYNC_ERROR_CODES.MISSING_SUPER_ADMIN) &&
!status.connected
) {
emit("syncClosed")
}
emit("syncStatusChange", { otherUser: status.otherUser })
},
})
}
const handleEditorReady = (payload: EditorReadyPayload) => {
editorView.value = payload.view as EditorView
if (sync) {
initSync()
}
}
watch(
() => sync,
(shouldSync) => {
if (shouldSync) {
initSync()
} else {
cleanupSyncResources()
}
},
)
watch(
() => problem,
(newProblem, oldProblem) => {
if (newProblem !== oldProblem && sync) {
initSync()
}
},
)
onUnmounted(cleanupSyncResources)
</script>
<template>
@@ -148,6 +64,5 @@ onUnmounted(cleanupSyncResources)
:tab-size="4"
:placeholder="placeholder"
:style="{ height, fontSize: `${fontSize}px` }"
@ready="handleEditorReady"
/>
</template>