285 lines
7.5 KiB
TypeScript
285 lines
7.5 KiB
TypeScript
/**
|
||
* 工作流协作 Composable
|
||
* 支持多人实时协作编辑工作流
|
||
*/
|
||
import { ref, onUnmounted } from 'vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import { useUserStore } from '@/stores/user'
|
||
import api from '@/api'
|
||
|
||
export interface CollaborationUser {
|
||
user_id: string
|
||
username: string
|
||
joined_at: string
|
||
color: string
|
||
}
|
||
|
||
export interface CollaborationOperation {
|
||
type: string
|
||
user_id: string
|
||
username: string
|
||
data?: any
|
||
timestamp?: string
|
||
}
|
||
|
||
export interface CollaborationMessage {
|
||
type: string
|
||
workflow_id?: string
|
||
current_user?: CollaborationUser
|
||
online_users?: CollaborationUser[]
|
||
user?: CollaborationUser
|
||
user_id?: string
|
||
operation?: CollaborationOperation
|
||
timestamp?: string
|
||
}
|
||
|
||
export function useCollaboration(workflowId: string) {
|
||
const connected = ref<boolean>(false)
|
||
const onlineUsers = ref<CollaborationUser[]>([])
|
||
const currentUser = ref<CollaborationUser | null>(null)
|
||
const ws = ref<WebSocket | null>(null)
|
||
let heartbeatInterval: number | null = null
|
||
let reconnectTimeout: number | null = null
|
||
const reconnectAttempts = ref<number>(0)
|
||
const maxReconnectAttempts = 5
|
||
|
||
// 操作监听器
|
||
type OperationHandler = (operation: CollaborationOperation) => void
|
||
const operationHandlers = new Set<OperationHandler>()
|
||
|
||
// 获取WebSocket URL
|
||
const getWebSocketUrl = () => {
|
||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||
const hostname = window.location.hostname
|
||
const port = hostname === 'localhost' || hostname === '127.0.0.1' ? '8037' : '8037'
|
||
const userStore = useUserStore()
|
||
const token = userStore.token
|
||
|
||
if (!token) {
|
||
throw new Error('未登录,无法建立协作连接')
|
||
}
|
||
|
||
return `${protocol}//${hostname}:${port}/api/v1/collaboration/ws/workflows/${workflowId}?token=${token}`
|
||
}
|
||
|
||
// 连接WebSocket
|
||
const connect = () => {
|
||
if (ws.value && ws.value.readyState === WebSocket.OPEN) {
|
||
return // 已经连接
|
||
}
|
||
|
||
const wsUrl = getWebSocketUrl()
|
||
console.log('[协作] 连接中:', wsUrl)
|
||
|
||
try {
|
||
ws.value = new WebSocket(wsUrl)
|
||
|
||
ws.value.onopen = () => {
|
||
console.log('[协作] 连接已建立')
|
||
connected.value = true
|
||
reconnectAttempts.value = 0
|
||
|
||
// 启动心跳
|
||
startHeartbeat()
|
||
}
|
||
|
||
ws.value.onmessage = (event) => {
|
||
try {
|
||
const message: CollaborationMessage = JSON.parse(event.data)
|
||
handleMessage(message)
|
||
} catch (e) {
|
||
console.error('[协作] 解析消息失败:', e)
|
||
}
|
||
}
|
||
|
||
ws.value.onerror = (err) => {
|
||
console.error('[协作] 错误:', err)
|
||
connected.value = false
|
||
ElMessage.error('协作连接错误')
|
||
}
|
||
|
||
ws.value.onclose = (event) => {
|
||
console.log('[协作] 连接已关闭', event.code, event.reason)
|
||
connected.value = false
|
||
stopHeartbeat()
|
||
|
||
// 如果不是正常关闭,尝试重连
|
||
if (event.code !== 1000 && reconnectAttempts.value < maxReconnectAttempts) {
|
||
reconnectAttempts.value++
|
||
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts.value), 30000)
|
||
console.log(`[协作] ${delay}ms后尝试重连 (${reconnectAttempts.value}/${maxReconnectAttempts})`)
|
||
|
||
reconnectTimeout = window.setTimeout(() => {
|
||
connect()
|
||
}, delay)
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.error('[协作] 连接失败:', err)
|
||
ElMessage.error('协作连接失败')
|
||
}
|
||
}
|
||
|
||
// 处理消息
|
||
const handleMessage = (message: CollaborationMessage) => {
|
||
console.log('[协作] 收到消息:', message)
|
||
|
||
switch (message.type) {
|
||
case 'collaboration_init':
|
||
// 初始化消息
|
||
if (message.current_user) {
|
||
currentUser.value = message.current_user
|
||
}
|
||
if (message.online_users) {
|
||
onlineUsers.value = message.online_users
|
||
}
|
||
break
|
||
|
||
case 'user_joined':
|
||
// 用户加入
|
||
if (message.user) {
|
||
onlineUsers.value.push(message.user)
|
||
ElMessage.info(`${message.user.username} 加入了协作编辑`)
|
||
}
|
||
break
|
||
|
||
case 'user_left':
|
||
// 用户离开
|
||
if (message.user_id) {
|
||
onlineUsers.value = onlineUsers.value.filter(u => u.user_id !== message.user_id)
|
||
const user = onlineUsers.value.find(u => u.user_id === message.user_id)
|
||
if (user) {
|
||
ElMessage.info(`${user.username} 离开了协作编辑`)
|
||
}
|
||
}
|
||
break
|
||
|
||
case 'operation':
|
||
// 工作流操作
|
||
if (message.operation) {
|
||
// 触发所有注册的操作处理器
|
||
operationHandlers.forEach(handler => {
|
||
try {
|
||
handler(message.operation!)
|
||
} catch (e) {
|
||
console.error('[协作] 操作处理器执行失败:', e)
|
||
}
|
||
})
|
||
console.log('[协作] 收到操作:', message.operation)
|
||
}
|
||
break
|
||
|
||
case 'pong':
|
||
// 心跳响应
|
||
break
|
||
|
||
case 'error':
|
||
ElMessage.error(message.message || '协作错误')
|
||
break
|
||
}
|
||
}
|
||
|
||
// 发送操作
|
||
const sendOperation = (operation: Omit<CollaborationOperation, 'user_id' | 'username' | 'timestamp'>) => {
|
||
if (!ws.value || ws.value.readyState !== WebSocket.OPEN) {
|
||
console.warn('[协作] WebSocket未连接,无法发送操作')
|
||
return
|
||
}
|
||
|
||
const fullOperation: CollaborationOperation = {
|
||
...operation,
|
||
user_id: currentUser.value?.user_id || '',
|
||
username: currentUser.value?.username || '',
|
||
timestamp: new Date().toISOString()
|
||
}
|
||
|
||
const message = {
|
||
type: 'operation',
|
||
operation: fullOperation
|
||
}
|
||
|
||
try {
|
||
ws.value.send(JSON.stringify(message))
|
||
} catch (e) {
|
||
console.error('[协作] 发送操作失败:', e)
|
||
}
|
||
}
|
||
|
||
// 启动心跳
|
||
const startHeartbeat = () => {
|
||
heartbeatInterval = window.setInterval(() => {
|
||
if (ws.value && ws.value.readyState === WebSocket.OPEN) {
|
||
ws.value.send(JSON.stringify({ type: 'ping' }))
|
||
}
|
||
}, 30000) // 每30秒发送一次心跳
|
||
}
|
||
|
||
// 停止心跳
|
||
const stopHeartbeat = () => {
|
||
if (heartbeatInterval) {
|
||
clearInterval(heartbeatInterval)
|
||
heartbeatInterval = null
|
||
}
|
||
if (reconnectTimeout) {
|
||
clearTimeout(reconnectTimeout)
|
||
reconnectTimeout = null
|
||
}
|
||
}
|
||
|
||
// 断开连接
|
||
const disconnect = () => {
|
||
stopHeartbeat()
|
||
if (ws.value) {
|
||
ws.value.close(1000, '正常关闭')
|
||
ws.value = null
|
||
}
|
||
connected.value = false
|
||
onlineUsers.value = []
|
||
currentUser.value = null
|
||
}
|
||
|
||
// 获取在线用户列表
|
||
const fetchOnlineUsers = async () => {
|
||
try {
|
||
const response = await api.get(`/api/v1/collaboration/workflows/${workflowId}/users`)
|
||
if (response.data.online_users) {
|
||
onlineUsers.value = response.data.online_users
|
||
}
|
||
} catch (e) {
|
||
console.error('[协作] 获取在线用户失败:', e)
|
||
}
|
||
}
|
||
|
||
// 注册操作监听器
|
||
const onOperation = (handler: OperationHandler) => {
|
||
operationHandlers.add(handler)
|
||
// 返回取消注册函数
|
||
return () => {
|
||
operationHandlers.delete(handler)
|
||
}
|
||
}
|
||
|
||
// 取消注册操作监听器
|
||
const offOperation = (handler: OperationHandler) => {
|
||
operationHandlers.delete(handler)
|
||
}
|
||
|
||
// 清理
|
||
onUnmounted(() => {
|
||
disconnect()
|
||
operationHandlers.clear()
|
||
})
|
||
|
||
return {
|
||
connected,
|
||
onlineUsers,
|
||
currentUser,
|
||
connect,
|
||
disconnect,
|
||
sendOperation,
|
||
fetchOnlineUsers,
|
||
onOperation,
|
||
offOperation
|
||
}
|
||
}
|