| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import { computed, ref, watch } from 'vue' |
| | | |
| | | import { DICT_TYPE } from '#/packages/constants/src' |
| | | import { getDictLabel } from '#/packages/effects/hooks/src' |
| | | import { IconifyIcon as Icon } from '#/packages/icons/src' |
| | | |
| | | import { message, Popover } from 'ant-design-vue' |
| | | |
| | | import { getActiveCall, joinCall } from '#/api/im/rtc' |
| | | import { getCurrentUserId } from '#/views/im/utils/auth' |
| | | |
| | | import { useGroupCallMembers } from '../../composables/useGroupCallMembers' |
| | | import { useGroupStore } from '../../store/groupStore' |
| | | import { useRtcStore } from '../../store/rtcStore' |
| | | import { UserAvatar } from '../user' |
| | | |
| | | defineOptions({ name: 'ImRtcGroupCallBanner' }) |
| | | |
| | | const props = defineProps<{ |
| | | groupId: number |
| | | }>() |
| | | |
| | | const rtcStore = useRtcStore() |
| | | const groupStore = useGroupStore() |
| | | |
| | | const popoverVisible = ref(false) |
| | | |
| | | /** å½åç¾¤çæ´»è·éè¯ï¼rtcStore ç»´æ¤ï¼åä¸è
å å
¥ / 离å¼éç¥å¢å joinedUserIdsï¼éè¯ç»æç§»é¤ */ |
| | | const activeCall = computed(() => rtcStore.getGroupCall(props.groupId)) |
| | | |
| | | /** è¶åæ¡ææ¡ï¼ææåï¼å·²å å
¥ + æ¥å
¥ä¸ï¼å带人æ°ï¼åå§ 0 人æ¶åªæ¾ç¤ºåªä½ç±»å */ |
| | | const pillText = computed(() => { |
| | | const media = getDictLabel(DICT_TYPE.IM_RTC_CALL_MEDIA_TYPE, activeCall.value?.mediaType) |
| | | const count = memberList.value.length |
| | | return count > 0 ? `æ£å¨${media}éè¯ï¼${count} 人ï¼` : `æ£å¨${media}éè¯` |
| | | }) |
| | | |
| | | /** |
| | | * åå°ç¾¤ / éè¯ room ååæ¶æä¸æ¬¡ææ°åä¸è
åè¡¨ï¼ |
| | | * 两个触ååºæ¯ï¼1ï¼ç¨æ·åç¾¤ï¼æ¬ç«¯å¯è½æ²¡æè¯¥ç¾¤éè¯çææ°ç¼åï¼2ï¼åä¸è
éç¥é¦æ¬¡å¡«å
ååªå«æ¬æ¬¡å å
¥è
ï¼ç¼ºåå²å å
¥è
ï¼ |
| | | * ç¨ [groupId, room] åæºçå¬ + 已填å
å®å«ï¼é¿å
å群 / 馿¬¡å¡«å
触åç忬¡é夿å |
| | | */ |
| | | watch( |
| | | () => |
| | | [ |
| | | props.groupId, |
| | | activeCall.value?.room, |
| | | groupStore.isGroupActiveCallExpired(props.groupId) |
| | | ] as const, |
| | | async ([groupId, room], oldValues) => { |
| | | if (!groupId) { |
| | | return |
| | | } |
| | | |
| | | if (!activeCall.value) { |
| | | if (!groupStore.isGroupActiveCallExpired(groupId)) { |
| | | return |
| | | } |
| | | try { |
| | | const data = await getActiveCall(groupId) |
| | | if (data) { |
| | | rtcStore.setGroupCall(data, true) |
| | | } else { |
| | | rtcStore.removeGroupCall(groupId) |
| | | } |
| | | } catch (error) { |
| | | console.warn('[GroupCallBanner] getActiveCall 失败', { groupId }, error) |
| | | } |
| | | return |
| | | } |
| | | |
| | | // å³çæ¯å¦éè¦æåï¼è¡¥é½æ¬å°å·²æéè¯ï¼æ²¡ææ¬å°éè¯æ¶æç¾¤ç¼åè¿æç¶æææ¢æµä¸æ¬¡ |
| | | const groupChanged = !oldValues || oldValues[0] !== groupId |
| | | const roomChanged = oldValues && oldValues[1] !== room |
| | | const participantsLoaded = (activeCall.value?.joinedUserIds?.length ?? 0) > 1 |
| | | const activeCallExpired = groupStore.isGroupActiveCallExpired(groupId) |
| | | if ( |
| | | !activeCallExpired && |
| | | (rtcStore.isGroupCallParticipantsLoaded(groupId, room) || |
| | | (!groupChanged && !roomChanged && participantsLoaded)) |
| | | ) { |
| | | return |
| | | } |
| | | |
| | | // æææ°åä¸è
åå storeï¼æ¥å£è¿å空 â è¯¥ç¾¤å·²æ æ´»è·éè¯ï¼ç§»é¤æ¬å°ç¼å |
| | | try { |
| | | const data = await getActiveCall(groupId) |
| | | if (data) { |
| | | rtcStore.setGroupCall(data, true) |
| | | } else { |
| | | rtcStore.removeGroupCall(groupId) |
| | | } |
| | | } catch (error) { |
| | | console.warn('[GroupCallBanner] getActiveCall 失败', { groupId }, error) |
| | | } |
| | | }, |
| | | { immediate: true } |
| | | ) |
| | | |
| | | /** å¨éè¯ä¸çæåï¼å·²å å
¥ï¼+ æ¥å
¥ä¸çæåï¼å·²éè¯·æªæ¥éï¼ */ |
| | | const memberList = useGroupCallMembers(computed(() => props.groupId)) |
| | | |
| | | /** æ¬ç«¯æ¯å¦æ£å¨è¯¥æ¿é´éè¯ï¼å¤äº INVITING / RUNNINGï¼ */ |
| | | const isInThisCall = computed( |
| | | () => rtcStore.isActive && rtcStore.call?.room === activeCall.value?.room |
| | | ) |
| | | |
| | | /** |
| | | * æå¡ç«¯æ¯å¦è®°å½æå·²å å
¥ï¼å·æ°å LiveKit è¿æ¥å·²æä½ webhook è¿æ²¡æ status æ 为 LEFT æ¶ä»ä¸º trueï¼ |
| | | * ç¨äºææé®ææ¡åå°ãéæ°å å
¥ãï¼ä½ä¸ disable æé® |
| | | */ |
| | | const serverSaysJoined = computed(() => { |
| | | const myId = getCurrentUserId() |
| | | return activeCall.value?.joinedUserIds?.includes(myId) ?? false |
| | | }) |
| | | |
| | | /** å å
¥æé®ç¦ç¨ï¼ä»
卿¬ç«¯å®é
ææ LiveKit è¿æ¥æ¶ç¦ç¨ */ |
| | | const joinDisabled = computed(() => isInThisCall.value) |
| | | |
| | | /** å å
¥æé®ææ¡ï¼æ¬ç«¯è¿ç â å·²å¨éè¯ä¸ï¼æå¡ç«¯è¿æ®çæä½æ¬ç«¯æäº â éæ°å å
¥ï¼å
¶å® â å å
¥ */ |
| | | const joinLabel = computed(() => { |
| | | if (isInThisCall.value) return 'å·²å¨éè¯ä¸' |
| | | if (serverSaysJoined.value) return 'éæ°å å
¥' |
| | | return 'å å
¥' |
| | | }) |
| | | |
| | | /** 主å¨å å
¥ï¼è° invite å½ä¸å·²æ call æ¿ tokenï¼rtcStore æ status èªå¨è¿ RUNNING */ |
| | | async function handleJoin() { |
| | | const call = activeCall.value |
| | | if (!call || joinDisabled.value) { |
| | | return |
| | | } |
| | | if (rtcStore.isActive) { |
| | | message.warning('æ¨æ£å¨éè¯ä¸') |
| | | return |
| | | } |
| | | popoverVisible.value = false |
| | | const data = await joinCall(call.room) |
| | | rtcStore.startInviting(data) |
| | | } |
| | | </script> |
| | | |
| | | <template> |
| | | <!-- ä»
å½è¯¥ç¾¤ææ´»è·éè¯æ¶æ¾ç¤ºï¼ç¹å»è¶åæ¡å±å¼ popover çå¨éè¯æå + å å
¥ --> |
| | | <div v-if="activeCall" class="flex-shrink-0 px-4 pb-2 bg-[var(--ant-color-fill-secondary)]"> |
| | | <Popover |
| | | v-model:open="popoverVisible" |
| | | placement="bottomLeft" |
| | | :overlay-style="{ width: '280px' }" |
| | | trigger="click" |
| | | > |
| | | <!-- è¶åæ¡æ¬ä½ï¼çµè¯å¾æ + ææ¡ï¼å«äººæ°ï¼+ å³ç®å¤´ --> |
| | | <div |
| | | class="inline-flex gap-2 items-center px-2.5 py-1 text-13px rounded-full cursor-pointer select-none transition-colors duration-150 bg-[var(--ant-color-success-bg)] text-[var(--ant-color-text)] hover:bg-[var(--ant-color-success-bg-hover)]" |
| | | > |
| | | <span |
| | | class="inline-flex flex-shrink-0 justify-center items-center w-[18px] h-[18px] text-white rounded-full bg-[#07c160]" |
| | | > |
| | | <Icon icon="ant-design:phone-filled" :size="14" /> |
| | | </span> |
| | | <span class="font-medium">{{ pillText }}</span> |
| | | <Icon |
| | | icon="ant-design:right-outlined" |
| | | :size="12" |
| | | class="text-[var(--ant-color-text-secondary)]" |
| | | /> |
| | | </div> |
| | | |
| | | <!-- å±å¼é¢æ¿ï¼éè¯æåï¼å«æ¥å
¥ä¸ï¼å¤´å横æ + å å
¥æé® --> |
| | | <template #content> |
| | | <div class="flex flex-col gap-4 items-center pt-2 pb-1"> |
| | | <div class="flex flex-wrap gap-1.5 justify-center max-w-[240px]"> |
| | | <UserAvatar |
| | | v-for="member in memberList" |
| | | :key="member.userId" |
| | | :url="member.avatar" |
| | | :name="member.nickname" |
| | | :size="40" |
| | | radius="6px" |
| | | :clickable="false" |
| | | :class="{ 'opacity-50': member.pending }" |
| | | :title="member.pending ? `${member.nickname}ï¼æ¥å
¥ä¸ï¼` : member.nickname" |
| | | /> |
| | | <!-- 馿¬¡å¡«å
æ¶æ¿å
å¯è½ææ¶ 0 人ï¼å å
¥åç± ParticipantConnected äºä»¶è¿½å --> |
| | | <div |
| | | v-if="memberList.length === 0" |
| | | class="p-3 text-13px text-[var(--ant-color-text-secondary)]" |
| | | > |
| | | ææ æåå¨éè¯ |
| | | </div> |
| | | </div> |
| | | <!-- æ¬ç«¯å¨éè¯å
æ¶ç½®ç°ãå·²å¨éè¯ä¸ãï¼æå¡ç«¯æ®çæä½æ¬ç«¯è¿æ¥æäºæ¾ç¤ºãéæ°å å
¥ãï¼å·æ°é¡µé¢ååºæ¯ï¼ --> |
| | | <button |
| | | class="w-[200px] h-9 text-sm font-medium rounded-lg cursor-pointer border-none bg-[#f1f1f3] text-[#1a1a1c] transition-colors duration-150 disabled:cursor-not-allowed disabled:text-[#999] hover:[&:not(:disabled)]:bg-[#e7e7ea]" |
| | | :disabled="joinDisabled" |
| | | @click="handleJoin" |
| | | > |
| | | {{ joinLabel }} |
| | | </button> |
| | | </div> |
| | | </template> |
| | | </Popover> |
| | | </div> |
| | | </template> |