协同编辑代码
This commit is contained in:
397
src/shared/composables/sync.ts
Normal file
397
src/shared/composables/sync.ts
Normal file
@@ -0,0 +1,397 @@
|
||||
import { useUserStore } from "../store/user"
|
||||
import type { EditorView } from "@codemirror/view"
|
||||
import { Compartment } from "@codemirror/state"
|
||||
import type { WebrtcProvider } from "y-webrtc"
|
||||
import type { Doc, Text } from "yjs"
|
||||
|
||||
// 常量定义
|
||||
const SYNC_CONSTANTS = {
|
||||
MAX_ROOM_USERS: 2,
|
||||
AWARENESS_SYNC_DELAY: 500,
|
||||
INIT_SYNC_TIMEOUT: 500,
|
||||
SUPER_ADMIN_COLOR: "#ff6b6b",
|
||||
REGULAR_USER_COLOR: "#4dabf7",
|
||||
} as const
|
||||
|
||||
// 类型定义
|
||||
type SyncState = "waiting" | "active" | "error"
|
||||
|
||||
interface UserInfo {
|
||||
name: string
|
||||
isSuperAdmin: boolean
|
||||
}
|
||||
|
||||
interface PeersEvent {
|
||||
added: string[]
|
||||
removed: string[]
|
||||
webrtcPeers: string[]
|
||||
}
|
||||
|
||||
interface StatusEvent {
|
||||
connected: boolean
|
||||
}
|
||||
|
||||
interface SyncedEvent {
|
||||
synced: boolean
|
||||
}
|
||||
|
||||
interface SyncOptions {
|
||||
problemId: string
|
||||
editorView: EditorView
|
||||
onStatusChange?: (status: SyncStatus) => void
|
||||
}
|
||||
|
||||
export interface SyncStatus {
|
||||
connected: boolean
|
||||
roomUsers: number
|
||||
canSync: boolean
|
||||
message: string
|
||||
error?: string
|
||||
otherUser?: UserInfo
|
||||
}
|
||||
|
||||
export function useCodeSync() {
|
||||
const userStore = useUserStore()
|
||||
const message = useMessage()
|
||||
|
||||
// 状态变量
|
||||
let ydoc: Doc | null = null
|
||||
let provider: WebrtcProvider | null = null
|
||||
let ytext: Text | null = null
|
||||
const collabCompartment = new Compartment()
|
||||
let currentEditorView: EditorView | null = null
|
||||
let lastSyncState: SyncState | null = null
|
||||
let roomUserInfo = new Map<number, UserInfo>()
|
||||
let hasShownSuperAdminLeftMessage = false
|
||||
|
||||
const updateStatus = (
|
||||
status: SyncStatus,
|
||||
onStatusChange?: (status: SyncStatus) => void,
|
||||
) => {
|
||||
onStatusChange?.(status)
|
||||
}
|
||||
|
||||
const normalizeClientId = (clientId: number | string): number => {
|
||||
return typeof clientId === "string" ? parseInt(clientId, 10) : clientId
|
||||
}
|
||||
|
||||
const checkHasSuperAdmin = (awarenessStates: Map<number, any>): boolean => {
|
||||
if (userStore.isSuperAdmin) return true
|
||||
return Array.from(awarenessStates.values()).some(
|
||||
(state) => state.user?.isSuperAdmin,
|
||||
)
|
||||
}
|
||||
|
||||
const getOtherUserInfo = (
|
||||
awarenessStates: Map<number, any>,
|
||||
): UserInfo | undefined => {
|
||||
if (!provider) return undefined
|
||||
|
||||
const localClientId = provider.awareness.clientID
|
||||
for (const [clientId, state] of awarenessStates) {
|
||||
if (clientId !== localClientId && state.user) {
|
||||
return {
|
||||
name: state.user.name,
|
||||
isSuperAdmin: state.user.isSuperAdmin,
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
const checkIfSuperAdminLeft = (
|
||||
removedClientIds: number[],
|
||||
onStatusChange?: (status: SyncStatus) => void,
|
||||
) => {
|
||||
if (userStore.isSuperAdmin || hasShownSuperAdminLeftMessage) return
|
||||
|
||||
const superAdminInfo = removedClientIds
|
||||
.map((id) => roomUserInfo.get(id))
|
||||
.find((info) => info?.isSuperAdmin)
|
||||
|
||||
if (superAdminInfo) {
|
||||
hasShownSuperAdminLeftMessage = true
|
||||
updateStatus(
|
||||
{
|
||||
connected: false,
|
||||
roomUsers: 0,
|
||||
canSync: false,
|
||||
message: `超管 ${superAdminInfo.name} 已退出`,
|
||||
error: "超管已离开",
|
||||
},
|
||||
onStatusChange,
|
||||
)
|
||||
message.warning(`超管 ${superAdminInfo.name} 已退出`)
|
||||
stopSync()
|
||||
}
|
||||
}
|
||||
|
||||
const checkRoomPermissions = (
|
||||
roomUsers: number,
|
||||
onStatusChange?: (status: SyncStatus) => void,
|
||||
) => {
|
||||
const awarenessStates = provider?.awareness.getStates()
|
||||
if (!awarenessStates) return
|
||||
|
||||
const hasSuperAdmin = checkHasSuperAdmin(awarenessStates)
|
||||
const canSync = roomUsers === SYNC_CONSTANTS.MAX_ROOM_USERS && hasSuperAdmin
|
||||
const otherUser = getOtherUserInfo(awarenessStates)
|
||||
|
||||
if (roomUsers === SYNC_CONSTANTS.MAX_ROOM_USERS && !hasSuperAdmin) {
|
||||
updateStatus(
|
||||
{
|
||||
connected: true,
|
||||
roomUsers,
|
||||
canSync: false,
|
||||
message: "房间内必须有一个超级管理员",
|
||||
error: "缺少超级管理员",
|
||||
otherUser,
|
||||
},
|
||||
onStatusChange,
|
||||
)
|
||||
if (lastSyncState !== "error") {
|
||||
message.warning("协同编辑需要至少一个超级管理员")
|
||||
lastSyncState = "error"
|
||||
}
|
||||
} else if (canSync) {
|
||||
updateStatus(
|
||||
{
|
||||
connected: true,
|
||||
roomUsers,
|
||||
canSync: true,
|
||||
message: "协同编辑已激活,可以开始协作!",
|
||||
otherUser,
|
||||
},
|
||||
onStatusChange,
|
||||
)
|
||||
if (lastSyncState !== "active") {
|
||||
message.success("协同编辑已激活,可以开始协作!")
|
||||
lastSyncState = "active"
|
||||
}
|
||||
} else {
|
||||
updateStatus(
|
||||
{
|
||||
connected: true,
|
||||
roomUsers,
|
||||
canSync: false,
|
||||
message: roomUsers === 1 ? "正在等待加入" : "等待超级管理员加入...",
|
||||
otherUser,
|
||||
},
|
||||
onStatusChange,
|
||||
)
|
||||
lastSyncState = "waiting"
|
||||
}
|
||||
}
|
||||
|
||||
const setupContentSync = (
|
||||
editorView: EditorView,
|
||||
ytext: Text,
|
||||
provider: WebrtcProvider,
|
||||
savedContent: string,
|
||||
) => {
|
||||
let hasInitialized = false
|
||||
|
||||
const initTimeout = setTimeout(() => {
|
||||
if (!hasInitialized && ytext.length === 0 && savedContent) {
|
||||
ytext.insert(0, savedContent)
|
||||
}
|
||||
hasInitialized = true
|
||||
}, SYNC_CONSTANTS.INIT_SYNC_TIMEOUT)
|
||||
|
||||
provider.on("synced", (event: SyncedEvent) => {
|
||||
if (!event.synced || hasInitialized) return
|
||||
|
||||
clearTimeout(initTimeout)
|
||||
if (ytext.length === 0 && savedContent) {
|
||||
ytext.insert(0, savedContent)
|
||||
}
|
||||
hasInitialized = true
|
||||
})
|
||||
}
|
||||
|
||||
async function startSync(options: SyncOptions): Promise<() => void> {
|
||||
const { problemId, editorView, onStatusChange } = options
|
||||
|
||||
if (!userStore.isAuthed) {
|
||||
updateStatus(
|
||||
{
|
||||
connected: false,
|
||||
roomUsers: 0,
|
||||
canSync: false,
|
||||
message: "请先登录后再使用同步功能",
|
||||
error: "用户未登录",
|
||||
},
|
||||
onStatusChange,
|
||||
)
|
||||
message.error("请先登录后再使用同步功能")
|
||||
return () => {}
|
||||
}
|
||||
|
||||
// 动态导入 yjs 相关模块
|
||||
const [Y, { WebrtcProvider }, { yCollab }] = await Promise.all([
|
||||
import("yjs"),
|
||||
import("y-webrtc"),
|
||||
import("y-codemirror.next"),
|
||||
])
|
||||
|
||||
// 初始化文档和提供者
|
||||
ydoc = new Y.Doc()
|
||||
ytext = ydoc.getText("codemirror")
|
||||
const roomName = `problem-${problemId}`
|
||||
|
||||
provider = new WebrtcProvider(roomName, ydoc, {
|
||||
signaling: [import.meta.env.PUBLIC_SIGNALING_URL],
|
||||
maxConns: 1,
|
||||
filterBcConns: true,
|
||||
})
|
||||
|
||||
// 监听连接状态
|
||||
provider.on("status", (event: StatusEvent) => {
|
||||
if (!event.connected) {
|
||||
updateStatus(
|
||||
{
|
||||
connected: false,
|
||||
roomUsers: 0,
|
||||
canSync: false,
|
||||
message: "连接已断开",
|
||||
error: "WebRTC 连接断开",
|
||||
},
|
||||
onStatusChange,
|
||||
)
|
||||
message.warning("协同编辑连接已断开")
|
||||
}
|
||||
})
|
||||
|
||||
// 监听用户加入/离开
|
||||
provider.on("peers", (event: PeersEvent) => {
|
||||
const roomUsers = event.webrtcPeers.length + 1
|
||||
|
||||
if (roomUsers > SYNC_CONSTANTS.MAX_ROOM_USERS) {
|
||||
updateStatus(
|
||||
{
|
||||
connected: false,
|
||||
roomUsers,
|
||||
canSync: false,
|
||||
message: "房间人数已满,已自动断开连接",
|
||||
error: `房间最多只能有${SYNC_CONSTANTS.MAX_ROOM_USERS}个人`,
|
||||
},
|
||||
onStatusChange,
|
||||
)
|
||||
message.warning(
|
||||
`房间人数已满(最多${SYNC_CONSTANTS.MAX_ROOM_USERS}人),已自动断开连接`,
|
||||
)
|
||||
stopSync()
|
||||
return
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
checkRoomPermissions(roomUsers, onStatusChange)
|
||||
}, SYNC_CONSTANTS.AWARENESS_SYNC_DELAY)
|
||||
})
|
||||
|
||||
// 监听 awareness 变化
|
||||
provider.awareness.on("change", (changes: any) => {
|
||||
if (!provider) return
|
||||
|
||||
const awarenessStates = provider.awareness.getStates()
|
||||
|
||||
if (changes.removed?.length > 0) {
|
||||
checkIfSuperAdminLeft(changes.removed, onStatusChange)
|
||||
}
|
||||
|
||||
awarenessStates.forEach((state, clientId) => {
|
||||
if (state.user) {
|
||||
const normalizedId = normalizeClientId(clientId)
|
||||
roomUserInfo.set(normalizedId, {
|
||||
name: state.user.name,
|
||||
isSuperAdmin: state.user.isSuperAdmin,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
checkRoomPermissions(awarenessStates.size, onStatusChange)
|
||||
})
|
||||
|
||||
// 配置编辑器扩展
|
||||
if (editorView && ytext) {
|
||||
currentEditorView = editorView
|
||||
const userColor = userStore.isSuperAdmin
|
||||
? SYNC_CONSTANTS.SUPER_ADMIN_COLOR
|
||||
: SYNC_CONSTANTS.REGULAR_USER_COLOR
|
||||
const userName = userStore.user?.username || "匿名用户"
|
||||
const savedContent = editorView.state.doc.toString()
|
||||
|
||||
// 设置用户信息
|
||||
provider.awareness.setLocalStateField("user", {
|
||||
name: userName,
|
||||
color: userColor,
|
||||
isSuperAdmin: userStore.isSuperAdmin,
|
||||
})
|
||||
|
||||
// 清空编辑器并应用协同扩展
|
||||
editorView.dispatch({
|
||||
changes: { from: 0, to: editorView.state.doc.length, insert: "" },
|
||||
})
|
||||
|
||||
const collabExt = yCollab(ytext, provider.awareness)
|
||||
editorView.dispatch({
|
||||
effects: collabCompartment.reconfigure(collabExt),
|
||||
})
|
||||
|
||||
// 设置内容同步
|
||||
setupContentSync(editorView, ytext, provider, savedContent)
|
||||
|
||||
// 设置初始状态
|
||||
updateStatus(
|
||||
{
|
||||
connected: true,
|
||||
roomUsers: 1,
|
||||
canSync: false,
|
||||
message: "正在等待加入",
|
||||
},
|
||||
onStatusChange,
|
||||
)
|
||||
|
||||
message.info(
|
||||
userStore.isSuperAdmin ? "正在等待学生加入..." : "正在等待超管加入...",
|
||||
)
|
||||
lastSyncState = "waiting"
|
||||
}
|
||||
|
||||
return () => stopSync()
|
||||
}
|
||||
|
||||
function stopSync() {
|
||||
if (currentEditorView) {
|
||||
try {
|
||||
currentEditorView.dispatch({
|
||||
effects: collabCompartment.reconfigure([]),
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn("移除协同编辑扩展失败:", error)
|
||||
}
|
||||
currentEditorView = null
|
||||
}
|
||||
|
||||
provider?.disconnect()
|
||||
provider?.destroy()
|
||||
ydoc?.destroy()
|
||||
|
||||
provider = null
|
||||
ydoc = null
|
||||
ytext = null
|
||||
lastSyncState = null
|
||||
roomUserInfo.clear()
|
||||
hasShownSuperAdminLeftMessage = false
|
||||
}
|
||||
|
||||
function getInitialExtension() {
|
||||
return collabCompartment.of([])
|
||||
}
|
||||
|
||||
return {
|
||||
startSync,
|
||||
stopSync,
|
||||
getInitialExtension,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user