You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
393 lines
13 KiB
393 lines
13 KiB
/**
|
|
* WebSocket 房间连接服务(SockJS + STOMP)
|
|
*/
|
|
import SockJS from 'sockjs-client'
|
|
import { Client } from '@stomp/stompjs'
|
|
import { getToken } from '@/utils/auth'
|
|
|
|
const WS_BASE = process.env.VUE_APP_BASE_API || '/dev-api'
|
|
|
|
/**
|
|
* 创建房间 WebSocket 连接
|
|
* @param {Object} options
|
|
* @param {string|number} options.roomId - 房间 ID
|
|
* @param {Function} options.onMembers - 收到成员列表回调 (members) => {}
|
|
* @param {Function} options.onMemberJoined - 成员加入回调 (member) => {}
|
|
* @param {Function} options.onMemberLeft - 成员离开回调 (member, sessionId) => {}
|
|
* @param {Function} options.onChatMessage - 群聊消息回调 (msg) => {}
|
|
* @param {Function} options.onPrivateChat - 私聊消息回调 (msg) => {}
|
|
* @param {Function} options.onChatHistory - 群聊历史回调 (messages) => {}
|
|
* @param {Function} options.onPrivateChatHistory - 私聊历史回调 (targetUserId, messages) => {}
|
|
* @param {Function} options.onSyncRouteVisibility - 航线显隐同步回调 (routeId, visible, senderUserId) => {}
|
|
* @param {Function} options.onSyncWaypoints - 航点变更同步回调 (routeId, senderUserId) => {}
|
|
* @param {Function} options.onSyncPlatformIcons - 平台图标变更同步回调 (senderUserId) => {}
|
|
* @param {Function} options.onSyncRoomDrawings - 空域图形变更同步回调 (senderUserId) => {}
|
|
* @param {Function} options.onSyncPlatformStyles - 探测区/威力区样式变更同步回调 (senderUserId) => {}
|
|
* @param {Function} options.onRoomState - 新加入时收到的房间状态 (visibleRouteIds: number[]) => {}
|
|
* @param {Function} options.onObjectEditLock - 对象被某成员编辑锁定 (msg: { objectType, objectId, editor }) => {}
|
|
* @param {Function} options.onObjectEditUnlock - 对象编辑解锁 (msg: { objectType, objectId, sessionId }) => {}
|
|
* @param {Function} options.onUserActivities - 房间用户当前操作快照 (activities: object[]) => {}
|
|
* @param {Function} options.onConnected - 连接成功回调
|
|
* @param {Function} options.onDisconnected - 断开回调
|
|
* @param {Function} options.onError - 错误回调
|
|
* @param {string} [options.deviceId] - 设备标识
|
|
*/
|
|
export function createRoomWebSocket(options) {
|
|
const {
|
|
roomId,
|
|
onMembers,
|
|
onMemberJoined,
|
|
onMemberLeft,
|
|
onChatMessage,
|
|
onPrivateChat,
|
|
onChatHistory,
|
|
onPrivateChatHistory,
|
|
onSyncRouteVisibility,
|
|
onSyncWaypoints,
|
|
onSyncPlatformIcons,
|
|
onSyncRoomDrawings,
|
|
onSyncPlatformStyles,
|
|
onRoomState,
|
|
onObjectEditLock,
|
|
onObjectEditUnlock,
|
|
onUserActivities,
|
|
onConnected,
|
|
onDisconnected,
|
|
onError,
|
|
deviceId = 'web-' + Math.random().toString(36).slice(2, 10)
|
|
} = options
|
|
|
|
let client = null
|
|
let roomSubscription = null
|
|
let privateSubscription = null
|
|
let heartbeatTimer = null
|
|
let reconnectAttempts = 0
|
|
const maxReconnectAttempts = 10
|
|
const reconnectDelay = 2000
|
|
|
|
function getWsUrl() {
|
|
const token = getToken()
|
|
const base = window.location.origin + WS_BASE
|
|
const sep = base.includes('?') ? '&' : '?'
|
|
return base + '/ws' + (token ? sep + 'token=' + encodeURIComponent(token) : '')
|
|
}
|
|
|
|
function sendJoin() {
|
|
if (client && client.connected) {
|
|
client.publish({
|
|
destination: '/app/room/' + roomId,
|
|
body: JSON.stringify({ type: 'JOIN', deviceId })
|
|
})
|
|
}
|
|
}
|
|
|
|
function sendLeave() {
|
|
if (client && client.connected) {
|
|
client.publish({
|
|
destination: '/app/room/' + roomId,
|
|
body: JSON.stringify({ type: 'LEAVE' })
|
|
})
|
|
}
|
|
}
|
|
|
|
function sendPing() {
|
|
if (client && client.connected) {
|
|
client.publish({
|
|
destination: '/app/room/' + roomId,
|
|
body: JSON.stringify({ type: 'PING' })
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {string|{content?: string, messageType?: string, imageUrl?: string}} contentOrPayload - 纯文本或含图片字段的对象
|
|
*/
|
|
function sendChat(contentOrPayload) {
|
|
if (!client || !client.connected) return
|
|
const payload =
|
|
typeof contentOrPayload === 'string'
|
|
? { type: 'CHAT', content: contentOrPayload }
|
|
: {
|
|
type: 'CHAT',
|
|
content: (contentOrPayload && contentOrPayload.content) || '',
|
|
...(contentOrPayload && contentOrPayload.messageType
|
|
? { messageType: contentOrPayload.messageType }
|
|
: {}),
|
|
...(contentOrPayload && contentOrPayload.imageUrl
|
|
? { imageUrl: contentOrPayload.imageUrl }
|
|
: {})
|
|
}
|
|
client.publish({
|
|
destination: '/app/room/' + roomId,
|
|
body: JSON.stringify(payload)
|
|
})
|
|
}
|
|
|
|
/**
|
|
* @param {string|{content?: string, messageType?: string, imageUrl?: string}} contentOrPayload
|
|
*/
|
|
function sendPrivateChat(targetUserId, targetUserName, contentOrPayload) {
|
|
if (!client || !client.connected) return
|
|
const isStr = typeof contentOrPayload === 'string'
|
|
const payload = {
|
|
type: 'PRIVATE_CHAT',
|
|
targetUserId,
|
|
targetUserName,
|
|
content: isStr ? contentOrPayload : (contentOrPayload && contentOrPayload.content) || '',
|
|
...(isStr
|
|
? {}
|
|
: {
|
|
...(contentOrPayload && contentOrPayload.messageType
|
|
? { messageType: contentOrPayload.messageType }
|
|
: {}),
|
|
...(contentOrPayload && contentOrPayload.imageUrl
|
|
? { imageUrl: contentOrPayload.imageUrl }
|
|
: {})
|
|
})
|
|
}
|
|
client.publish({
|
|
destination: '/app/room/' + roomId,
|
|
body: JSON.stringify(payload)
|
|
})
|
|
}
|
|
|
|
function sendPrivateChatHistoryRequest(targetUserId) {
|
|
if (client && client.connected) {
|
|
client.publish({
|
|
destination: '/app/room/' + roomId,
|
|
body: JSON.stringify({ type: 'PRIVATE_CHAT_HISTORY_REQUEST', targetUserId })
|
|
})
|
|
}
|
|
}
|
|
|
|
function sendSyncRouteVisibility(routeId, visible) {
|
|
if (client && client.connected) {
|
|
client.publish({
|
|
destination: '/app/room/' + roomId,
|
|
body: JSON.stringify({ type: 'SYNC_ROUTE_VISIBILITY', routeId, visible })
|
|
})
|
|
}
|
|
}
|
|
|
|
function sendSyncWaypoints(routeId) {
|
|
if (client && client.connected) {
|
|
client.publish({
|
|
destination: '/app/room/' + roomId,
|
|
body: JSON.stringify({ type: 'SYNC_WAYPOINTS', routeId })
|
|
})
|
|
}
|
|
}
|
|
|
|
function sendSyncPlatformIcons() {
|
|
if (client && client.connected) {
|
|
client.publish({
|
|
destination: '/app/room/' + roomId,
|
|
body: JSON.stringify({ type: 'SYNC_PLATFORM_ICONS' })
|
|
})
|
|
}
|
|
}
|
|
|
|
function sendSyncRoomDrawings() {
|
|
if (client && client.connected) {
|
|
client.publish({
|
|
destination: '/app/room/' + roomId,
|
|
body: JSON.stringify({ type: 'SYNC_ROOM_DRAWINGS' })
|
|
})
|
|
}
|
|
}
|
|
|
|
function sendSyncPlatformStyles() {
|
|
if (client && client.connected) {
|
|
client.publish({
|
|
destination: '/app/room/' + roomId,
|
|
body: JSON.stringify({ type: 'SYNC_PLATFORM_STYLES' })
|
|
})
|
|
}
|
|
}
|
|
|
|
/** 发送:当前成员锁定某对象进入编辑 */
|
|
function sendObjectEditLock(objectType, objectId, objectName) {
|
|
if (!client || !client.connected) return
|
|
const payload = { type: 'OBJECT_EDIT_LOCK', objectType, objectId }
|
|
if (objectName != null && String(objectName).trim() !== '') {
|
|
payload.objectName = String(objectName)
|
|
}
|
|
client.publish({
|
|
destination: '/app/room/' + roomId,
|
|
body: JSON.stringify(payload)
|
|
})
|
|
}
|
|
|
|
/** 发送:当前地图选中(航线等),供「当前操作」展示 */
|
|
function sendUserSelection(selection) {
|
|
if (!client || !client.connected) return
|
|
const s = selection || {}
|
|
client.publish({
|
|
destination: '/app/room/' + roomId,
|
|
body: JSON.stringify({
|
|
type: 'USER_SELECTION',
|
|
summary: s.summary != null ? String(s.summary) : '',
|
|
selectedCount: s.selectedCount != null ? Number(s.selectedCount) : 0,
|
|
items: Array.isArray(s.items) ? s.items : []
|
|
})
|
|
})
|
|
}
|
|
|
|
/** 发送:当前成员解锁某对象(结束编辑) */
|
|
function sendObjectEditUnlock(objectType, objectId) {
|
|
if (client && client.connected) {
|
|
client.publish({
|
|
destination: '/app/room/' + roomId,
|
|
body: JSON.stringify({ type: 'OBJECT_EDIT_UNLOCK', objectType, objectId })
|
|
})
|
|
}
|
|
}
|
|
|
|
function startHeartbeat() {
|
|
stopHeartbeat()
|
|
heartbeatTimer = setInterval(sendPing, 30000)
|
|
}
|
|
|
|
function stopHeartbeat() {
|
|
if (heartbeatTimer) {
|
|
clearInterval(heartbeatTimer)
|
|
heartbeatTimer = null
|
|
}
|
|
}
|
|
|
|
function handleRoomMessage(message) {
|
|
try {
|
|
const body = JSON.parse(message.body)
|
|
const type = body.type
|
|
if (type === 'MEMBER_LIST' && body.members) {
|
|
onMembers && onMembers(body.members)
|
|
} else if (type === 'MEMBER_JOINED' && body.member) {
|
|
onMemberJoined && onMemberJoined(body.member)
|
|
} else if (type === 'MEMBER_LEFT' && body.member) {
|
|
onMemberLeft && onMemberLeft(body.member, body.sessionId)
|
|
} else if (type === 'CHAT' && body.sender) {
|
|
onChatMessage && onChatMessage(body)
|
|
} else if (type === 'SYNC_ROUTE_VISIBILITY' && body.routeId != null) {
|
|
onSyncRouteVisibility && onSyncRouteVisibility(body.routeId, !!body.visible, body.senderSessionId)
|
|
} else if (type === 'SYNC_WAYPOINTS' && body.routeId != null) {
|
|
onSyncWaypoints && onSyncWaypoints(body.routeId, body.senderSessionId)
|
|
} else if (type === 'SYNC_PLATFORM_ICONS') {
|
|
onSyncPlatformIcons && onSyncPlatformIcons(body.senderSessionId)
|
|
} else if (type === 'SYNC_ROOM_DRAWINGS') {
|
|
onSyncRoomDrawings && onSyncRoomDrawings(body.senderSessionId)
|
|
} else if (type === 'SYNC_PLATFORM_STYLES') {
|
|
onSyncPlatformStyles && onSyncPlatformStyles(body.senderSessionId)
|
|
} else if (type === 'OBJECT_EDIT_LOCK' && body.objectType != null && body.objectId != null && body.editor) {
|
|
onObjectEditLock && onObjectEditLock(body)
|
|
} else if (type === 'OBJECT_EDIT_UNLOCK' && body.objectType != null && body.objectId != null) {
|
|
onObjectEditUnlock && onObjectEditUnlock(body)
|
|
} else if (type === 'USER_ACTIVITIES' && Array.isArray(body.activities)) {
|
|
onUserActivities && onUserActivities(body.activities)
|
|
}
|
|
} catch (e) {
|
|
console.warn('[WebSocket] parse message error:', e)
|
|
}
|
|
}
|
|
|
|
function handlePrivateMessage(message) {
|
|
try {
|
|
const body = JSON.parse(message.body)
|
|
const type = body.type
|
|
if (type === 'PRIVATE_CHAT' && body.sender) {
|
|
onPrivateChat && onPrivateChat(body)
|
|
} else if (type === 'CHAT_HISTORY' && Array.isArray(body.messages)) {
|
|
onChatHistory && onChatHistory(body.messages)
|
|
} else if (type === 'PRIVATE_CHAT_HISTORY' && body.targetUserId != null && Array.isArray(body.messages)) {
|
|
onPrivateChatHistory && onPrivateChatHistory(body.targetUserId, body.messages)
|
|
} else if (type === 'ROOM_STATE' && Array.isArray(body.visibleRouteIds)) {
|
|
onRoomState && onRoomState(body.visibleRouteIds)
|
|
} else if (type === 'USER_ACTIVITIES' && Array.isArray(body.activities)) {
|
|
onUserActivities && onUserActivities(body.activities)
|
|
}
|
|
} catch (e) {
|
|
console.warn('[WebSocket] parse private message error:', e)
|
|
}
|
|
}
|
|
|
|
function connect() {
|
|
const token = getToken()
|
|
if (!token) {
|
|
onError && onError(new Error('未登录'))
|
|
return
|
|
}
|
|
|
|
const sock = new SockJS(getWsUrl())
|
|
client = new Client({
|
|
webSocketFactory: () => sock,
|
|
reconnectDelay: 0,
|
|
heartbeatIncoming: 0,
|
|
heartbeatOutgoing: 0,
|
|
onConnect: () => {
|
|
reconnectAttempts = 0
|
|
roomSubscription = client.subscribe('/topic/room/' + roomId, handleRoomMessage)
|
|
privateSubscription = client.subscribe('/user/queue/private', handlePrivateMessage)
|
|
sendJoin()
|
|
startHeartbeat()
|
|
onConnected && onConnected()
|
|
},
|
|
onStompError: (frame) => {
|
|
console.warn('[WebSocket] STOMP error:', frame)
|
|
onError && onError(new Error(frame.headers?.message || '连接错误'))
|
|
},
|
|
onWebSocketClose: () => {
|
|
stopHeartbeat()
|
|
roomSubscription = null
|
|
privateSubscription = null
|
|
onDisconnected && onDisconnected()
|
|
}
|
|
})
|
|
client.activate()
|
|
}
|
|
|
|
function disconnect() {
|
|
stopHeartbeat()
|
|
sendLeave()
|
|
if (roomSubscription) {
|
|
roomSubscription.unsubscribe()
|
|
roomSubscription = null
|
|
}
|
|
if (privateSubscription) {
|
|
privateSubscription.unsubscribe()
|
|
privateSubscription = null
|
|
}
|
|
if (client) {
|
|
client.deactivate()
|
|
client = null
|
|
}
|
|
}
|
|
|
|
function tryReconnect() {
|
|
if (reconnectAttempts >= maxReconnectAttempts) return
|
|
reconnectAttempts++
|
|
setTimeout(() => {
|
|
disconnect()
|
|
connect()
|
|
}, reconnectDelay)
|
|
}
|
|
|
|
connect()
|
|
|
|
return {
|
|
disconnect,
|
|
reconnect: connect,
|
|
sendChat,
|
|
sendPrivateChat,
|
|
sendPrivateChatHistoryRequest,
|
|
sendSyncRouteVisibility,
|
|
sendSyncWaypoints,
|
|
sendSyncPlatformIcons,
|
|
sendSyncRoomDrawings,
|
|
sendSyncPlatformStyles,
|
|
sendObjectEditLock,
|
|
sendObjectEditUnlock,
|
|
sendUserSelection,
|
|
get connected() {
|
|
return client && client.connected
|
|
}
|
|
}
|
|
}
|
|
|