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