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
|
|||
|
|
}
|
|||
|
|
}
|