| | |
| | | <template> |
| | | <div class="call-container"> |
| | | <div class="call-container" :class="{'incoming-call': state === 'ringing'} "> |
| | | <div class="content-wrapper"> |
| | | <!-- <div class="input-group">--> |
| | | <!-- <div class="input-item">--> |
| | |
| | | <img :src="defaultAvatar" alt=""></img> |
| | | </div> |
| | | <div class="status-group"> |
| | | <!-- <strong class="status-text">状态:<span :style="{color: getStateColor(state)}">{{ state }}</span></strong>--> |
| | | <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">接听</button> |
| | | <button v-if="state==='ringing'" @click="rejectCall" class="btn btn-reject">拒绝</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> |
| | |
| | | pingTimer = setInterval(() => { |
| | | if (connected.value) send('ping', 'system', null) |
| | | }, 25000) |
| | | } |
| | | |
| | | // 来电提示相关 |
| | | 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) |
| | |
| | | |
| | | state.value = 'ringing' |
| | | log('📞 收到来电 call,来自', incomingFrom) |
| | | |
| | | // 开始来电提示(铃声和震动) |
| | | playRingtone() |
| | | startVibration() |
| | | |
| | | return |
| | | } |
| | | |
| | |
| | | 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) |
| | |
| | | |
| | | function rejectCall() { |
| | | if (!incomingFrom) return |
| | | |
| | | |
| | | // 停止来电提示 |
| | | stopRingtone() |
| | | stopVibration() |
| | | |
| | | // 你后端没有 reject,用 busy 表示拒绝/不可接听 |
| | | send('busy', incomingFrom, null) |
| | | log('❌ 已拒绝来电,发送 busy 给', incomingFrom) |
| | |
| | | } catch {} |
| | | |
| | | stopTimer() |
| | | |
| | | // 停止来电提示 |
| | | stopRingtone() |
| | | stopVibration() |
| | | |
| | | if (pc) { |
| | | pc.getSenders().forEach(s => s.track && s.track.stop()) |
| | | pc.close() |
| | |
| | | </script> |
| | | <style scoped lang="scss"> |
| | | .call-container { |
| | | padding:100px 20px 20px 20px; |
| | | 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: 15px; |
| | | gap: 20px; |
| | | max-width: 600px; |
| | | margin: 0 auto; |
| | | height: 100vh; |
| | | justify-content: center; |
| | | align-items: center; |
| | | } |
| | | |
| | | .input-group { |
| | |
| | | |
| | | .button-group { |
| | | display: flex; |
| | | gap: 10px; |
| | | gap: 50px; |
| | | flex-wrap: wrap; |
| | | justify-content: center; |
| | | margin-top: 80px; |
| | | } |
| | | |
| | | .btn { |
| | | padding: 10px 20px; |
| | | padding: 15px 30px; |
| | | color: white; |
| | | border: none; |
| | | border-radius: 4px; |
| | | border-radius: 8px; |
| | | cursor: pointer; |
| | | font-size: 14px; |
| | | transition: background-color 0.3s; |
| | | 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 { |
| | |
| | | |
| | | .btn-hangup { |
| | | background-color: #9E9E9E; |
| | | padding: 15px 40px; |
| | | min-width: 150px; |
| | | } |
| | | |
| | | .status-group { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 5px; |
| | | gap: 10px; |
| | | align-items: center; |
| | | } |
| | | |
| | | .status-text { |
| | | font-size: 16px; |
| | | font-size: 18px; |
| | | color: #333; |
| | | text-align: center; |
| | | margin-bottom: 50px; |
| | | 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 { |
| | |
| | | .hidden-audio { |
| | | display: none; |
| | | } |
| | | |
| | | /* 头像样式 */ |
| | | .contactAvatar { |
| | | width: 172rpx; |
| | | height: 172rpx; |
| | | width: 180px; |
| | | height: 180px; |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | margin-bottom: 200px; |
| | | img{ |
| | | width: 172rpx; |
| | | height: 172rpx; |
| | | 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> |