From 04a859aa95b231c032c71d06518497a973c2f4e8 Mon Sep 17 00:00:00 2001
From: 罗广辉 <guanghui.luo@foxmail.com>
Date: Thu, 15 Jan 2026 17:41:07 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/master'
---
applications/mobile-web-view/src/appPages/voiceCallDetail/index.vue | 961 ++++++++++++++++++++++++++++++++++++-------------------
1 files changed, 624 insertions(+), 337 deletions(-)
diff --git a/applications/mobile-web-view/src/appPages/voiceCallDetail/index.vue b/applications/mobile-web-view/src/appPages/voiceCallDetail/index.vue
index da7fd12..ab07b87 100644
--- a/applications/mobile-web-view/src/appPages/voiceCallDetail/index.vue
+++ b/applications/mobile-web-view/src/appPages/voiceCallDetail/index.vue
@@ -1,52 +1,55 @@
<template>
- <div class="call-container">
- <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 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="input-item">
- <span class="input-label">对方 userId:</span>
- <input v-model="peerUid" class="input-field" />
- </div>
- </div>
-
- <div class="button-group">
- <button @click="connectWS" :disabled="connected" class="btn btn-connect">连接WS</button>
- </div>
- <!-- 头像-->
+ <!-- 头像-->
<div class="contactAvatar">
<img :src="defaultAvatar" alt=""></img>
</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">接听</button>
- <button v-if="state==='ringing'" @click="rejectCall" class="btn btn-reject">拒绝</button>
-
- <button @click="hangup" :disabled="!connected || (state!=='calling' && state!=='in-call')" class="btn btn-hangup">挂断</button>
+ <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="state === 'idle' && !receiveParameters" @click="requestCall" :disabled="!connected" 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="status-group">
- <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>
-
- <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="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>
@@ -57,25 +60,69 @@
/** ✅ 你的 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 = () => {
+ // 1. 优先从route.query.voiceparams获取参数
+ if (route.query && route.query.voiceparams) {
+ console.log('从route.query.voiceparams获取参数:', route.query.voiceparams);
+ try {
+ // 解码并解析voiceparams参数
+ const params = JSON.parse(decodeURIComponent(route.query.voiceparams));
+ console.log('解析后的voiceparams:', params);
+
+ if (params.peerUid) {
+ peerUid.value = String(params.peerUid);
+ log('🔗 从voiceparams获取peerUid:', params.peerUid);
+ }
+
+ // 如果是来电类型,直接触发来电状态
+ if (params.type === 'incoming') {
+ log('📞 voiceparams检测到来电,直接进入来电状态');
+ incomingFrom = String(params.from);
+ peerUid.value = incomingFrom;
+ acceptedByMe = false;
+ offeredByPeer = false;
+
+ state.value = 'ringing';
+ log('📞 收到来电 call,来自', incomingFrom);
+
+ // 开始来电提示(铃声和震动)
+ playRingtone();
+ startVibration();
+ }
+ } catch (e) {
+ log('❌ 解析voiceparams参数失败:', String(e));
+ }
+ }
+
+ // 2. 同时保留原有的URL参数解析逻辑,作为备选
+ 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
@@ -112,59 +159,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)
- }
- function stopPing() {
+}
+
+// 来电提示相关
+let ringtoneAudio = null
+let vibrationTimer = null
+
+// 播放来电铃声
+function playRingtone() {
+ // 创建音频对象(使用默认的系统提示音)
+ ringtoneAudio = new Audio('data:audio/wav;base64,UklGRigAAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQQAAAAAAA==');
+ ringtoneAudio.loop = true;
+ ringtoneAudio.volume = 0.5;
+
+ try {
+ ringtoneAudio.play();
+ log('🔊 开始播放来电铃声')
+ } catch (e) {
+ log('❌ 播放来电铃声失败:', String(e))
+ }
+}
+
+// 停止来电铃声
+function stopRingtone() {
+ if (ringtoneAudio) {
+ try {
+ ringtoneAudio.pause();
+ ringtoneAudio.currentTime = 0;
+ ringtoneAudio = null;
+ log('🔇 停止播放来电铃声')
+ } catch (e) {
+ log('❌ 停止来电铃声失败:', String(e))
+ }
+ }
+}
+
+// 开始震动
+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('ℹ️ 当前浏览器不支持震动功能')
+ }
+}
+
+// 停止震动
+function stopVibration() {
+ if (vibrationTimer) {
+ clearInterval(vibrationTimer);
+ vibrationTimer = null;
+ }
+
+ if ('vibrate' in navigator) {
+ try {
+ navigator.vibrate(0); // 停止震动
+ log('📳 停止震动提示')
+ } catch (e) {
+ log('❌ 停止震动失败:', String(e))
+ }
+ }
+}
+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' }],
})
@@ -244,6 +362,8 @@
/* ---------------- 信令处理 ---------------- */
async function onSignal(msg) {
+console.log('1cesss',msg);
+
if (!msg) return
const t = (msg.type || '').toString()
@@ -269,6 +389,11 @@
state.value = 'ringing'
log('📞 收到来电 call,来自', incomingFrom)
+
+ // 开始来电提示(铃声和震动)
+ playRingtone()
+ startVibration()
+
return
}
@@ -284,356 +409,518 @@
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
-
+
+ // 停止来电提示
+ stopRingtone()
+ stopVibration()
+
// 先告诉对方我接听了(后端会把双方置忙)
send('accept', incomingFrom, null)
log('✅ 已点击接听,发送 accept 给', incomingFrom)
-
+
state.value = 'calling'
-
+
// 如果 offer 已经提前到达(我们暂存了),立刻 answer
if (lastOfferSdp) {
- answerOffer(lastOfferSdp, incomingFrom)
- lastOfferSdp = null
+ answerOffer(lastOfferSdp, incomingFrom)
+ lastOfferSdp = null
}
- }
-
- function rejectCall() {
+}
+
+function rejectCall() {
if (!incomingFrom) return
-
+
+ // 停止来电提示
+ stopRingtone()
+ stopVibration()
+
// 你后端没有 reject,用 busy 表示拒绝/不可接听
send('busy', incomingFrom, null)
log('❌ 已拒绝来电,发送 busy 给', incomingFrom)
-
+
incomingFrom = null
acceptedByMe = false
offeredByPeer = false
lastOfferSdp = null
state.value = 'idle'
- }
-
- async function startOffer() {
+
+ // 返回上一页(uni-app的语音通话列表页面)
+ if (window.uni) {
+ window.uni.navigateBack()
+ } else {
+ log('❌ uni对象不存在,无法返回上一页')
+ }
+}
+
+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()
+ } else {
+ log('❌ uni对象不存在,无法返回上一页')
+ }
}
+// 定义receiveParameters变量,用于模板中判断是否显示呼叫按钮
+const receiveParameters = ref(route.query.contact || null);
+
onMounted(() => {
- const receiveParameters = route.query.contact
- console.log('接收参数', receiveParameters)
+ console.log('收拾收拾',route.query.voiceparams);
+ console.log('接收参数', receiveParameters.value)
+
+ // 解析并使用contact参数
+ if (receiveParameters.value) {
+ try {
+ const contact = JSON.parse(decodeURIComponent(receiveParameters.value))
+ if (contact.friendId) {
+ peerUid.value = contact.friendId
+ log('🔗 从contact参数获取peerUid:', contact.friendId)
+ log('👤 联系人名称:', contact.friendNickName || '未知联系人')
+ }
+ } catch (e) {
+ log('❌ 解析contact参数失败:', String(e))
+ }
+ }
+
// 解析URL参数
parseUrlParams()
+ // 自动连接WebSocket
+ connectWS()
+});
+
+// 监听连接状态,当connected变为true且有receiveParameters时自动呼叫
+watch(() => connected.value, (newVal) => {
+ if (newVal && state.value === 'idle' && receiveParameters.value) {
+ log('📞 连接成功且有contact参数,自动发起呼叫');
+ 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;
- }
+/* 来电时的全屏样式 */
+.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: 15px;
- max-width: 600px;
- margin: 0 auto;
- }
+.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-group {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
- .input-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
+.input-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
- .input-label {
- font-weight: bold;
- color: #333;
- }
+.input-label {
+ font-weight: bold;
+ color: #333;
+}
- .input-field {
- width: 120px;
- padding: 8px;
- border: 1px solid #ddd;
- border-radius: 4px;
- font-size: 14px;
- }
+.input-field {
+ width: 120px;
+ padding: 8px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ font-size: 14px;
+}
- .button-group {
- display: flex;
- gap: 10px;
- flex-wrap: wrap;
- justify-content: center;
- }
+.button-group {
+ display: flex;
+ gap: 50px;
+ flex-wrap: wrap;
+ justify-content: center;
+ margin-top: 80px;
+}
- .btn {
- padding: 10px 20px;
- color: white;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- font-size: 14px;
- transition: background-color 0.3s;
+.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;
- }
+ &:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
}
+}
- .btn-connect {
- background-color: #4CAF50;
- }
+/* 来电时的接听和拒绝按钮样式 */
+.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-call {
- background-color: #2196F3;
- }
+.btn.incoming-accept {
+ background-color: #4CAF50;
+ transform: scale(1.1);
+}
- .btn-accept {
- background-color: #4CAF50;
- }
+.btn.incoming-reject {
+ background-color: #f44336;
+}
- .btn-reject {
- background-color: #f44336;
- }
+.btn.incoming-accept:active {
+ transform: scale(0.95);
+}
- .btn-hangup {
- background-color: #9E9E9E;
- }
+.btn.incoming-reject:active {
+ transform: scale(0.95);
+}
- .status-group {
- display: flex;
- flex-direction: column;
- gap: 5px;
- }
+.btn-connect {
+ background-color: #4CAF50;
+}
- .status-text {
- font-size: 16px;
- color: #333;
- }
+.btn-call {
+ background-color: #2196F3;
+}
- .tip-message {
- margin-top: 15px;
- color: #666;
- text-align: center;
- }
+.btn-accept {
+ background-color: #4CAF50;
+}
- .log-container {
- margin-top: 20px;
- max-width: 600px;
- margin-left: auto;
- margin-right: auto;
- }
+.btn-reject {
+ background-color: #f44336;
+}
- .log-box {
- background-color: white;
- border: 1px solid #ddd;
- border-radius: 8px;
- padding: 10px;
- }
+.btn-hangup {
+ background-color: #9E9E9E;
+ padding: 15px 40px;
+ min-width: 150px;
+}
- .log-title {
- font-weight: bold;
- color: #333;
- margin-bottom: 8px;
- }
+.status-group {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ align-items: center;
+}
- .log-content {
- max-height: 200px;
- overflow: auto;
- background-color: #fafafa;
- padding: 10px;
- border-radius: 4px;
- font-size: 12px;
- line-height: 1.4;
- }
+.status-text {
+ font-size: 18px;
+ color: #333;
+ text-align: center;
+ margin-bottom: 20px;
+}
- .hidden-audio {
- display: none;
+/* 来电状态样式 */
+.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);
}
- .contactAvatar {
- width: 172rpx;
- height: 172rpx;
- display: flex;
- justify-content: center;
- align-items: center;
- img{
- width: 172rpx;
- height: 172rpx;
- }
+ 50% {
+ opacity: 0.7;
+ transform: scale(1.05);
}
- </style>
\ No newline at end of file
+ 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
--
Gitblit v1.9.3