gaoluyang
2026-06-29 27cd042df9aca0383a49f3514bc21958dd890912
src/views/im/home/components/rtc/rtc-call-participant-tile.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,110 @@
<script lang="ts" setup>
import { useMediaStreamElement } from '../../composables/useMediaStreamElement'
import { UserAvatar } from '../user'
export interface CallParticipantVM {
  userId: number
  nickname: string
  avatar?: string
  isLocal: boolean
  videoStream?: MediaStream | null
  audioStream?: MediaStream | null
  /** ç­‰å¾…加入;UI æ˜¾ç¤ºä¸‰ç‚¹åŠ¨ç”» */
  pending?: boolean
}
const props = defineProps<{
  participant: CallParticipantVM
  /** æ‰¬å£°å™¨å¼€å…³ï¼›ä¸º false æ—¶é™éŸ³è¯¥æ ¼å­çš„远端音频 */
  speakerEnabled: boolean
}>()
const setVideoRef = useMediaStreamElement<HTMLVideoElement>(() => props.participant.videoStream)
const setAudioRef = useMediaStreamElement<HTMLAudioElement>(() => props.participant.audioStream)
</script>
<template>
  <!--
    ç¾¤é€šè¯å®«æ ¼é‡Œçš„单个参与者格子;
    ä¼˜å…ˆæ¸²æŸ“远端视频(有 videoStream æ—¶é“ºæ»¡ï¼‰ï¼Œæ— è§†é¢‘降级渲染头像 + åå­—胶囊;
    pending=true æ—¶å åŠ ã€ŒæŽ¥å…¥ä¸­ã€ä¸‰ç‚¹åŠ¨ç”»å ä½ï¼Œç”¨äºŽè¢«é‚€è¯·äººæœªæŽ¥é€šå‰çš„å ä½æ¸²æŸ“ï¼›
    è¿œç«¯éŸ³é¢‘通过隐藏 audio æ’­æ”¾ï¼ˆæœ¬ç«¯ isLocal ä¸æ¸²æŸ“避免回声)
  -->
  <div
    class="flex relative overflow-hidden justify-center items-center w-full h-full rounded-md bg-[#2a2a2c]"
  >
    <!-- è§†é¢‘可用:渲染 video;否则渲染头像或默认占位 -->
    <video
      v-if="participant.videoStream"
      :ref="setVideoRef"
      class="object-cover w-full h-full"
      autoplay
      playsinline
      :muted="participant.isLocal"
    ></video>
    <div v-else class="flex justify-center items-center w-full h-full">
      <UserAvatar
        :url="participant.avatar"
        :name="participant.nickname"
        :size="64"
        radius="50%"
        :clickable="false"
      />
    </div>
    <!-- è¿œç«¯éŸ³é¢‘;通过 audio å…ƒç´ æ’­æ”¾ï¼Œæœ¬ç«¯é™éŸ³é¿å…å›žå£°ï¼›æ‰¬å£°å™¨å…³é—­æ—¶æ•´ä½“静音 -->
    <audio
      v-if="participant.audioStream && !participant.isLocal"
      :ref="setAudioRef"
      autoplay
      :muted="!speakerEnabled"
    ></audio>
    <!-- å·¦ä¸‹è§’名字胶囊 -->
    <div
      class="flex absolute bottom-3 left-3 gap-1.5 items-center py-[3px] pr-2.5 pl-[3px] text-13px text-white rounded-full bg-black/45 max-w-[calc(100%-60px)]"
    >
      <UserAvatar
        :url="participant.avatar"
        :name="participant.nickname"
        :size="22"
        radius="50%"
        :clickable="false"
      />
      <span class="truncate min-w-0">{{ participant.nickname }}</span>
    </div>
    <!-- ç­‰å¾…对方加入:三点动画 +「接入中」文案;位置在格子中心下方 60px -->
    <div
      v-if="participant.pending"
      class="flex absolute left-1/2 flex-col gap-1.5 items-center -translate-x-1/2 -translate-y-1/2 top-[calc(50%+60px)]"
    >
      <div class="flex gap-1.5">
        <span
          v-for="i in 3"
          :key="i"
          class="tile-dot w-1.5 h-1.5 rounded-full bg-white/60"
          :style="{ animationDelay: `${(i - 1) * 0.2}s` }"
        ></span>
      </div>
      <span class="text-xs text-white/70">接入中</span>
    </div>
  </div>
</template>
<style scoped>
/* ä¸‰ç‚¹æ·¡å…¥æ·¡å‡ºåŠ¨ç”»ï¼›@keyframes å¿…é¡» CSS å®šä¹‰ */
.tile-dot {
  animation: tile-dot 1.4s infinite ease-in-out both;
}
@keyframes tile-dot {
  0%,
  80%,
  100% {
    opacity: 0.3;
  }
  40% {
    opacity: 1;
  }
}
</style>