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-call-running.vue |  291 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 291 insertions(+), 0 deletions(-)

diff --git a/src/views/im/home/components/rtc/rtc-call-running.vue b/src/views/im/home/components/rtc/rtc-call-running.vue
new file mode 100644
index 0000000..e0b8c2b
--- /dev/null
+++ b/src/views/im/home/components/rtc/rtc-call-running.vue
@@ -0,0 +1,291 @@
+<script lang="ts" setup>
+import { computed, onUnmounted, ref, watch } from 'vue'
+
+import { IconifyIcon as Icon } from '#/packages/icons/src'
+
+import { formatCallDuration } from '#/views/im/utils/time'
+
+import { useMediaStreamElement } from '../../composables/useMediaStreamElement'
+import { UserAvatar } from '../user'
+import RtcCallParticipantTile, { type CallParticipantVM } from './rtc-call-participant-tile.vue'
+
+const props = defineProps<{
+  cameraEnabled: boolean
+  hangingUp?: boolean
+  /** 鏄惁缇ら�氳瘽锛涘喅瀹氱綉鏍� / 鍗曠偣甯冨眬 + 娴獥澶у皬 */
+  isGroup: boolean
+  isVideo: boolean
+  localStream?: MediaStream | null
+  micEnabled: boolean
+  /** 缃戞牸瑙嗗浘鐢細鎵�鏈夊弬涓庤�咃紙鍚嚜宸憋級 */
+  participants: CallParticipantVM[]
+  peerAvatar?: string
+  /** 1v1 瑙嗗浘鐢� */
+  peerNickname?: string
+  /** 鏄惁澶勪簬缃戠粶閲嶈繛涓紱鏄剧ず椤堕儴榛勮壊鎻愮ず鏉� */
+  reconnecting?: boolean
+  remoteAudioStream?: MediaStream | null
+  remoteVideoStream?: MediaStream | null
+  /** 鏄惁姝e湪灞忓箷鍏变韩锛涙寜閽珮浜� + 鏂囨鍒囨崲 */
+  screenShareEnabled?: boolean
+  /** 鎵0鍣ㄥ紑鍏筹紱true 鏃舵甯告挱鏀捐繙绔煶棰戯紝false 鏃舵墍鏈夎繙绔� audio 鍏冪礌闈欓煶 */
+  speakerEnabled: boolean
+  startedAt: number
+}>()
+
+defineEmits<{
+  addMember: []
+  hangup: []
+  toggleCamera: []
+  toggleMic: []
+  toggleScreenShare: []
+  toggleSpeaker: []
+}>()
+
+/** 缃戞牸鍒楁暟锛涙寜浜烘暟鑷�傚簲锛涜繑鍥� UnoCSS class 瀛楅潰閲忚 JIT 鎵弿鍣ㄩ潤鎬佽瘑鍒� */
+const gridColsClass = computed(() => {
+  const n = props.participants.length
+  if (n <= 1) return 'grid-cols-1'
+  if (n <= 4) return 'grid-cols-2'
+  return 'grid-cols-3'
+})
+
+const setLocalVideoRef = useMediaStreamElement<HTMLVideoElement>(() => props.localStream)
+const setRemoteVideoRef = useMediaStreamElement<HTMLVideoElement>(() => props.remoteVideoStream)
+const setRemoteAudioRef = useMediaStreamElement<HTMLAudioElement>(() => props.remoteAudioStream)
+
+/** 1v1 瑙嗛锛氭槸鍚︽湁杩滅瑙嗛娴� */
+const hasRemoteVideo = computed(() => !props.isGroup && !!props.remoteVideoStream)
+
+const now = ref(Date.now()) // 閫氳瘽鏃堕暱锛涗粎 1v1 璇煶瑙嗗浘闇�瑕佸睍绀猴紝鍏跺畠瑙嗗浘涓嶅惎 tick
+let tick = 0
+watch(
+  () => props.isGroup || props.isVideo,
+  (suppressTick) => {
+    if (suppressTick) {
+      if (tick) {
+        clearInterval(tick)
+        tick = 0
+      }
+      return
+    }
+    now.value = Date.now()
+    tick = window.setInterval(() => {
+      now.value = Date.now()
+    }, 1000)
+  },
+  { immediate: true }
+)
+onUnmounted(() => {
+  if (tick) {
+    clearInterval(tick)
+  }
+})
+
+/** 閫氳瘽鏃堕暱 MM:SS / HH:MM:SS */
+const formattedDuration = computed(() =>
+  formatCallDuration(Math.floor((now.value - props.startedAt) / 1000))
+)
+</script>
+
+<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-[1000] flex flex-col text-white bg-[#1a1a1c]"
+    :class="isGroup ? 'w-[720px] h-[560px]' : 'w-[320px] h-[540px]'"
+  >
+    <!-- 閲嶈繛涓í骞咃紱缃戠粶鎶栧姩鏃舵樉绀猴紝鐩村埌 Reconnected 浜嬩欢娓呴櫎 -->
+    <div
+      v-if="reconnecting"
+      class="inline-flex absolute top-3 left-1/2 z-10 gap-2 items-center px-3.5 py-1.5 text-13px text-[#ffd45e] rounded-full -translate-x-1/2 bg-[rgba(255,196,0,0.18)]"
+    >
+      <span class="reconnect-dot w-2 h-2 rounded-full bg-[#ffd45e]"></span>
+      缃戠粶涓嶄匠锛屾鍦ㄩ噸杩炩�︹��
+    </div>
+    <div class="flex relative flex-1 justify-center items-center">
+      <!-- 缇ら�氳瘽锛氱綉鏍煎竷灞�锛屽垪鏁伴殢浜烘暟鑷�傚簲 -->
+      <div
+        v-if="isGroup"
+        class="grid gap-1 p-1 w-full h-full content-stretch"
+        :class="gridColsClass"
+      >
+        <RtcCallParticipantTile
+          v-for="participant in participants"
+          :key="participant.userId"
+          :participant="participant"
+          :speaker-enabled="speakerEnabled"
+        />
+      </div>
+
+      <!-- 1v1 瑙嗛锛氳繙绔摵搴� + 鏈湴缂╃暐 -->
+      <template v-else-if="isVideo">
+        <div v-show="hasRemoteVideo" class="absolute inset-0">
+          <video
+            :ref="setRemoteVideoRef"
+            class="object-cover w-full h-full"
+            autoplay
+            playsinline
+          ></video>
+        </div>
+        <div v-if="!hasRemoteVideo" class="flex z-[1] flex-col gap-4 items-center">
+          <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
+          v-if="localStream"
+          class="absolute top-4 right-4 z-[2] overflow-hidden w-30 rounded-lg aspect-[9/16] bg-[#333]"
+        >
+          <video
+            :ref="setLocalVideoRef"
+            class="object-cover w-full h-full scale-x-[-1]"
+            autoplay
+            muted
+            playsinline
+          ></video>
+        </div>
+      </template>
+
+      <!-- 1v1 璇煶锛氬ご鍍� + 鏃堕暱 -->
+      <template v-else>
+        <div class="flex z-[1] flex-col gap-4 items-center">
+          <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">{{ formattedDuration }}</div>
+        </div>
+      </template>
+      <audio
+        v-if="!isGroup && remoteAudioStream"
+        :ref="setRemoteAudioRef"
+        autoplay
+        :muted="!speakerEnabled"
+      ></audio>
+    </div>
+
+    <!-- 搴曢儴鎿嶄綔鍖猴細楹﹀厠椋� / 鎵0鍣� / 鎽勫儚澶� / (缇よ亰锛氬叡浜睆骞� / 娣诲姞鎴愬憳) / 鎸傛柇 -->
+    <div
+      class="flex flex-shrink-0 gap-3 justify-around items-center pt-4 px-4 pb-5 bg-black/20"
+    >
+      <div
+        class="flex flex-col gap-2 items-center cursor-pointer select-none min-w-[64px]"
+        @click="$emit('toggleMic')"
+      >
+        <span
+          class="flex justify-center items-center w-[52px] h-[52px] rounded-full"
+          :class="micEnabled ? 'bg-white text-[#1a1a1c]' : 'bg-white/15 text-white'"
+        >
+          <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 min-w-[64px]"
+        @click="$emit('toggleSpeaker')"
+      >
+        <span
+          class="flex justify-center items-center w-[52px] h-[52px] rounded-full"
+          :class="speakerEnabled ? 'bg-white text-[#1a1a1c]' : 'bg-white/15 text-white'"
+        >
+          <Icon
+            :icon="speakerEnabled ? 'ant-design:sound-outlined' : 'tabler:volume-off'"
+            :size="22"
+          />
+        </span>
+        <span class="text-xs text-white/70 whitespace-nowrap">
+          {{ speakerEnabled ? '鎵0鍣ㄥ凡寮�' : '鎵0鍣ㄥ凡鍏�' }}
+        </span>
+      </div>
+      <!-- 鎽勫儚澶存寜閽細绉佽亰瑙嗛鍥哄畾鏄剧ず锛涚兢鑱婁笉璁鸿捣濮� mediaType 閮芥樉绀猴紝璁╃敤鎴锋寜闇�寮� -->
+      <div
+        v-if="isVideo || isGroup"
+        class="flex flex-col gap-2 items-center cursor-pointer select-none min-w-[64px]"
+        @click="$emit('toggleCamera')"
+      >
+        <span
+          class="flex justify-center items-center w-[52px] h-[52px] rounded-full"
+          :class="cameraEnabled ? 'bg-white text-[#1a1a1c]' : 'bg-white/15 text-white'"
+        >
+          <Icon
+            :icon="cameraEnabled ? 'ant-design:video-camera-outlined' : 'tabler:video-off'"
+            :size="22"
+          />
+        </span>
+        <span class="text-xs text-white/70 whitespace-nowrap">
+          {{ cameraEnabled ? '鎽勫儚澶村凡寮�' : '鎽勫儚澶村凡鍏�' }}
+        </span>
+      </div>
+      <!-- 缇ら�氳瘽鎵嶆湁锛氬叡浜睆骞� / 娣诲姞鎴愬憳锛涘叡浜腑鎸夐挳楂樹寒 + 鏂囨鎹㈡垚銆屽仠姝㈠叡浜�� -->
+      <template v-if="isGroup">
+        <div
+          class="flex flex-col gap-2 items-center cursor-pointer select-none min-w-[64px]"
+          @click="$emit('toggleScreenShare')"
+        >
+          <span
+            class="flex justify-center items-center w-[52px] h-[52px] rounded-full"
+            :class="screenShareEnabled ? 'bg-[#07c160] text-white' : 'bg-white/15 text-white'"
+          >
+            <Icon
+              :icon="screenShareEnabled ? 'ant-design:laptop-outlined' : 'tabler:device-laptop-off'"
+              :size="22"
+            />
+          </span>
+          <span class="text-xs text-white/70 whitespace-nowrap">
+            {{ screenShareEnabled ? '鍋滄鍏变韩' : '鍏变韩灞忓箷' }}
+          </span>
+        </div>
+        <div
+          class="flex flex-col gap-2 items-center cursor-pointer select-none min-w-[64px]"
+          @click="$emit('addMember')"
+        >
+          <span class="flex justify-center items-center w-[52px] h-[52px] text-white rounded-full bg-white/15">
+            <Icon icon="ant-design:plus-outlined" :size="22" />
+          </span>
+          <span class="text-xs text-white/70 whitespace-nowrap">娣诲姞鎴愬憳</span>
+        </div>
+      </template>
+      <div
+        class="flex flex-col gap-2 items-center cursor-pointer select-none min-w-[64px]"
+        :class="{ 'opacity-60 pointer-events-none': hangingUp }"
+        @click="$emit('hangup')"
+      >
+        <span class="flex justify-center items-center w-[52px] h-[52px] 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>
+  </div>
+</template>
+
+<style scoped>
+/* 閲嶈繛灏忕偣娣″叆娣″嚭锛汙keyframes 蹇呴』 CSS 瀹氫箟 */
+.reconnect-dot {
+  animation: reconnect-pulse 1s ease-in-out infinite;
+}
+@keyframes reconnect-pulse {
+  0%,
+  100% {
+    opacity: 0.3;
+  }
+  50% {
+    opacity: 1;
+  }
+}
+</style>

--
Gitblit v1.9.3