Files
uniapp-im-shop/pages/room/incom.vue
2026-02-09 02:10:27 +08:00

528 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import * as CallLib from '@/uni_modules/RongCloud-CallWrapper/lib/index'
import { ref, computed, onUnmounted } from 'vue'
import { onLoad, onUnload, onHide } from '@dcloudio/uni-app'
import { TUIUserService } from '@tencentcloud/chat-uikit-engine-lite'
import { useUI } from '../../utils/use-ui'
import { useAuthUser } from '../../composables/useAuthUser'
import { useUserStore } from '../../stores/user'
const { callUserId, callDuration } = useAuthUser()
const { startTimer, stopTimer } = useUserStore()
const { showLoading, hideLoading } = useUI()
const callBackground = ref('/static/images/public/random2.png')
/** 呼叫用户信息 */
const callerInfo = ref({})
/** 通话状态 */
const isRinging = ref(true)
/** 控制按钮状态 */
const activeBtn = ref(null)
/** 麦克风是否静音 */
const isMicMuted = ref(false)
/** 扬声器是否开启 */
const isSpeakerOn = ref(false)
/** 通话计时器 */
const timer = ref(null)
/** 通话时长(秒) */
// const callDuration = ref(0)
/** 好友ID */
const friendID = ref('')
/**
* 拨打状态
* call: 发起呼叫
* answer: 接听
* dialogue: 对话中
*/
const callState = ref('')
CallLib.onRemoteUserJoined(res => {
console.log(
'Engine:OnRemoteUserJoined=>' +
'主叫端拨出电话被叫端收到请求后加入通话被叫端Id为=>',
res.data.userId
)
callState.value = 'dialogue'
// 开始计时
startTimer()
})
// CallLib.onRemoteUserRinging(res => {
// console.log(
// '主叫端拨出电话被叫端收到请求发出振铃响应时触发对端Id为=>',
// res.data.userId
// )
// })
// CallLib.onError(res => {
// console.log('通话过程中,发生异常,异常原因=>', res.data.reason)
// })
/** 格式化时间显示 */
const formattedTime = computed(() => {
const minutes = Math.floor(callDuration.value / 60)
const seconds = callDuration.value % 60
return `${minutes.toString().padStart(2, '0')}:${seconds
.toString()
.padStart(2, '0')}`
})
/** 切换麦克风状态 */
const toggleMicrophone = () => {
isMicMuted.value = !isMicMuted.value
// 调用融云SDK的麦克风控制
CallLib.enableMicrophone(!isMicMuted.value)
activeBtn.value = 'mic'
setTimeout(() => (activeBtn.value = null), 200)
}
/** 切换扬声器状态 */
const toggleSpeaker = () => {
isSpeakerOn.value = !isSpeakerOn.value
// 调用融云SDK的扬声器控制
CallLib.enableSpeaker(isSpeakerOn.value)
activeBtn.value = 'speaker'
setTimeout(() => (activeBtn.value = null), 200)
}
/**
* 按钮状态
* @param show true: 接听电话 false: 拒接/挂断电话
*/
const cutFn = show => {
if (show) {
// 接听电话 dialogue
if (mediaType.value === 'audio') {
CallLib.enableMicrophone(true)
}
CallLib.accept()
callState.value = 'dialogue'
// 开始计时
startTimer()
if (mediaType.value === 'video') {
uni.redirectTo({
url: '/pages/room/room'
})
}
} else {
// 停止计时
stopTimer()
// 取消接听,返回上一页
CallLib.hangup()
}
}
const mediaType = ref('audio')
onLoad(async e => {
callState.value = e.type
mediaType.value = e.mediaType
friendID.value = e.userID
callUserId.value = e.userID
console.log(e)
showLoading()
const res = await TUIUserService.getUserProfile({
userIDList: [e.userID]
})
hideLoading()
callerInfo.value = res.data[0]
if (e.type === 'call') {
CallLib.enableMicrophone(true)
CallLib.startSingleCall(callerInfo.value.userID, 0, 'answer')
}
if (e.callType == 'in') {
// 接听语音通话
callState.value = 'answer'
}
console.log('callState.value: ', callState.value)
console.log('===好友信息', res)
})
// 组件卸载时清理计时器
onUnload(() => {
stopTimer()
})
onUnmounted(() => {
stopTimer()
})
</script>
<template>
<!-- 来电接听界面 -->
<view class="incoming-call">
<!-- 背景模糊层 -->
<view class="blur-background" v-if="callBackground">
<image
:src="callBackground"
mode="aspectFill"
class="bg-image"
></image>
<view class="bg-overlay"></view>
</view>
<!-- 顶部栏 -->
<view class="top-bar">
<view class="top-left">
<!-- 通话时间显示 -->
<view v-if="callState === 'dialogue'" class="call-time-container">
<uni-icons type="sound" size="16" color="#fff"></uni-icons>
<text class="call-time">{{ formattedTime }}</text>
</view>
</view>
</view>
<!-- 主内容区 -->
<view class="caller-info">
<view class="caller-avatar-container">
<view class="caller-avatar" :class="{ 'avatar-ring': isRinging }">
<image
v-if="callerInfo?.avatar"
:src="callerInfo?.avatar"
class="avatar-image"
></image>
<uni-icons
v-else
type="contact-filled"
size="50"
color="#fff"
></uni-icons>
</view>
</view>
<text class="caller-name">
{{ callerInfo?.nick || callerInfo?.userID }}
</text>
<text class="call-type">
{{
callState === 'call'
? '呼叫中,等待对方接受邀请...'
: callState === 'answer'
? '等待接听...'
: '对话中...'
}}
</text>
<text class="call-status">
{{ mediaType === 'audio' ? '语音通话' : '视频通话' }}
</text>
</view>
<!-- 控制按钮 -->
<view class="incoming-controls">
<!-- 麦克风控制按钮仅在通话中显示 -->
<view
v-if="callState === 'dialogue'"
class="incoming-control"
@click="toggleMicrophone"
>
<view
class="call-btn mic-btn"
:class="{
'btn-active': activeBtn === 'mic',
'mic-muted': isMicMuted
}"
>
<uni-icons
v-if="!isMicMuted"
type="mic"
size="28"
color="#fff"
></uni-icons>
<uni-icons
v-else
type="micoff"
size="28"
color="#fff"
></uni-icons>
</view>
<text class="btn-text">{{ isMicMuted ? '开启' : '静音' }}</text>
</view>
<!-- 挂断/拒绝按钮 -->
<view class="incoming-control" @click="cutFn(false)">
<view
class="call-btn decline-btn"
:class="{ 'btn-active': activeBtn === 'decline' }"
>
<uni-icons type="closeempty" size="32" color="#fff"></uni-icons>
</view>
<text class="btn-text">
{{
callState === 'call' || callState == 'dialogue'
? '挂断'
: '拒绝'
}}
</text>
</view>
<!-- 接听按钮 -->
<view
v-if="callState === 'answer'"
class="incoming-control"
@click="cutFn(true)"
>
<view
class="call-btn accept-btn"
:class="{ 'btn-active': activeBtn === 'accept' }"
>
<uni-icons
type="checkmarkempty"
size="28"
color="#fff"
></uni-icons>
</view>
<text class="btn-text">接听</text>
</view>
<!-- 扬声器控制按钮仅在通话中显示 -->
<view
v-if="callState === 'dialogue'"
class="incoming-control"
@click="toggleSpeaker"
>
<view
class="call-btn speaker-btn"
:class="{
'btn-active': activeBtn === 'speaker',
'speaker-on': isSpeakerOn
}"
>
<uni-icons
v-if="!isSpeakerOn"
type="sound"
size="28"
color="#fff"
></uni-icons>
<uni-icons
v-else
type="sound-filled"
size="28"
color="#fff"
></uni-icons>
</view>
<text class="btn-text">扬声器</text>
</view>
</view>
</view>
</template>
<style lang="scss" scoped>
.incoming-call {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 9999;
display: flex;
flex-direction: column;
justify-content: space-between;
background-color: #000;
/* 顶部栏样式 */
.top-bar {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 120rpx;
display: flex;
align-items: center;
padding: 0 40rpx;
z-index: 2;
box-sizing: border-box;
padding-top: env(safe-area-inset-top);
.top-left {
.call-time-container {
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.15);
padding: 8rpx 20rpx;
border-radius: 30rpx;
backdrop-filter: blur(10px);
.call-time {
font-size: 28rpx;
color: #fff;
font-weight: 500;
margin-left: 10rpx;
}
}
}
}
.blur-background {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
.bg-image {
width: 100%;
height: 100%;
filter: blur(20px);
opacity: 0.6;
}
.bg-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
}
}
.caller-info {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 100rpx;
z-index: 1;
.caller-avatar-container {
position: relative;
margin-bottom: 40rpx;
.caller-avatar {
width: 180rpx;
height: 180rpx;
border-radius: 50%;
background: linear-gradient(135deg, #1aad19, #129611);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
.avatar-image {
width: 100%;
height: 100%;
}
&.avatar-ring {
animation: ringPulse 1.5s infinite;
}
}
}
.caller-name {
font-size: 48rpx;
color: #fff;
font-weight: 500;
margin-bottom: 20rpx;
}
.call-type {
font-size: 32rpx;
color: rgba(255, 255, 255, 0.9);
margin-bottom: 10rpx;
}
.call-status {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.7);
}
}
.incoming-controls {
display: flex;
justify-content: space-around;
padding: 80rpx 40rpx 120rpx;
z-index: 1;
.incoming-control {
display: flex;
flex-direction: column;
align-items: center;
min-width: 120rpx;
.call-btn {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20rpx;
transition: all 0.2s;
&.decline-btn {
background-color: #f43f3b;
&.btn-active {
transform: scale(0.9);
background-color: #d93632;
}
}
&.accept-btn {
background-color: #07c160;
&.btn-active {
transform: scale(0.9);
background-color: #06ad56;
}
}
&.mic-btn {
background-color: rgba(255, 255, 255, 0.2);
&.mic-muted {
background-color: rgba(255, 255, 255, 0.1);
}
&.btn-active {
transform: scale(0.9);
background-color: rgba(255, 255, 255, 0.3);
}
}
&.speaker-btn {
background-color: rgba(255, 255, 255, 0.2);
&.speaker-on {
background-color: rgba(255, 255, 255, 0.3);
}
&.btn-active {
transform: scale(0.9);
background-color: rgba(255, 255, 255, 0.3);
}
}
}
.btn-text {
font-size: 24rpx;
color: #fff;
text-align: center;
}
}
}
}
@keyframes ringPulse {
0% {
box-shadow: 0 0 0 0 rgba(26, 173, 25, 0.4);
}
70% {
box-shadow: 0 0 0 30rpx rgba(26, 173, 25, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(26, 173, 25, 0);
}
}
/* 响应式调整 */
@media (max-height: 600px) {
.incoming-controls {
padding: 40rpx 30rpx 80rpx !important;
}
}
</style>