✨ feat(im): 优化群邀请的 incoming、inviting 的交互
parent
e629ac3825
commit
68922ebf02
|
|
@ -0,0 +1,115 @@
|
||||||
|
<template>
|
||||||
|
<!--
|
||||||
|
被叫端来电小条;参考微信,右上角固定悬浮;
|
||||||
|
群聊:左头像 + 中(邀请人 + 文案一行 / 通话成员行)+ 右下角拒绝 / 接听;
|
||||||
|
私聊:左头像 + 中(邀请人 + 文案两行)+ 右下角拒绝 / 接听
|
||||||
|
-->
|
||||||
|
<div
|
||||||
|
class="fixed top-5 right-5 rounded-2xl overflow-hidden shadow-[0_12px_36px_rgba(0,0,0,0.4)] z-[9999] flex gap-3 items-end p-3 text-white bg-[#2a2a2c]"
|
||||||
|
:class="isGroup ? 'w-[360px]' : 'w-[340px]'"
|
||||||
|
>
|
||||||
|
<!-- 左:邀请者头像 -->
|
||||||
|
<UserAvatar
|
||||||
|
:url="payload?.inviterAvatar"
|
||||||
|
:name="payload?.inviterNickname"
|
||||||
|
:size="48"
|
||||||
|
radius="8px"
|
||||||
|
:clickable="false"
|
||||||
|
class="self-start"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 中:群聊单行(邀请人 + 文案)+ 通话成员;私聊两行 -->
|
||||||
|
<div class="flex flex-col flex-1 gap-1 self-start min-w-0">
|
||||||
|
<template v-if="isGroup">
|
||||||
|
<div class="text-sm truncate">
|
||||||
|
<span class="font-medium">{{ payload?.inviterNickname || '对方' }}</span>
|
||||||
|
<span class="ml-1 text-white/60">{{ tipText }}</span>
|
||||||
|
</div>
|
||||||
|
<template v-if="callMembers.length > 0">
|
||||||
|
<div class="mt-1 text-xs text-white/45">通话成员</div>
|
||||||
|
<div class="flex flex-wrap gap-1">
|
||||||
|
<UserAvatar
|
||||||
|
v-for="m in callMembers"
|
||||||
|
:key="m.userId"
|
||||||
|
:url="m.avatar"
|
||||||
|
:name="m.nickname"
|
||||||
|
:size="22"
|
||||||
|
radius="4px"
|
||||||
|
:clickable="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="text-sm font-medium truncate">{{ payload?.inviterNickname || '对方' }}</div>
|
||||||
|
<div class="text-13px text-white/60 truncate">{{ tipText }}</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右下角:拒绝 / 接听 -->
|
||||||
|
<div class="flex flex-shrink-0 gap-2 items-center">
|
||||||
|
<button
|
||||||
|
class="flex flex-shrink-0 justify-center items-center w-10 h-10 text-white rounded-full transition-opacity bg-[#f04a4a] hover:opacity-90"
|
||||||
|
@click="$emit('reject')"
|
||||||
|
>
|
||||||
|
<Icon icon="ant-design:phone-outlined" :size="18" class="rotate-[135deg]" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="flex flex-shrink-0 justify-center items-center w-10 h-10 text-white rounded-full transition-opacity bg-[#2ec27e] hover:opacity-90"
|
||||||
|
@click="$emit('accept')"
|
||||||
|
>
|
||||||
|
<Icon icon="ant-design:phone-outlined" :size="18" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import Icon from '@/components/Icon/src/Icon.vue'
|
||||||
|
import UserAvatar from '../user/UserAvatar.vue'
|
||||||
|
import { useRtcStore } from '../../store/rtcStore'
|
||||||
|
import type { ImRtcCallNotification } from '../../store/rtcStore'
|
||||||
|
import { DICT_TYPE, getDictLabel } from '@/utils/dict'
|
||||||
|
import { ImConversationType } from '@/views/im/utils/constants'
|
||||||
|
import { getCurrentUserId } from '@/views/im/utils/storage'
|
||||||
|
import { getSenderAvatar, getSenderDisplayName } from '@/views/im/utils/user'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
payload: ImRtcCallNotification | null
|
||||||
|
isGroup?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
defineEmits<{ accept: []; reject: [] }>()
|
||||||
|
|
||||||
|
const rtcStore = useRtcStore()
|
||||||
|
|
||||||
|
/** 来电提示文案;区分语音 / 视频 */
|
||||||
|
const tipText = computed(() => {
|
||||||
|
if (!props.payload) return ''
|
||||||
|
return `邀请你${getDictLabel(DICT_TYPE.IM_RTC_CALL_MEDIA_TYPE, props.payload.mediaType)}通话`
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 群聊:已加入通话的成员(自己除外);缓存里 joinedUserIds 为空时降级到主叫,保证至少一头像 */
|
||||||
|
const callMembers = computed(() => {
|
||||||
|
if (!props.isGroup) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const groupId = props.payload?.groupId
|
||||||
|
if (!groupId) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const myId = getCurrentUserId()
|
||||||
|
const joined = rtcStore.getGroupCall(groupId)?.joinedUserIds ?? []
|
||||||
|
const ids = joined.length > 0
|
||||||
|
? joined.filter((id) => id !== myId)
|
||||||
|
: props.payload?.inviterUserId
|
||||||
|
? [props.payload.inviterUserId]
|
||||||
|
: []
|
||||||
|
return ids.map((userId) => ({
|
||||||
|
userId,
|
||||||
|
nickname: getSenderDisplayName(userId, ImConversationType.GROUP, groupId),
|
||||||
|
avatar: getSenderAvatar(userId, ImConversationType.GROUP, groupId) || undefined
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
<template>
|
||||||
|
<!-- 主叫等待对方接听的悬浮窗;1v1 私聊 320×540;群通话切大窗 720×560 -->
|
||||||
|
<div
|
||||||
|
class="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-xl overflow-hidden shadow-[0_12px_36px_rgba(0,0,0,0.35)] z-[9999] flex flex-col text-white bg-gradient-to-b from-[#2a2a2c] to-[#1a1a1c]"
|
||||||
|
:class="isGroup ? 'w-[720px] h-[560px]' : 'w-[320px] h-[540px]'"
|
||||||
|
>
|
||||||
|
<div class="flex relative flex-1 justify-center items-center">
|
||||||
|
<!-- 视频呼叫:自己摄像头预览铺底,对方头像悬浮顶部 -->
|
||||||
|
<video
|
||||||
|
v-if="isVideo && localStream"
|
||||||
|
ref="localVideoRef"
|
||||||
|
class="absolute inset-0 object-cover w-full h-full scale-x-[-1]"
|
||||||
|
autoplay
|
||||||
|
muted
|
||||||
|
playsinline
|
||||||
|
></video>
|
||||||
|
<div
|
||||||
|
class="flex relative z-[1] flex-col gap-4 items-center"
|
||||||
|
:class="{ 'self-start mt-16': isVideo }"
|
||||||
|
>
|
||||||
|
<UserAvatar
|
||||||
|
:url="peerAvatar"
|
||||||
|
:name="peerNickname"
|
||||||
|
:size="96"
|
||||||
|
radius="8px"
|
||||||
|
:clickable="false"
|
||||||
|
/>
|
||||||
|
<div class="text-[17px] font-medium">{{ peerNickname || '对方' }}</div>
|
||||||
|
<div class="text-13px text-white/60">等待对方接受邀请...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 底部操作区:麦克风 / 取消 / (摄像头 | 扬声器) -->
|
||||||
|
<div class="flex flex-shrink-0 gap-4 justify-around items-center pt-4 px-5 pb-5">
|
||||||
|
<div
|
||||||
|
class="flex flex-col gap-2 items-center cursor-pointer select-none"
|
||||||
|
@click="$emit('toggle-mic')"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="flex justify-center items-center w-12 h-12 bg-white rounded-full text-[#1a1a1c]"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
:icon="micEnabled ? 'ant-design:audio-outlined' : 'ant-design:audio-muted-outlined'"
|
||||||
|
:size="22"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-white/70 whitespace-nowrap">
|
||||||
|
{{ micEnabled ? '麦克风已开' : '麦克风已关' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex flex-col gap-2 items-center cursor-pointer select-none"
|
||||||
|
@click="$emit('cancel')"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="flex justify-center items-center w-12 h-12 text-white rounded-full bg-[#f04a4a]"
|
||||||
|
>
|
||||||
|
<Icon icon="ant-design:phone-outlined" :size="22" class="rotate-[135deg]" />
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-white/70 whitespace-nowrap">取消</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="isVideo"
|
||||||
|
class="flex flex-col gap-2 items-center cursor-pointer select-none"
|
||||||
|
@click="$emit('toggle-camera')"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="flex justify-center items-center w-12 h-12 bg-white rounded-full text-[#1a1a1c]"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
:icon="
|
||||||
|
cameraEnabled
|
||||||
|
? 'ant-design:video-camera-outlined'
|
||||||
|
: 'ant-design:video-camera-add-outlined'
|
||||||
|
"
|
||||||
|
:size="22"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-white/70 whitespace-nowrap">
|
||||||
|
{{ cameraEnabled ? '摄像头已开' : '摄像头已关' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="flex flex-col gap-2 items-center cursor-pointer select-none"
|
||||||
|
@click="$emit('toggle-speaker')"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="flex justify-center items-center w-12 h-12 bg-white rounded-full text-[#1a1a1c]"
|
||||||
|
>
|
||||||
|
<Icon icon="ant-design:sound-outlined" :size="22" />
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-white/70 whitespace-nowrap">扬声器已开</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import Icon from '@/components/Icon/src/Icon.vue'
|
||||||
|
import UserAvatar from '../user/UserAvatar.vue'
|
||||||
|
import { useMediaStreamElement } from '../../composables/useMediaStreamElement'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
peerNickname?: string
|
||||||
|
peerAvatar?: string
|
||||||
|
isGroup?: boolean
|
||||||
|
isVideo: boolean
|
||||||
|
micEnabled: boolean
|
||||||
|
cameraEnabled: boolean
|
||||||
|
localStream?: MediaStream | null // 本地视频流;视频呼叫预览铺底
|
||||||
|
}>()
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
cancel: []
|
||||||
|
'toggle-mic': []
|
||||||
|
'toggle-camera': []
|
||||||
|
'toggle-speaker': []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const localVideoRef = useMediaStreamElement<HTMLVideoElement>(() => props.localStream)
|
||||||
|
</script>
|
||||||
|
|
@ -27,6 +27,8 @@ export interface ImRtcCallNotification {
|
||||||
inviterUserId?: number
|
inviterUserId?: number
|
||||||
inviterNickname?: string
|
inviterNickname?: string
|
||||||
inviterAvatar?: string
|
inviterAvatar?: string
|
||||||
|
// INVITE 专属:本次被邀请人列表;包含收件人自身,前端来电小条按需过滤展示「邀请的其他人」
|
||||||
|
inviteeIds?: number[]
|
||||||
// REJECT 专属:操作者展示信息(其它子类型走 RTC_CALL_END)
|
// REJECT 专属:操作者展示信息(其它子类型走 RTC_CALL_END)
|
||||||
operatorUserId?: number
|
operatorUserId?: number
|
||||||
operatorNickname?: string
|
operatorNickname?: string
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue