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