From a6842ea879721063affc0eb51f75a02b59c7c1f4 Mon Sep 17 00:00:00 2001
From: 张含笑 <zhx18749296735@163.com>
Date: Thu, 15 Jan 2026 16:12:32 +0800
Subject: [PATCH] feat:语音通话模板

---
 applications/mobile-web-view/src/appPages/voiceCallDetail/index.vue | 1058 ++++++++++++++++++++-------------------
 uniapps/work-app/src/hooks/useGlobalWS.js                           |  243 +++++---
 uniapps/work-app/src/utils/websocket.js                             |  188 +++++++
 uniapps/work-app/src/pages/login/index.vue                          |   53 ++
 4 files changed, 938 insertions(+), 604 deletions(-)

diff --git a/applications/mobile-web-view/src/appPages/voiceCallDetail/index.vue b/applications/mobile-web-view/src/appPages/voiceCallDetail/index.vue
index ff4cdd5..91c6124 100644
--- a/applications/mobile-web-view/src/appPages/voiceCallDetail/index.vue
+++ b/applications/mobile-web-view/src/appPages/voiceCallDetail/index.vue
@@ -1,55 +1,55 @@
 <template>
 	<div class="call-container" :class="{'incoming-call': state === 'ringing'} ">
-	  <div class="content-wrapper">
-<!--		<div class="input-group">-->
-<!--			<div class="input-item">-->
-<!--				<span class="input-label">我的 userId:</span>-->
-<!--				<input v-model="uid" class="input-field" />-->
-<!--			</div>-->
-<!--		  -->
-<!--			<div class="input-item">-->
-<!--				<span class="input-label">对方 userId:</span>-->
-<!--				<input v-model="peerUid" class="input-field" />-->
-<!--			</div>-->
-<!--		</div>-->
-  
-		<div class="button-group" v-if="false">
-			<button @click="connectWS" :disabled="connected" class="btn btn-connect">连接WS</button>
-		</div>
-      <!--  头像-->
-	 	<div class="contactAvatar">
-	 		<img :src="defaultAvatar" alt=""></img>
-	 	</div>
-	 	<div class="status-group">
-			<strong v-if="state==='ringing'" class="status-text incoming-status">来电中...</strong>
-			<strong v-if="state==='calling'" class="status-text calling-status">呼叫中...</strong>
-		<!--	<strong class="status-text">状态:<span :style="{color: getStateColor(state)}">{{ state }}</span></strong>-->
-			<strong v-if="state==='in-call'" class="status-text">通话时长:{{ durationText }}</strong>
-		</div>
-		<div class="button-group">
-			<button v-if="false" @click="requestCall" :disabled="!connected || state!=='idle'" class="btn btn-call">呼叫</button>
-		  
-			<button v-if="state==='ringing'" @click="acceptCall" class="btn btn-accept incoming-accept">接听</button>
-			<button v-if="state==='ringing'" @click="rejectCall" class="btn btn-reject incoming-reject">拒绝</button>
-		  
-			<button v-if="state==='calling' || state==='in-call'" @click="hangup" class="btn btn-hangup">挂断</button>
-		</div>
-  
+		<div class="content-wrapper">
+			<!--		<div class="input-group">
+					<div class="input-item">
+						<span class="input-label">我的 userId:</span>
+						<input v-model="uid" class="input-field" />
+					</div>
 
-	  </div>
-  
-<!--	  <div class="tip-message">-->
-<!--		<small>提示:若 getUserMedia 不可用,请用 https 或 localhost 访问页面。</small>-->
-<!--	  </div>-->
-  
-<!--	  <div class="log-container">-->
-<!--		<div class="log-box">-->
-<!--			<div class="log-title">日志信息:</div>-->
-<!--			<pre class="log-content">{{ logText }}</pre>-->
-<!--		</div>-->
-<!--	  </div>-->
-  
-	  <audio ref="remoteAudio" autoplay class="hidden-audio"></audio>
+					<div class="input-item">
+						<span class="input-label">对方 userId:</span>
+						<input v-model="peerUid" class="input-field" />
+					</div>
+				</div>-->
+
+			<div class="button-group" v-if="false">
+				<button @click="connectWS" :disabled="connected" class="btn btn-connect">连接WS</button>
+			</div>
+			<!--  头像-->
+			<div class="contactAvatar">
+				<img :src="defaultAvatar" alt=""></img>
+			</div>
+			<div class="status-group">
+				<strong v-if="state==='ringing'" class="status-text incoming-status">来电中...</strong>
+				<strong v-if="state==='calling'" class="status-text calling-status">呼叫中...</strong>
+				<!--	<strong class="status-text">状态:<span :style="{color: getStateColor(state)}">{{ state }}</span></strong>-->
+				<strong v-if="state==='in-call'" class="status-text">通话时长:{{ durationText }}</strong>
+			</div>
+			<div class="button-group">
+				<button  @click="requestCall" :disabled="!connected || state!=='idle'" class="btn btn-call">呼叫</button>
+
+				<button v-if="state==='ringing'" @click="acceptCall" class="btn btn-accept incoming-accept">接听</button>
+				<button v-if="state==='ringing'" @click="rejectCall" class="btn btn-reject incoming-reject">拒绝</button>
+
+				<button v-if="state==='calling' || state==='in-call'" @click="hangup" class="btn btn-hangup">挂断</button>
+			</div>
+
+
+		</div>
+
+		<!--	<div class="tip-message">
+			<small>提示:若 getUserMedia 不可用,请用 https 或 localhost 访问页面。</small>
+			</div>-->
+
+		<!--	<div class="log-container">
+			<div class="log-box">
+				<div class="log-title">日志信息:</div>
+				<pre class="log-content">{{ logText }}</pre>
+			</div>
+			</div>-->
+
+		<audio ref="remoteAudio" autoplay class="hidden-audio"></audio>
 	</div>
 </template>
 
@@ -60,25 +60,35 @@
 /** ✅ 你的 WS 地址前缀(后面拼 userId) */
 const WS_BASE = 'wss://wrj.shuixiongit.com/ws/chat?userId='
 
-  // 解析URL参数
-  const parseUrlParams = () => {
-    const url = new URL(window.location.href)
-    const paramsStr = url.searchParams.get('params')
-    if (paramsStr) {
-      try {
-        const params = JSON.parse(decodeURIComponent(paramsStr))
-        if (params.peerUid) {
-          peerUid.value = String(params.peerUid)
-          log('🔗 从URL参数获取peerUid:', params.peerUid)
-        }
-      } catch (e) {
-        log('❌ 解析URL参数失败:', String(e))
-      }
-    }
-  }
+// 解析URL参数
+const parseUrlParams = () => {
+	const url = new URL(window.location.href)
+	const paramsStr = url.searchParams.get('params')
+	if (paramsStr) {
+		try {
+			const params = JSON.parse(decodeURIComponent(paramsStr))
+			if (params.peerUid) {
+				peerUid.value = String(params.peerUid)
+				log('🔗 从URL参数获取peerUid:', params.peerUid)
+			}
+		} catch (e) {
+			log('❌ 解析URL参数失败:', String(e))
+		}
+	}
+}
 
-  const uid = ref('3')
-  const peerUid = ref('2')
+// 从URL参数或本地存储获取用户ID
+const getUserIdFromStorage = () => {
+	const userId = localStorage.getItem('userId') || sessionStorage.getItem('userId')
+	if (userId) {
+		log('🔗 从本地存储获取userId:', userId)
+		return userId
+	}
+	return '3' // 默认值
+}
+
+const uid = ref('3')
+const peerUid = ref('2')
 
 const connected = ref(false)
 const state = ref('idle') // idle | calling | ringing | in-call
@@ -115,130 +125,130 @@
 	const m = String(Math.floor(seconds.value / 60)).padStart(2, '0')
 	const s = String(seconds.value % 60).padStart(2, '0')
 	return `${m}:${s}`
-  })
-  function startTimer() {
+})
+function startTimer() {
 	stopTimer()
 	seconds.value = 0
 	timer = setInterval(() => seconds.value++, 1000)
-  }
-  function stopTimer() {
+}
+function stopTimer() {
 	if (timer) clearInterval(timer)
 	timer = null
-  }
-  
-  // 心跳
-  let pingTimer = null
-  function startPing() {
+}
+
+// 心跳
+let pingTimer = null
+function startPing() {
 	stopPing()
 	// 每 25 秒发一次 ping(你后端有 PING/PONG)
 	pingTimer = setInterval(() => {
-	  if (connected.value) send('ping', 'system', null)
+		if (connected.value) send('ping', 'system', null)
 	}, 25000)
-  }
+}
 
-  // 来电提示相关
-  let ringtoneAudio = null
-  let vibrationTimer = null
+// 来电提示相关
+let ringtoneAudio = null
+let vibrationTimer = null
 
-  // 播放来电铃声
-  function playRingtone() {
+// 播放来电铃声
+function playRingtone() {
 	// 创建音频对象(使用默认的系统提示音)
 	ringtoneAudio = new Audio('data:audio/wav;base64,UklGRigAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQQAAAAAAA==');
 	ringtoneAudio.loop = true;
 	ringtoneAudio.volume = 0.5;
-	
+
 	try {
-	  ringtoneAudio.play();
-	  log('🔊 开始播放来电铃声')
+		ringtoneAudio.play();
+		log('🔊 开始播放来电铃声')
 	} catch (e) {
-	  log('❌ 播放来电铃声失败:', String(e))
+		log('❌ 播放来电铃声失败:', String(e))
 	}
-  }
+}
 
-  // 停止来电铃声
-  function stopRingtone() {
+// 停止来电铃声
+function stopRingtone() {
 	if (ringtoneAudio) {
-	  try {
-		ringtoneAudio.pause();
-		ringtoneAudio.currentTime = 0;
-		ringtoneAudio = null;
-		log('🔇 停止播放来电铃声')
-	  } catch (e) {
-		log('❌ 停止来电铃声失败:', String(e))
-	  }
+		try {
+			ringtoneAudio.pause();
+			ringtoneAudio.currentTime = 0;
+			ringtoneAudio = null;
+			log('🔇 停止播放来电铃声')
+		} catch (e) {
+			log('❌ 停止来电铃声失败:', String(e))
+		}
 	}
-  }
+}
 
-  // 开始震动
-  function startVibration() {
+// 开始震动
+function startVibration() {
 	// 检查浏览器是否支持震动 API
 	if ('vibrate' in navigator) {
-	  // 震动模式:震动1秒,暂停0.5秒,重复
-	  const vibrationPattern = [1000, 500];
-	  
-	  try {
-		navigator.vibrate(vibrationPattern);
-		vibrationTimer = setInterval(() => {
-		  navigator.vibrate(vibrationPattern);
-		}, 1500); // 每1.5秒重复一次震动模式
-		log('📳 开始震动提示')
-	  } catch (e) {
-		log('❌ 震动失败:', String(e))
-	  }
-	} else {
-	  log('ℹ️ 当前浏览器不支持震动功能')
-	}
-  }
+		// 震动模式:震动1秒,暂停0.5秒,重复
+		const vibrationPattern = [1000, 500];
 
-  // 停止震动
-  function stopVibration() {
+		try {
+			navigator.vibrate(vibrationPattern);
+			vibrationTimer = setInterval(() => {
+				navigator.vibrate(vibrationPattern);
+			}, 1500); // 每1.5秒重复一次震动模式
+			log('📳 开始震动提示')
+		} catch (e) {
+			log('❌ 震动失败:', String(e))
+		}
+	} else {
+		log('ℹ️ 当前浏览器不支持震动功能')
+	}
+}
+
+// 停止震动
+function stopVibration() {
 	if (vibrationTimer) {
-	  clearInterval(vibrationTimer);
-	  vibrationTimer = null;
+		clearInterval(vibrationTimer);
+		vibrationTimer = null;
 	}
-	
+
 	if ('vibrate' in navigator) {
-	  try {
-		navigator.vibrate(0); // 停止震动
-		log('📳 停止震动提示')
-	  } catch (e) {
-		log('❌ 停止震动失败:', String(e))
-	  }
+		try {
+			navigator.vibrate(0); // 停止震动
+			log('📳 停止震动提示')
+		} catch (e) {
+			log('❌ 停止震动失败:', String(e))
+		}
 	}
-  }
-  function stopPing() {
+}
+function stopPing() {
 	if (pingTimer) clearInterval(pingTimer)
 	pingTimer = null
-  }
-  
-  function log(...args) {
+}
+
+function log(...args) {
 	const line = args.map(v => (typeof v === 'string' ? v : JSON.stringify(v))).join(' ')
 	logText.value += `[${new Date().toLocaleTimeString()}] ${line}\n`
 	console.log(...args)
-  }
-  
-  /**
-   * ✅ 严格按后端 ChatMessage 字段发:
-   * { type, from, to, payload, timestamp }
-   * 注意:后端会用 session 的 userId 覆盖 from,但我们带上也没坏处
-   */
-  function send(type, to, payload) {
+}
+
+/**
+ * ✅ 严格按后端 ChatMessage 字段发:
+ * { type, from, to, payload, timestamp }
+ * 注意:后端会用 session 的 userId 覆盖 from,但我们带上也没坏处
+ */
+function send(type, to, payload) {
 	if (!type) return
 	if (!ws || ws.readyState !== WebSocket.OPEN) return
-  
+
 	const msg = {
-	  type,                 // "call/accept/busy/offer/answer/candidate/hangup/ping"
-	  from: String(uid.value),
-	  to: String(to),
-	  payload: payload ?? null,
-	  timestamp: Date.now(),
+		type,                 // "call/accept/busy/offer/answer/candidate/hangup/ping"
+		from: String(uid.value),
+		to: String(to),
+		payload: payload ?? null,
+		timestamp: Date.now(),
 	}
 	ws.send(JSON.stringify(msg))
-  }
-  
-  /* ---------------- WebRTC ---------------- */
-  
-  function createPC() {
+}
+
+/* ---------------- WebRTC ---------------- */
+
+function createPC() {
 	pc = new RTCPeerConnection({
 		iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
 	})
@@ -318,6 +328,8 @@
 /* ---------------- 信令处理 ---------------- */
 
 async function onSignal(msg) {
+console.log('1cesss',msg);
+
 	if (!msg) return
 	const t = (msg.type || '').toString()
 
@@ -343,11 +355,11 @@
 
 		state.value = 'ringing'
 		log('📞 收到来电 call,来自', incomingFrom)
-		
+
 		// 开始来电提示(铃声和震动)
 		playRingtone()
 		startVibration()
-		
+
 		return
 	}
 
@@ -363,104 +375,107 @@
 		cleanup(false)
 		return
 	}
-  
+
 	if (t === 'offer') {
-	  // 被叫收到 offer:只有点击接听后才真正应答(你要求“先点接听”)
-	  offeredByPeer = true
-	  incomingFrom = String(msg.from)
-	  peerUid.value = incomingFrom
-  
-	  if (!acceptedByMe) {
-		state.value = 'ringing'
-		log('📩 收到 offer(但未接听),已等待用户点击接听')
-		// 暂存 offer 到一个变量
-		lastOfferSdp = msg.payload?.sdp
+		// 被叫收到 offer:只有点击接听后才真正应答(你要求“先点接听”)
+		offeredByPeer = true
+		incomingFrom = String(msg.from)
+		peerUid.value = incomingFrom
+
+		if (!acceptedByMe) {
+			state.value = 'ringing'
+			log('📩 收到 offer(但未接听),已等待用户点击接听')
+			// 暂存 offer 到一个变量
+			lastOfferSdp = msg.payload?.sdp
+			return
+		}
+
+		// 已接听:直接 answer
+		await answerOffer(msg.payload?.sdp, incomingFrom)
 		return
-	  }
-  
-	  // 已接听:直接 answer
-	  await answerOffer(msg.payload?.sdp, incomingFrom)
-	  return
 	}
-  
+
 	if (t === 'answer') {
-	  log('✅ 收到 answer,通话建立')
-	  if (!pc) createPC()
-	  await pc.setRemoteDescription(new RTCSessionDescription(msg.payload?.sdp))
-	  await flushCandidates()
-  
-	  state.value = 'in-call'
-	  startTimer()
-	  return
+		log('✅ 收到 answer,通话建立')
+		if (!pc) createPC()
+		await pc.setRemoteDescription(new RTCSessionDescription(msg.payload?.sdp))
+		await flushCandidates()
+
+		state.value = 'in-call'
+		startTimer()
+		return
 	}
-  
+
 	if (t === 'candidate') {
-	  const c = msg.payload?.candidate
-	  if (!c) return
-	  if (pc && pc.remoteDescription) {
-		await pc.addIceCandidate(new RTCIceCandidate(c))
-	  } else {
-		pendingCandidates.push(c)
-	  }
-	  return
+		const c = msg.payload?.candidate
+		if (!c) return
+		if (pc && pc.remoteDescription) {
+			await pc.addIceCandidate(new RTCIceCandidate(c))
+		} else {
+			pendingCandidates.push(c)
+		}
+		return
 	}
-  
+
 	if (t === 'hangup') {
-	  log('📴 对方挂断')
-	  cleanup(false)
-	  return
+		log('📴 对方挂断')
+		cleanup(false)
+		return
 	}
-  
+
 	if (t === 'pong') {
-	  // 心跳响应
-	  // log('pong')
-	  return
+		// 心跳响应
+		// log('pong')
+		return
 	}
-  }
-  
-  // 暂存 offer(用于“先点接听再 answer”)
-  let lastOfferSdp = null
-  
-  /* ---------------- 业务操作 ---------------- */
-  
-  function connectWS() {
+}
+
+// 暂存 offer(用于“先点接听再 answer”)
+let lastOfferSdp = null
+
+/* ---------------- 业务操作 ---------------- */
+
+function connectWS() {
+	// 确保获取最新的用户ID
+	uid.value = getUserIdFromStorage()
+
 	if (ws) ws.close()
-  
+
 	ws = new WebSocket(WS_BASE + encodeURIComponent(uid.value))
-  
+
 	ws.onopen = () => {
-	  connected.value = true
-	  log('WS 已连接 userId=', uid.value)
-	  startPing()
+		connected.value = true
+		log('WS 已连接 userId=', uid.value)
+		startPing()
 	}
-  
+
 	ws.onclose = () => {
-	  connected.value = false
-	  log('WS 已关闭')
-	  stopPing()
+		connected.value = false
+		log('WS 已关闭')
+		stopPing()
 	}
-  
+
 	ws.onerror = (e) => log('WS error', e)
-  
+
 	ws.onmessage = (e) => {
-	  const msg = JSON.parse(e.data)
-	  log('收到信令', msg)
-	  onSignal(msg)
+		const msg = JSON.parse(e.data)
+		log('收到信令', msg)
+		onSignal(msg)
 	}
-  }
-  
-  function requestCall() {
+}
+
+function requestCall() {
 	state.value = 'calling'
 	incomingFrom = null
 	acceptedByMe = false
 	offeredByPeer = false
 	lastOfferSdp = null
-  
+
 	send('call', peerUid.value, null)
 	log('➡️ 发起呼叫 call 给', peerUid.value)
-  }
-  
-  function acceptCall() {
+}
+
+function acceptCall() {
 	if (!incomingFrom) return
 	acceptedByMe = true
 
@@ -476,12 +491,12 @@
 
 	// 如果 offer 已经提前到达(我们暂存了),立刻 answer
 	if (lastOfferSdp) {
-	  answerOffer(lastOfferSdp, incomingFrom)
-	  lastOfferSdp = null
+		answerOffer(lastOfferSdp, incomingFrom)
+		lastOfferSdp = null
 	}
-  }
-  
-  function rejectCall() {
+}
+
+function rejectCall() {
 	if (!incomingFrom) return
 
 	// 停止来电提示
@@ -491,102 +506,117 @@
 	// 你后端没有 reject,用 busy 表示拒绝/不可接听
 	send('busy', incomingFrom, null)
 	log('❌ 已拒绝来电,发送 busy 给', incomingFrom)
-  
+
 	incomingFrom = null
 	acceptedByMe = false
 	offeredByPeer = false
 	lastOfferSdp = null
 	state.value = 'idle'
-	
+
 	// 返回上一页(uni-app的语音通话列表页面)
 	if (window.uni) {
-	  window.uni.navigateBack()
+		window.uni.navigateBack()
 	} else {
-	  log('❌ uni对象不存在,无法返回上一页')
+		log('❌ uni对象不存在,无法返回上一页')
 	}
-  }
-  
-  async function startOffer() {
+}
+
+async function startOffer() {
 	if (!pc) createPC()
-  
+
 	// ✅ 不会重复 addTrack
 	await ensureLocalTracksAdded()
-  
+
 	const offer = await pc.createOffer()
 	await pc.setLocalDescription(offer)
-  
+
 	send('offer', peerUid.value, { sdp: pc.localDescription })
 	log('➡️ 已发送 offer 给', peerUid.value)
-  }
-  
-  async function answerOffer(offerSdp, from) {
+}
+
+async function answerOffer(offerSdp, from) {
 	if (!offerSdp) {
-	  log('❌ offer sdp 为空,无法接听')
-	  return
+		log('❌ offer sdp 为空,无法接听')
+		return
 	}
 	if (!pc) createPC()
-  
+
 	await pc.setRemoteDescription(new RTCSessionDescription(offerSdp))
 	await flushCandidates()
-  
+
 	await ensureLocalTracksAdded()
-  
+
 	const answer = await pc.createAnswer()
 	await pc.setLocalDescription(answer)
-  
+
 	send('answer', from, { sdp: pc.localDescription })
 	log('⬅️ 已发送 answer 给', from)
-  
+
 	state.value = 'in-call'
 	startTimer()
-  }
-  
-  function hangup() {
+}
+
+function hangup() {
 	cleanup(true)
-  }
-  
-  /* ---------------- 清理 ---------------- */
-  
-  function cleanup(sendToPeer) {
+}
+
+/* ---------------- 清理 ---------------- */
+
+function cleanup(sendToPeer) {
 	try {
-	  if (sendToPeer) send('hangup', peerUid.value, null)
+		if (sendToPeer) send('hangup', peerUid.value, null)
 	} catch {}
-  
+
 	stopTimer()
 	// 停止来电提示
 	stopRingtone()
 	stopVibration()
 
 	if (pc) {
-	  pc.getSenders().forEach(s => s.track && s.track.stop())
-	  pc.close()
+		pc.getSenders().forEach(s => s.track && s.track.stop())
+		pc.close()
 	}
 	if (localStream) {
-	  localStream.getTracks().forEach(t => t.stop())
+		localStream.getTracks().forEach(t => t.stop())
 	}
-  
+
 	pc = null
 	localStream = null
 	pendingCandidates.length = 0
-  
+
 	incomingFrom = null
 	acceptedByMe = false
 	offeredByPeer = false
 	lastOfferSdp = null
-  
+
 	state.value = 'idle'
-	
+
 	// 返回上一页(uni-app的语音通话列表页面)
 	if (window.uni) {
-	  window.uni.navigateBack()
+		window.uni.navigateBack()
 	} else {
-	  log('❌ uni对象不存在,无法返回上一页')
+		log('❌ uni对象不存在,无法返回上一页')
 	}
 }
 
 onMounted(() => {
 	const receiveParameters = route.query.contact
 	console.log('接收参数', receiveParameters)
+
+	// 解析并使用contact参数
+	if (receiveParameters) {
+		try {
+			const contact = JSON.parse(decodeURIComponent(receiveParameters))
+			if (contact.friendId) {
+				peerUid.value = contact.friendId
+				log('🔗 从contact参数获取peerUid:', contact.friendId)
+				log('👤 联系人名称:', contact.friendNickName || '未知联系人')
+			}
+		} catch (e) {
+			log('❌ 解析contact参数失败:', String(e))
+		}
+	}
+
 	// 解析URL参数
 	parseUrlParams()
 	// 自动连接WebSocket
@@ -594,258 +624,258 @@
 })
 
 // 监听连接状态,连接成功后自动呼叫
-watch(() => connected.value, (newVal) => {
-	if (newVal && state.value === 'idle') {
-		requestCall()
-	}
+// watch(() => connected.value, (newVal) => {
+// 	if (newVal && state.value === 'idle') {
+// 		requestCall()
+// 	}
+// })
+
+onBeforeUnmount(() => {
+	cleanup(false)
+	stopPing()
+	if (ws) ws.close()
 })
+</script>
+<style scoped lang="scss">
+.call-container {
+	padding: 20px;
+	min-height: 100vh;
+	font-family: Arial, sans-serif;
+	background-color: #f5f5f5;
+	transition: all 0.3s ease;
+}
 
-  onBeforeUnmount(() => {
-    cleanup(false)
-    stopPing()
-    if (ws) ws.close()
-  })
-  </script>
-  <style scoped lang="scss">
-	.call-container {
-		padding: 20px;
-		min-height: 100vh;
-		font-family: Arial, sans-serif;
-		background-color: #f5f5f5;
-		transition: all 0.3s ease;
+/* 来电时的全屏样式 */
+.call-container.incoming-call {
+	position: fixed;
+	top: 0;
+	left: 0;
+	right: 0;
+	bottom: 0;
+	z-index: 99999;
+	background-color: #ffffff;
+	padding: 0;
+}
+
+.content-wrapper {
+	display: flex;
+	flex-direction: column;
+	gap: 20px;
+	max-width: 600px;
+	margin: 0 auto;
+	height: 100vh;
+	justify-content: center;
+	align-items: center;
+}
+
+.input-group {
+	display: flex;
+	flex-direction: column;
+	gap: 10px;
+}
+
+.input-item {
+	display: flex;
+	justify-content: space-between;
+	align-items: center;
+}
+
+.input-label {
+	font-weight: bold;
+	color: #333;
+}
+
+.input-field {
+	width: 120px;
+	padding: 8px;
+	border: 1px solid #ddd;
+	border-radius: 4px;
+	font-size: 14px;
+}
+
+.button-group {
+	display: flex;
+	gap: 50px;
+	flex-wrap: wrap;
+	justify-content: center;
+	margin-top: 80px;
+}
+
+.btn {
+	padding: 15px 30px;
+	color: white;
+	border: none;
+	border-radius: 8px;
+	cursor: pointer;
+	font-size: 16px;
+	font-weight: bold;
+	transition: all 0.3s ease;
+	min-width: 120px;
+
+	&:disabled {
+		opacity: 0.6;
+		cursor: not-allowed;
 	}
+}
 
-	/* 来电时的全屏样式 */
-	.call-container.incoming-call {
-		position: fixed;
-		top: 0;
-		left: 0;
-		right: 0;
-		bottom: 0;
-		z-index: 99999;
-		background-color: #ffffff;
-		padding: 0;
+/* 来电时的接听和拒绝按钮样式 */
+.btn.incoming-accept, .btn.incoming-reject {
+	width: 80px;
+	height: 80px;
+	border-radius: 50%;
+	padding: 0;
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
+}
+
+.btn.incoming-accept {
+	background-color: #4CAF50;
+	transform: scale(1.1);
+}
+
+.btn.incoming-reject {
+	background-color: #f44336;
+}
+
+.btn.incoming-accept:active {
+	transform: scale(0.95);
+}
+
+.btn.incoming-reject:active {
+	transform: scale(0.95);
+}
+
+.btn-connect {
+	background-color: #4CAF50;
+}
+
+.btn-call {
+	background-color: #2196F3;
+}
+
+.btn-accept {
+	background-color: #4CAF50;
+}
+
+.btn-reject {
+	background-color: #f44336;
+}
+
+.btn-hangup {
+	background-color: #9E9E9E;
+	padding: 15px 40px;
+	min-width: 150px;
+}
+
+.status-group {
+	display: flex;
+	flex-direction: column;
+	gap: 10px;
+	align-items: center;
+}
+
+.status-text {
+	font-size: 18px;
+	color: #333;
+	text-align: center;
+	margin-bottom: 20px;
+}
+
+/* 来电状态样式 */
+.status-text.incoming-status {
+	font-size: 24px;
+	color: #333;
+	font-weight: bold;
+	animation: pulse 1.5s infinite;
+}
+
+.status-text.calling-status {
+	font-size: 20px;
+	color: #666;
+}
+
+/* 呼吸灯动画 */
+@keyframes pulse {
+	0% {
+		opacity: 1;
+		transform: scale(1);
 	}
-
-	.content-wrapper {
-		display: flex;
-		flex-direction: column;
-		gap: 20px;
-		max-width: 600px;
-		margin: 0 auto;
-		height: 100vh;
-		justify-content: center;
-		align-items: center;
+	50% {
+		opacity: 0.7;
+		transform: scale(1.05);
 	}
-
-	.input-group {
-		display: flex;
-		flex-direction: column;
-		gap: 10px;
+	100% {
+		opacity: 1;
+		transform: scale(1);
 	}
+}
 
-	.input-item {
-		display: flex;
-		justify-content: space-between;
-		align-items: center;
+.tip-message {
+	margin-top: 15px;
+	color: #666;
+	text-align: center;
+}
+
+.log-container {
+	margin-top: 20px;
+	max-width: 600px;
+	margin-left: auto;
+	margin-right: auto;
+}
+
+.log-box {
+	background-color: white;
+	border: 1px solid #ddd;
+	border-radius: 8px;
+	padding: 10px;
+}
+
+.log-title {
+	font-weight: bold;
+	color: #333;
+	margin-bottom: 8px;
+}
+
+.log-content {
+	max-height: 200px;
+	overflow: auto;
+	background-color: #fafafa;
+	padding: 10px;
+	border-radius: 4px;
+	font-size: 12px;
+	line-height: 1.4;
+}
+
+.hidden-audio {
+	display: none;
+}
+
+/* 头像样式 */
+.contactAvatar {
+	width: 180px;
+	height: 180px;
+	display: flex;
+	justify-content: center;
+	align-items: center;
+	margin-bottom: 40px;
+	border-radius: 50%;
+	background-color: #f0f0f0;
+	overflow: hidden;
+	box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
+
+	img {
+		width: 100%;
+		height: 100%;
+		object-fit: cover;
 	}
+}
 
-	.input-label {
-		font-weight: bold;
-		color: #333;
-	}
-
-	.input-field {
-		width: 120px;
-		padding: 8px;
-		border: 1px solid #ddd;
-		border-radius: 4px;
-		font-size: 14px;
-	}
-
-	.button-group {
-		display: flex;
-		gap: 50px;
-		flex-wrap: wrap;
-		justify-content: center;
-		margin-top: 80px;
-	}
-
-	.btn {
-		padding: 15px 30px;
-		color: white;
-		border: none;
-		border-radius: 8px;
-		cursor: pointer;
-		font-size: 16px;
-		font-weight: bold;
-		transition: all 0.3s ease;
-		min-width: 120px;
-
-		&:disabled {
-			opacity: 0.6;
-			cursor: not-allowed;
-		}
-	}
-
-	/* 来电时的接听和拒绝按钮样式 */
-	.btn.incoming-accept, .btn.incoming-reject {
-		width: 80px;
-		height: 80px;
-		border-radius: 50%;
-		padding: 0;
-		display: flex;
-		justify-content: center;
-		align-items: center;
-		box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
-	}
-
-	.btn.incoming-accept {
-		background-color: #4CAF50;
-		transform: scale(1.1);
-	}
-
-	.btn.incoming-reject {
-		background-color: #f44336;
-	}
-
-	.btn.incoming-accept:active {
-		transform: scale(0.95);
-	}
-
-	.btn.incoming-reject:active {
-		transform: scale(0.95);
-	}
-
-	.btn-connect {
-		background-color: #4CAF50;
-	}
-
-	.btn-call {
-		background-color: #2196F3;
-	}
-
-	.btn-accept {
-		background-color: #4CAF50;
-	}
-
-	.btn-reject {
-		background-color: #f44336;
-	}
-
-	.btn-hangup {
-		background-color: #9E9E9E;
-		padding: 15px 40px;
-		min-width: 150px;
-	}
-
-	.status-group {
-		display: flex;
-		flex-direction: column;
-		gap: 10px;
-		align-items: center;
-	}
-
-	.status-text {
-		font-size: 18px;
-		color: #333;
-		text-align: center;
-		margin-bottom: 20px;
-	}
-
-	/* 来电状态样式 */
-	.status-text.incoming-status {
-		font-size: 24px;
-		color: #333;
-		font-weight: bold;
-		animation: pulse 1.5s infinite;
-	}
-
-	.status-text.calling-status {
-		font-size: 20px;
-		color: #666;
-	}
-
-	/* 呼吸灯动画 */
-	@keyframes pulse {
-		0% {
-			opacity: 1;
-			transform: scale(1);
-		}
-		50% {
-			opacity: 0.7;
-			transform: scale(1.05);
-		}
-		100% {
-			opacity: 1;
-			transform: scale(1);
-		}
-	}
-
-	.tip-message {
-		margin-top: 15px;
-		color: #666;
-		text-align: center;
-	}
-
-	.log-container {
-		margin-top: 20px;
-		max-width: 600px;
-		margin-left: auto;
-		margin-right: auto;
-	}
-
-	.log-box {
-		background-color: white;
-		border: 1px solid #ddd;
-		border-radius: 8px;
-		padding: 10px;
-	}
-
-	.log-title {
-		font-weight: bold;
-		color: #333;
-		margin-bottom: 8px;
-	}
-
-	.log-content {
-		max-height: 200px;
-		overflow: auto;
-		background-color: #fafafa;
-		padding: 10px;
-		border-radius: 4px;
-		font-size: 12px;
-		line-height: 1.4;
-	}
-
-	.hidden-audio {
-		display: none;
-	}
-
-	/* 头像样式 */
-	.contactAvatar {
-		width: 180px;
-		height: 180px;
-		display: flex;
-		justify-content: center;
-		align-items: center;
-		margin-bottom: 40px;
-		border-radius: 50%;
-		background-color: #f0f0f0;
-		overflow: hidden;
-		box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
-		
-		img {
-			width: 100%;
-			height: 100%;
-			object-fit: cover;
-		}
-	}
-
-	/* 来电时的头像放大效果 */
-	.call-container.incoming-call .contactAvatar {
-		width: 220px;
-		height: 220px;
-		margin-bottom: 60px;
-	}
-  </style>
\ No newline at end of file
+/* 来电时的头像放大效果 */
+.call-container.incoming-call .contactAvatar {
+	width: 220px;
+	height: 220px;
+	margin-bottom: 60px;
+}
+</style>
\ No newline at end of file
diff --git a/uniapps/work-app/src/hooks/useGlobalWS.js b/uniapps/work-app/src/hooks/useGlobalWS.js
index 83d6a3e..14c6611 100644
--- a/uniapps/work-app/src/hooks/useGlobalWS.js
+++ b/uniapps/work-app/src/hooks/useGlobalWS.js
@@ -1,104 +1,167 @@
-import {useUserStore} from "@/store/index.js";
-import {getEnvObj} from "@/utils/index.js";
-// import {enterRoom} from "@/utils/voiceCallByTX/index.js";
+import { useUserStore } from "@/store/index.js";
+import { ref, computed, watch } from "vue";
 import useAppStore from "../store/modules/app/index.js";
-
-let socketTask = null
+import websocketService from "@/utils/websocket.js";
 
 export function useGlobalWS() {
 	const userStore = useUserStore();
   const appStore = useAppStore();
-
-	const userId = computed(() => userStore?.userInfo?.user_id)
-	const access_token = computed(() => userStore?.userInfo?.access_token)
-	const {VITE_APP_WS_API_URL} = getEnvObj()
+  
+  // 设置默认用户ID为3
+  const defaultUserId = '3';
+  // WebSocket基础URL
+  const WS_BASE = 'wss://wrj.shuixiongit.com/ws/chat?userId=';
 
 	// 消息处理
-	function messageHandler(payload) {
-		switch (payload.biz_code) {
-			case 'JOB_ISREFRESH':
-        appStore.setJobUpdateKeyAdd()
-				break
-			case 'DEVICE_ISREFRESH':
-        appStore.setDeviceUpdateKeyAdd()
-				break
-			case 'DOWNLOAD_PROGRESS':
-				break
-			case 'LOGOUT_USER':
-				userStore.setUserInfo(null)
-				uni.reLaunch({
-					url: '/pages/login/index'
-				})
-				break
-			case 'VoiceCall':
-				// enterRoom(payload, userId.value)
-				break
-			default:
-				break;
-		}
-	}
+  function messageHandler(payload) {
+    console.log('🌐 全局WebSocket收到消息111111111111111111111:', payload)
+    // 先尝试直接处理消息(适用于mobile-web-view的voiceCallDetail页面的消息格式)
+    const t = (payload.type || '').toString()
+    const bizCode = payload.biz_code || ''
 
-	// 关闭ws
-	function closeWS() {
-		socketTask?.close({
-			success: () => {
-				console.log('ws关闭连接');
-			},
-		})
-	}
+    console.log('📋 消息分析:type=' + t + ', biz_code=' + bizCode)
 
-	// 初始化ws
-	function initWS() {
-		// 关闭,再连接ws
-		closeWS()
-		if (!access_token.value) return
-		const url = VITE_APP_WS_API_URL
-			+ `?x-auth-token=${encodeURI(access_token?.value)}`
-			+ `&model_type=3&workspace-id=${userId.value}`
-		// 创建连接
-		socketTask = uni.connectSocket({
-			url: url,
-			success: () => {
-				console.log('ws连接成功');
-			},
-			fail: (err) => {
-				console.error('ws连接失败:', err);
-			}
-		});
-		// 消息监听
-		socketTask.onMessage((result) => {
-			messageHandler(JSON.parse(result.data))
-		})
-    //==================================
-    // 监听连接打开
-    socketTask.onOpen((res) => {
-      console.log('✅ WebSocket连接已建立')
-      // reconnectAttempts = 0 // 连接成功后重置重连次数
-      // 可以在这里发送心跳或订阅消息
-      // startHeartbeat()
-    })
-    // 监听连接关闭
-    socketTask.onClose((res) => {
-      console.log(`WebSocket连接关闭,代码: ${res.code}, 原因: ${res.reason}`)
+    // 处理语音通话请求
+    if (t === 'call') {
+      console.log('📞 全局收到来电 call,来自', payload.from)
+      // 构建来电参数
+      const callParams = {
+        peerUid: payload.from,
+        from: payload.from,
+        type: 'incoming'
+      };
+      
+      // 转义参数以便在URL中传递
+      const encodedParams = encodeURIComponent(JSON.stringify(callParams));
+      
+      // 跳转到mobile-web-view应用中的语音通话页面
+      const voiceCallUrl = `http://localhost:9527/work-app/#/subPackages/voiceCallDetail/index?params=${encodedParams}`;
+      
+      console.log('🔗 准备导航到:', voiceCallUrl)
 
-      // 根据不同的关闭代码处理
-      if (res.code === 1000) { // 正常关闭
-        console.log('连接正常关闭')
-      } else if (res.code === 1006) { // 异常关闭
-        console.log('连接异常关闭,尝试重连...')
-      } else if (res.code === 1011) { // 服务器内部错误
-        console.log('服务器内部错误(1011),延迟重连...')
-      } else {
-        console.log('其他原因关闭,尝试重连...')
+      try {
+        // 优先使用uni-app的导航API(适用于uni-app环境)
+        if (typeof uni !== 'undefined' && uni.navigateTo) {
+          console.log('使用uni-app导航API跳转到语音通话页面');
+          
+          // 检查是否是跳转到应用内页面
+          if (voiceCallUrl.includes('/subPackages/voiceCallDetail/')) {
+            // 应用内页面跳转
+            uni.navigateTo({
+              url: voiceCallUrl.replace('http://localhost:9527/work-app/#', ''),
+              success: () => {
+                console.log('✅ 应用内导航成功!');
+              },
+              fail: (err) => {
+                console.error('❌ 应用内导航失败:', err);
+                // 如果应用内导航失败,尝试使用外部跳转
+                fallbackToExternalNavigation();
+              }
+            });
+          } else {
+            // 外部页面跳转
+            fallbackToExternalNavigation();
+          }
+        } 
+        // 否则尝试使用window.location跳转(适用于Web环境)
+        else if (typeof window !== 'undefined' && window.location) {
+          console.log('使用window.location跳转到语音通话页面');
+          window.location.href = voiceCallUrl;
+        } else {
+          console.error('无法跳转到语音通话页面:当前环境不支持导航');
+        }
+      } catch (error) {
+        console.error('跳转到语音通话页面失败:', error);
+        // 尝试使用外部跳转作为最后的补救措施
+        fallbackToExternalNavigation();
       }
-    })
+      
+      // 外部页面跳转的回调函数
+      function fallbackToExternalNavigation() {
+        console.log('尝试使用外部导航跳转到语音通话页面');
+        try {
+          // 使用uni-app的外部链接跳转API
+          if (typeof uni !== 'undefined' && uni.navigateToWebview) {
+            // 这是一个示例,具体API可能需要根据uni-app版本和配置调整
+            uni.navigateToWebview({
+              url: voiceCallUrl,
+              success: () => {
+                console.log('✅ 外部Webview导航成功!');
+              },
+              fail: (err) => {
+                console.error('❌ 外部Webview导航失败:', err);
+                // 如果还是失败,尝试使用浏览器打开
+                if (typeof plus !== 'undefined' && plus.runtime) {
+                  plus.runtime.openURL(voiceCallUrl);
+                } else {
+                  console.error('❌ 所有导航方式都失败了');
+                }
+              }
+            });
+          } 
+          // 或者使用plus.runtime.openURL(适用于App环境)
+          else if (typeof plus !== 'undefined' && plus.runtime) {
+            plus.runtime.openURL(voiceCallUrl);
+            console.log('尝试使用plus.runtime.openURL打开外部链接');
+          } else {
+            console.error('无法跳转到外部页面:当前环境不支持外部导航');
+          }
+        } catch (fallbackError) {
+          console.error('外部导航尝试失败:', fallbackError);
+        }
+      }
+      return
+    }
 
-    // 监听错误
-    socketTask.onError((err) => {
-      console.error('WebSocket发生错误:', err)
-    })
-	}
+    // 然后处理biz_code格式的消息
+    switch (bizCode) {
+      case 'JOB_ISREFRESH':
+        appStore.setJobUpdateKeyAdd()
+        break
+      case 'DEVICE_ISREFRESH':
+        appStore.setDeviceUpdateKeyAdd()
+        break
+      case 'DOWNLOAD_PROGRESS':
+        break
+      case 'LOGOUT_USER':
+        userStore.setUserInfo(null)
+        uni.reLaunch({
+          url: '/pages/login/index'
+        })
+        break
+      case 'VoiceCall':
+        // enterRoom(payload, userId.value)
+        break
+      default:
+        // 记录未处理的消息
+        console.log('未处理的WebSocket消息:', payload)
+        break;
+    }
+  }
 
-
-	// watch(access_token, initWS, {immediate: true})
+  // 初始化WebSocket连接
+  function initWS() {
+    try {
+      // 使用默认用户ID初始化WebSocket连接
+      websocketService.init(defaultUserId, WS_BASE);
+      console.log('🌐 全局WebSocket初始化成功,默认用户ID:', defaultUserId);
+      
+      // 设置全局消息处理回调
+      websocketService.setOnMessageCallback(messageHandler);
+      
+      console.log('✅ 全局WebSocket消息处理器已设置');
+    } catch (error) {
+      console.error('❌ 全局WebSocket初始化失败:', error);
+    }
+  }
+  
+  // 监听用户信息变化,初始化WebSocket
+  watch(
+    () => userStore.userInfo,
+    (newUserInfo) => {
+      console.log('👤 用户信息变化,尝试初始化WebSocket');
+      initWS();
+    },
+    { immediate: true }
+  )
 }
diff --git a/uniapps/work-app/src/pages/login/index.vue b/uniapps/work-app/src/pages/login/index.vue
index 618cd64..4dc8802 100644
--- a/uniapps/work-app/src/pages/login/index.vue
+++ b/uniapps/work-app/src/pages/login/index.vue
@@ -45,6 +45,7 @@
 import md5 from "js-md5";
 import { loginByUsername } from "@/api/user/index.js";
 import { useUserStore } from "@/store/index.js";
+import websocketService from "@/utils/websocket.js";
 
 import { HOME_PATH, LOGIN_PATH, removeQueryString } from "@/router";
 
@@ -134,7 +135,59 @@
       userInfo.key,
       userInfo.code
     );
+    console.log('登录成功,用户信息:', res.data.data);
     userStore.setUserInfo(res.data.data);
+
+
+    // 初始化WebSocket连接
+    try {
+      const wsUrl = 'wss://wrj.shuixiongit.com/ws/chat?userId=';
+      // 设置默认用户ID为3
+      const defaultUserId = '3';
+      // 使用默认用户ID初始化WebSocket连接
+      websocketService.init(defaultUserId, wsUrl);
+      console.log('登录成功,WebSocket已初始化,默认用户ID:', defaultUserId);
+      
+      // 设置来电消息回调
+      websocketService.setOnMessageCallback((message) => {
+        if (message.type === 'call') {
+          console.log('收到来电,准备跳转到语音通话页面');
+          // 构建来电参数
+          const callParams = {
+            peerUid: message.from,
+            from: message.from,
+            type: 'incoming'
+          };
+          
+          // 转义参数以便在URL中传递
+          const encodedParams = encodeURIComponent(JSON.stringify(callParams));
+          
+          try {
+            // 优先使用uni-app的导航API(适用于同应用内跳转)
+            if (typeof uni !== 'undefined' && uni.navigateTo) {
+              console.log('使用uni-app导航API跳转到语音通话页面');
+              uni.navigateTo({
+                url: `/subPackages/voiceCallDetail/index?params=${encodedParams}`
+              });
+            } 
+            // 否则使用window.location跳转(适用于Web环境)
+            else if (typeof window !== 'undefined' && window.location) {
+              console.log('使用window.location跳转到语音通话页面');
+              // 注意:实际部署时需要使用正确的URL
+              const voiceCallUrl = `http://localhost:8080/appPages/voiceCallDetail/index.html?params=${encodedParams}`;
+              window.location.href = voiceCallUrl;
+            } else {
+              console.error('无法跳转到语音通话页面:当前环境不支持导航');
+            }
+          } catch (error) {
+            console.error('跳转到语音通话页面失败:', error);
+          }
+        }
+      });
+    } catch (error) {
+      console.error('WebSocket初始化失败:', error);
+    }
+
     uni.reLaunch({
       url: "/pages/work/index",
     });
diff --git a/uniapps/work-app/src/utils/websocket.js b/uniapps/work-app/src/utils/websocket.js
new file mode 100644
index 0000000..9706abd
--- /dev/null
+++ b/uniapps/work-app/src/utils/websocket.js
@@ -0,0 +1,188 @@
+// WebSocket服务管理
+class WebSocketService {
+  constructor() {
+    this.socketTask = null
+    this.connected = false
+    this.url = ''
+    this.userId = ''
+    this.onMessageCallback = null
+    this.onOpenCallback = null
+    this.onCloseCallback = null
+    this.onErrorCallback = null
+    this.pingTimer = null
+  }
+
+  // 初始化WebSocket连接
+  init(userId, url) {
+    if (!url) {
+      console.error('WebSocket初始化失败:缺少url')
+      return
+    }
+
+    this.userId = userId || ''
+    this.url = url + encodeURIComponent(this.userId)
+    this.connect()
+  }
+
+  // 建立WebSocket连接
+  connect() {
+    try {
+      // 关闭现有连接
+      this.close()
+
+      // 使用uni-app的WebSocket API
+      this.socketTask = uni.connectSocket({
+        url: this.url,
+        success: () => {
+          console.log('WebSocket连接已请求')
+        },
+        fail: (error) => {
+          console.error('WebSocket连接请求失败:', error)
+          if (this.onErrorCallback) {
+            this.onErrorCallback(error)
+          }
+        }
+      })
+
+      // 监听连接打开
+      this.socketTask.onOpen(() => {
+        this.connected = true
+        console.log('WebSocket已连接,userId:', this.userId)
+        this.startPing()
+        if (this.onOpenCallback) {
+          this.onOpenCallback()
+        }
+      })
+
+      // 监听连接关闭
+      this.socketTask.onClose(() => {
+        this.connected = false
+        console.log('WebSocket已关闭')
+        this.stopPing()
+        if (this.onCloseCallback) {
+          this.onCloseCallback()
+        }
+      })
+
+      // 监听连接错误
+      this.socketTask.onError((error) => {
+        console.error('WebSocket错误:', error)
+        this.connected = false
+        this.stopPing()
+        if (this.onErrorCallback) {
+          this.onErrorCallback(error)
+        }
+      })
+
+      // 监听消息
+      this.socketTask.onMessage((res) => {
+        try {
+          const message = JSON.parse(res.data)
+          console.log('收到WebSocket消息:', message)
+          if (this.onMessageCallback) {
+            this.onMessageCallback(message)
+          }
+        } catch (error) {
+          console.error('解析WebSocket消息失败:', error)
+        }
+      })
+    } catch (error) {
+      console.error('WebSocket连接失败:', error)
+      if (this.onErrorCallback) {
+        this.onErrorCallback(error)
+      }
+    }
+  }
+
+  // 发送消息
+  send(type, to, payload) {
+    if (!type || !this.socketTask || !this.connected) {
+      console.error('WebSocket发送失败:连接未建立')
+      return
+    }
+
+    const message = {
+      type,
+      from: String(this.userId),
+      to: String(to),
+      payload: payload || null,
+      timestamp: Date.now()
+    }
+
+    this.socketTask.send({
+      data: JSON.stringify(message),
+      success: () => {
+        console.log('WebSocket消息发送成功:', message)
+      },
+      fail: (error) => {
+        console.error('WebSocket消息发送失败:', error)
+      }
+    })
+  }
+
+  // 开始心跳检测
+  startPing() {
+    this.stopPing()
+    // 每25秒发送一次心跳
+    this.pingTimer = setInterval(() => {
+      if (this.connected) {
+        this.send('ping', 'system')
+      }
+    }, 25000)
+  }
+
+  // 停止心跳检测
+  stopPing() {
+    if (this.pingTimer) {
+      clearInterval(this.pingTimer)
+      this.pingTimer = null
+    }
+  }
+
+  // 关闭连接
+  close() {
+    if (this.socketTask) {
+      this.socketTask.close({
+        code: 1000,
+        reason: '正常关闭',
+        success: () => {
+          console.log('WebSocket已关闭')
+        },
+        fail: (error) => {
+          console.error('WebSocket关闭失败:', error)
+        }
+      })
+      this.socketTask = null
+    }
+    this.stopPing()
+    this.connected = false
+  }
+
+  // 设置消息回调
+  setOnMessageCallback(callback) {
+    this.onMessageCallback = callback
+  }
+
+  // 设置连接打开回调
+  setOnOpenCallback(callback) {
+    this.onOpenCallback = callback
+  }
+
+  // 设置连接关闭回调
+  setOnCloseCallback(callback) {
+    this.onCloseCallback = callback
+  }
+
+  // 设置错误回调
+  setOnErrorCallback(callback) {
+    this.onErrorCallback = callback
+  }
+
+  // 获取连接状态
+  getConnected() {
+    return this.connected
+  }
+}
+
+// 导出单例实例
+export default new WebSocketService()

--
Gitblit v1.9.3