gaoluyang
2026-06-29 27cd042df9aca0383a49f3514bc21958dd890912
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
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>