useWebSocket.ts 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. import { ref, Ref } from 'vue'
  2. import { getAccessToken } from '@/utils/auth'
  3. interface WebSocketConfig {
  4. wsUrl: string
  5. onSOSAlert?: (data: any) => void
  6. onOrderAlert?: (data: any) => void
  7. onHealthAlert?: (data: any) => void
  8. onDeviceDataUpdate?: (data: any) => void
  9. onStatsUpdate?: (data: any) => void
  10. }
  11. export const useWebSocket = (config: WebSocketConfig) => {
  12. const socket: Ref<WebSocket | null> = ref(null)
  13. const isConnecting = ref(false)
  14. const connectionId: Ref<string | null> = ref(null)
  15. const reconnectAttempts = ref(0)
  16. const maxReconnectAttempts = ref(10)
  17. const lastActivityTime = ref('-')
  18. const heartbeatStatus = ref('normal')
  19. const lastHeartbeatTime: Ref<number | null> = ref(null)
  20. const lastHeartbeatAckTime: Ref<number | null> = ref(null)
  21. let heartbeatInterval: ReturnType<typeof setInterval> | null = null
  22. let heartbeatTimeout: ReturnType<typeof setTimeout> | null = null
  23. let reconnectTimeout: ReturnType<typeof setTimeout> | null = null
  24. let lastActivity = Date.now()
  25. const heartbeatIntervalTime = 25000
  26. const heartbeatTimeoutTime = 10000
  27. const updateLastActivity = () => {
  28. lastActivityTime.value = new Date().toLocaleTimeString()
  29. }
  30. const generateClientId = () => {
  31. const timestamp = Date.now()
  32. const random = Math.random().toString(36).substr(2, 9)
  33. return `monitor_${timestamp}_${random}`
  34. }
  35. const sendMessage = (message: any) => {
  36. if (socket.value && socket.value.readyState === WebSocket.OPEN) {
  37. try {
  38. socket.value.send(JSON.stringify(message))
  39. lastActivity = Date.now()
  40. updateLastActivity()
  41. return true
  42. } catch (error) {
  43. console.error('发送消息失败:', error)
  44. return false
  45. }
  46. }
  47. return false
  48. }
  49. const handleHeartbeatAck = (data: any) => {
  50. console.log('心跳消息', data)
  51. if (heartbeatTimeout) {
  52. clearTimeout(heartbeatTimeout)
  53. heartbeatTimeout = null
  54. }
  55. heartbeatStatus.value = 'normal'
  56. lastHeartbeatAckTime.value = Date.now()
  57. lastActivity = Date.now()
  58. updateLastActivity()
  59. }
  60. const handleHeartbeatExpired = () => {
  61. console.error('🚨 心跳已过期,关闭连接并重新连接')
  62. stopHeartbeat()
  63. if (socket.value) {
  64. socket.value.close(1000, '心跳过期')
  65. socket.value = null
  66. }
  67. reconnectAttempts.value = 0
  68. setTimeout(() => {
  69. if (!socket.value && !isConnecting.value) {
  70. connect()
  71. }
  72. }, 1000)
  73. }
  74. const handleHeartbeatTimeout = () => {
  75. console.warn('⏰ 心跳响应超时,关闭连接触发重连')
  76. stopHeartbeat()
  77. if (socket.value) {
  78. socket.value.close(1000, '心跳响应超时')
  79. }
  80. }
  81. const startHeartbeat = () => {
  82. stopHeartbeat()
  83. heartbeatStatus.value = 'normal'
  84. lastHeartbeatTime.value = Date.now()
  85. heartbeatInterval = setInterval(() => {
  86. if (!socket.value || socket.value.readyState !== WebSocket.OPEN) {
  87. heartbeatStatus.value = 'expired'
  88. stopHeartbeat()
  89. return
  90. }
  91. const now = Date.now()
  92. const timeSinceLastHeartbeat = now - (lastHeartbeatTime.value || 0)
  93. const totalTimeout = heartbeatIntervalTime + heartbeatTimeoutTime
  94. if (lastHeartbeatTime.value && timeSinceLastHeartbeat > totalTimeout) {
  95. console.warn(`💔 心跳已过期,距离上次心跳${Math.round(timeSinceLastHeartbeat / 1000)}秒`)
  96. heartbeatStatus.value = 'expired'
  97. handleHeartbeatExpired()
  98. return
  99. }
  100. heartbeatStatus.value = 'waiting'
  101. lastHeartbeatTime.value = now
  102. const params = {
  103. type: 'HEARTBEAT',
  104. timestamp: now,
  105. clientTime: now
  106. }
  107. const success = sendMessage(params)
  108. if (success) {
  109. if (heartbeatTimeout) {
  110. clearTimeout(heartbeatTimeout)
  111. }
  112. heartbeatTimeout = setTimeout(() => {
  113. console.warn('💔 心跳响应超时')
  114. heartbeatStatus.value = 'timeout'
  115. handleHeartbeatTimeout()
  116. }, heartbeatTimeoutTime)
  117. } else {
  118. heartbeatStatus.value = 'timeout'
  119. handleHeartbeatTimeout()
  120. }
  121. }, heartbeatIntervalTime)
  122. }
  123. const stopHeartbeat = () => {
  124. if (heartbeatInterval) {
  125. clearInterval(heartbeatInterval)
  126. heartbeatInterval = null
  127. }
  128. if (heartbeatTimeout) {
  129. clearTimeout(heartbeatTimeout)
  130. heartbeatTimeout = null
  131. }
  132. }
  133. const checkConnectionHealth = () => {
  134. if (!socket.value || socket.value.readyState !== WebSocket.OPEN) {
  135. return
  136. }
  137. const now = Date.now()
  138. const timeSinceLastActivity = now - lastActivity
  139. const timeSinceLastHeartbeat = now - (lastHeartbeatTime.value || 0)
  140. const totalTimeout = heartbeatIntervalTime + heartbeatTimeoutTime
  141. if (timeSinceLastActivity > totalTimeout + 10000) {
  142. console.error('🚨 连接长时间无活动')
  143. heartbeatStatus.value = 'expired'
  144. handleHeartbeatExpired()
  145. return
  146. }
  147. if (
  148. heartbeatStatus.value === 'waiting' &&
  149. timeSinceLastHeartbeat > heartbeatTimeoutTime + 5000
  150. ) {
  151. console.warn('⚠️ 心跳响应延迟')
  152. sendMessage({ type: 'PING', timestamp: now })
  153. }
  154. }
  155. const handleOpen = () => {
  156. isConnecting.value = false
  157. reconnectAttempts.value = 0
  158. lastActivity = Date.now()
  159. startHeartbeat()
  160. const postData = {
  161. type: 'AUTH',
  162. clientType: 'homecare-web',
  163. clientId: generateClientId(),
  164. timestamp: Date.now(),
  165. accessToken: `Bearer ${getAccessToken()}`
  166. }
  167. sendMessage(postData)
  168. }
  169. const handleMessage = (event: MessageEvent) => {
  170. try {
  171. lastActivity = Date.now()
  172. updateLastActivity()
  173. const data = JSON.parse(event.data)
  174. processIncomingData(data)
  175. } catch (error) {
  176. console.error('消息解析错误:', error)
  177. }
  178. }
  179. const processIncomingData = (data: any) => {
  180. if (!data || !data.type) return
  181. console.log('data', data)
  182. switch (data.type) {
  183. case 'CONNECT_SUCCESS':
  184. connectionId.value = data.connectionId
  185. break
  186. case 'AUTH_SUCCESS':
  187. console.log('身份验证成功')
  188. break
  189. case 'SOS_ALERT':
  190. config.onSOSAlert?.(data)
  191. break
  192. case 'HEALTH_ALERT':
  193. config.onHealthAlert?.(data)
  194. break
  195. case 'DEVICE_DATA_UPDATE':
  196. config.onDeviceDataUpdate?.(data)
  197. break
  198. case 'ORDER_ALERT':
  199. config.onOrderAlert?.(data)
  200. break
  201. case 'SYSTEM_STATS_UPDATE':
  202. config.onStatsUpdate?.(data)
  203. break
  204. case 'HEARTBEAT_ACK':
  205. handleHeartbeatAck(data)
  206. break
  207. default:
  208. lastActivity = Date.now()
  209. updateLastActivity()
  210. }
  211. }
  212. const handleClose = (event: CloseEvent) => {
  213. console.log(`WebSocket连接关闭: 代码 ${event.code}`)
  214. isConnecting.value = false
  215. socket.value = null
  216. connectionId.value = null
  217. heartbeatStatus.value = 'expired'
  218. stopHeartbeat()
  219. if (reconnectTimeout) {
  220. clearTimeout(reconnectTimeout)
  221. reconnectTimeout = null
  222. }
  223. if (event.code !== 1000 && reconnectAttempts.value < maxReconnectAttempts.value) {
  224. reconnectAttempts.value++
  225. const delay = Math.min(3000 * Math.pow(1.5, reconnectAttempts.value - 1), 30000)
  226. console.log(`${Math.round(delay / 1000)}秒后尝试重连`)
  227. reconnectTimeout = setTimeout(() => {
  228. if (!socket.value && !isConnecting.value) {
  229. connect()
  230. }
  231. }, delay)
  232. } else if (reconnectAttempts.value >= maxReconnectAttempts.value) {
  233. console.error('已达到最大重连次数')
  234. heartbeatStatus.value = 'expired'
  235. }
  236. }
  237. const handleError = (event: Event) => {
  238. console.error('WebSocket错误:', event)
  239. }
  240. const connect = () => {
  241. if (isConnecting.value || socket.value) {
  242. return
  243. }
  244. isConnecting.value = true
  245. if (reconnectTimeout) {
  246. clearTimeout(reconnectTimeout)
  247. reconnectTimeout = null
  248. }
  249. try {
  250. const clientId = generateClientId()
  251. const wsUrl = config.wsUrl + clientId
  252. socket.value = new WebSocket(wsUrl)
  253. socket.value.onopen = handleOpen
  254. socket.value.onmessage = handleMessage
  255. socket.value.onclose = handleClose
  256. socket.value.onerror = handleError
  257. } catch (error) {
  258. console.error('连接创建错误:', error)
  259. isConnecting.value = false
  260. socket.value = null
  261. if (reconnectAttempts.value < maxReconnectAttempts.value) {
  262. reconnectAttempts.value++
  263. const delay = Math.min(3000 * Math.pow(1.5, reconnectAttempts.value - 1), 30000)
  264. reconnectTimeout = setTimeout(() => {
  265. if (!socket.value && !isConnecting.value) {
  266. connect()
  267. }
  268. }, delay)
  269. }
  270. }
  271. }
  272. const disconnect = () => {
  273. stopHeartbeat()
  274. if (reconnectTimeout) {
  275. clearTimeout(reconnectTimeout)
  276. reconnectTimeout = null
  277. }
  278. if (socket.value) {
  279. socket.value.close(1000, '主动关闭')
  280. socket.value = null
  281. }
  282. }
  283. return {
  284. socket,
  285. isConnecting,
  286. connectionId,
  287. reconnectAttempts,
  288. lastActivityTime,
  289. heartbeatStatus,
  290. lastHeartbeatTime,
  291. lastHeartbeatAckTime,
  292. connect,
  293. disconnect,
  294. sendMessage,
  295. checkConnectionHealth,
  296. stopHeartbeat
  297. }
  298. }