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