From 27cd042df9aca0383a49f3514bc21958dd890912 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期一, 29 六月 2026 15:42:23 +0800
Subject: [PATCH] 银川 1.联调产品维护页面 2.添加IM即时通讯模块
---
src/views/im/home/components/rtc/rtc-group-call-banner.vue | 205 +++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 205 insertions(+), 0 deletions(-)
diff --git a/src/views/im/home/components/rtc/rtc-group-call-banner.vue b/src/views/im/home/components/rtc/rtc-group-call-banner.vue
new file mode 100644
index 0000000..8789f53
--- /dev/null
+++ b/src/views/im/home/components/rtc/rtc-group-call-banner.vue
@@ -0,0 +1,205 @@
+<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)
+
+/** 褰撳墠缇ょ殑娲昏穬閫氳瘽锛況tcStore 缁存姢锛屽弬涓庤�呭姞鍏� / 绂诲紑閫氱煡澧炲垹 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 ? `姝e湪${media}閫氳瘽锛�${count} 浜猴級` : `姝e湪${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锛涙帴鍙h繑鍥炵┖ 鈫� 璇ョ兢宸叉棤娲昏穬閫氳瘽锛岀Щ闄ゆ湰鍦扮紦瀛�
+ 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))
+
+/** 鏈鏄惁姝e湪璇ユ埧闂撮�氳瘽锛堝浜� 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锛況tcStore 鎸� 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>
--
Gitblit v1.9.3