✨ feat(im): 通话窗扬声器开关 + 按钮关闭态统一深色样式 + 群通话支持刷新后重新加入
- useLiveKitRoom 增加 speakerEnabled 状态 + setSpeakerEnabled;audio 元素 :muted 联动,实现扬声器实际开关 - mic / speaker / camera / 屏幕共享 4 个按钮关闭态统一 bg-white/15 深色(之前一直 bg-white 像「开」) - speaker / camera / 屏幕共享 关闭态 icon 借用 tabler:volume-off / video-off / device-laptop-off 显斜线(ant-design 缺 muted 变体) - RtcGroupCallBanner 修复刷新后无法重新加入:按钮文案改为「已在通话中 / 重新加入 / 加入」三态;按钮文字色锁定深色防暗色主题不可见 - RtcCallIncoming 对齐微信样式:右上角小条 + 横排(头像 / 名 / 按钮);群聊带「通话成员」头像行 - RtcCallRunning UnoCSS 重写 + 接收 isGroup prop(去 conversationType 派生) - RtcCallParticipantTile UnoCSS 重写 + speakerEnabled 透传静音 - 注释 / UI 文案半角省略号 → 全角……;watcher 参数 hidden → suppressTickim
parent
03d0ce800d
commit
5d222bdf48
|
|
@ -18,32 +18,33 @@
|
|||
class="self-start"
|
||||
/>
|
||||
|
||||
<!-- 中:群聊单行(邀请人 + 文案)+ 通话成员;私聊两行 -->
|
||||
<!-- 中:群聊单行(邀请人 + 文案)+ 通话成员;私聊两行(名 / 文案) -->
|
||||
<div class="flex flex-col flex-1 gap-1 self-start min-w-0">
|
||||
<template v-if="isGroup">
|
||||
<div class="text-sm truncate">
|
||||
<span class="font-medium">{{ payload?.inviterNickname || '对方' }}</span>
|
||||
<span class="ml-1 text-white/60">{{ tipText }}</span>
|
||||
</div>
|
||||
<template v-if="callMembers.length > 0">
|
||||
<div class="mt-1 text-xs text-white/45">通话成员</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<UserAvatar
|
||||
v-for="m in callMembers"
|
||||
:key="m.userId"
|
||||
:url="m.avatar"
|
||||
:name="m.nickname"
|
||||
:size="22"
|
||||
radius="4px"
|
||||
:clickable="false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<!-- 名 + 文案:群单行内联,私聊上下两行 -->
|
||||
<div v-if="isGroup" class="text-sm truncate">
|
||||
<span class="font-medium">{{ payload?.inviterNickname || '对方' }}</span>
|
||||
<span class="ml-1 text-white/60">{{ tipText }}</span>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="text-sm font-medium truncate">{{ payload?.inviterNickname || '对方' }}</div>
|
||||
<div class="text-13px text-white/60 truncate">{{ tipText }}</div>
|
||||
</template>
|
||||
|
||||
<!-- 群通话成员行;私聊无 -->
|
||||
<template v-if="isGroup && callMembers.length > 0">
|
||||
<div class="mt-1 text-xs text-white/45">通话成员</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<UserAvatar
|
||||
v-for="member in callMembers"
|
||||
:key="member.userId"
|
||||
:url="member.avatar"
|
||||
:name="member.nickname"
|
||||
:size="22"
|
||||
radius="4px"
|
||||
:clickable="false"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 右下角:拒绝 / 接听 -->
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<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-[9999] flex flex-col text-white bg-gradient-to-b from-[#2a2a2c] to-[#1a1a1c]"
|
||||
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-gradient-to-b from-[#2a2a2c] to-[#1a1a1c]"
|
||||
:class="isGroup ? 'w-[720px] h-[560px]' : 'w-[320px] h-[540px]'"
|
||||
>
|
||||
<div class="flex relative flex-1 justify-center items-center">
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
:clickable="false"
|
||||
/>
|
||||
<div class="text-[17px] font-medium">{{ peerNickname || '对方' }}</div>
|
||||
<div class="text-13px text-white/60">等待对方接受邀请...</div>
|
||||
<div class="text-13px text-white/60">等待对方接受邀请……</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -36,8 +36,10 @@
|
|||
class="flex flex-col gap-2 items-center cursor-pointer select-none"
|
||||
@click="$emit('toggle-mic')"
|
||||
>
|
||||
<!-- ant-design 系列里 mic 有 audio-muted-outlined 变体;speaker / camera 没有 muted 变体,off 态借 tabler:*-off 表达斜线 -->
|
||||
<span
|
||||
class="flex justify-center items-center w-12 h-12 bg-white rounded-full text-[#1a1a1c]"
|
||||
class="flex justify-center items-center w-12 h-12 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'"
|
||||
|
|
@ -65,14 +67,11 @@
|
|||
@click="$emit('toggle-camera')"
|
||||
>
|
||||
<span
|
||||
class="flex justify-center items-center w-12 h-12 bg-white rounded-full text-[#1a1a1c]"
|
||||
class="flex justify-center items-center w-12 h-12 rounded-full"
|
||||
:class="cameraEnabled ? 'bg-white text-[#1a1a1c]' : 'bg-white/15 text-white'"
|
||||
>
|
||||
<Icon
|
||||
:icon="
|
||||
cameraEnabled
|
||||
? 'ant-design:video-camera-outlined'
|
||||
: 'ant-design:video-camera-add-outlined'
|
||||
"
|
||||
:icon="cameraEnabled ? 'ant-design:video-camera-outlined' : 'tabler:video-off'"
|
||||
:size="22"
|
||||
/>
|
||||
</span>
|
||||
|
|
@ -86,11 +85,17 @@
|
|||
@click="$emit('toggle-speaker')"
|
||||
>
|
||||
<span
|
||||
class="flex justify-center items-center w-12 h-12 bg-white rounded-full text-[#1a1a1c]"
|
||||
class="flex justify-center items-center w-12 h-12 rounded-full"
|
||||
:class="speakerEnabled ? 'bg-white text-[#1a1a1c]' : 'bg-white/15 text-white'"
|
||||
>
|
||||
<Icon icon="ant-design:sound-outlined" :size="22" />
|
||||
<Icon
|
||||
:icon="speakerEnabled ? 'ant-design:sound-outlined' : 'tabler:volume-off'"
|
||||
:size="22"
|
||||
/>
|
||||
</span>
|
||||
<span class="text-xs text-white/70 whitespace-nowrap">
|
||||
{{ speakerEnabled ? '扬声器已开' : '扬声器已关' }}
|
||||
</span>
|
||||
<span class="text-xs text-white/70 whitespace-nowrap">扬声器已开</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -108,6 +113,7 @@ const props = defineProps<{
|
|||
isVideo: boolean
|
||||
micEnabled: boolean
|
||||
cameraEnabled: boolean
|
||||
speakerEnabled: boolean
|
||||
localStream?: MediaStream | null // 本地视频流;视频呼叫预览铺底
|
||||
}>()
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
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">
|
||||
<!-- 群通话:网格布局,列数随人数自适应 -->
|
||||
|
|
@ -20,9 +20,9 @@
|
|||
:class="gridColsClass"
|
||||
>
|
||||
<RtcCallParticipantTile
|
||||
v-for="p in participants"
|
||||
:key="p.userId"
|
||||
:participant="p"
|
||||
v-for="participant in participants"
|
||||
:key="participant.userId"
|
||||
:participant="participant"
|
||||
:speaker-enabled="speakerEnabled"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -46,7 +46,7 @@
|
|||
:clickable="false"
|
||||
/>
|
||||
<div class="text-[17px] font-medium">{{ peerNickname }}</div>
|
||||
<div class="text-13px text-white/60">等待对方开启摄像头...</div>
|
||||
<div class="text-13px text-white/60">等待对方开启摄像头……</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="localStream"
|
||||
|
|
@ -146,7 +146,10 @@
|
|||
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="ant-design:laptop-outlined" :size="22" />
|
||||
<Icon
|
||||
:icon="screenShareEnabled ? 'ant-design:laptop-outlined' : 'tabler:device-laptop-off'"
|
||||
:size="22"
|
||||
/>
|
||||
</span>
|
||||
<span class="text-xs text-white/70 whitespace-nowrap">
|
||||
{{ screenShareEnabled ? '停止共享' : '共享屏幕' }}
|
||||
|
|
@ -235,8 +238,8 @@ const now = ref(Date.now())
|
|||
let tick = 0
|
||||
watch(
|
||||
() => props.isGroup || props.isVideo,
|
||||
(hidden) => {
|
||||
if (hidden) {
|
||||
(suppressTick) => {
|
||||
if (suppressTick) {
|
||||
if (tick) {
|
||||
clearInterval(tick)
|
||||
tick = 0
|
||||
|
|
|
|||
|
|
@ -29,10 +29,15 @@
|
|||
<!-- 展开面板:在通话成员头像横排 + 加入按钮 -->
|
||||
<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]">
|
||||
<div v-for="m in joinedMembers" :key="m.userId" class="inline-flex" :title="m.nickname">
|
||||
<div
|
||||
v-for="member in joinedMembers"
|
||||
:key="member.userId"
|
||||
class="inline-flex"
|
||||
:title="member.nickname"
|
||||
>
|
||||
<UserAvatar
|
||||
:url="m.avatar"
|
||||
:name="m.nickname"
|
||||
:url="member.avatar"
|
||||
:name="member.nickname"
|
||||
:size="40"
|
||||
radius="6px"
|
||||
:clickable="false"
|
||||
|
|
@ -43,13 +48,13 @@
|
|||
暂无成员在通话
|
||||
</div>
|
||||
</div>
|
||||
<!-- 自己已在通话内时置灰显示「已在通话中」,避免重复 join -->
|
||||
<!-- 本端在通话内时置灰「已在通话中」;服务端残留我但本端连接断了显示「重新加入」(刷新页面后场景) -->
|
||||
<button
|
||||
class="w-[200px] h-9 text-sm font-medium rounded-lg cursor-pointer border-none bg-[#f1f1f3] text-[var(--el-text-color-primary)] transition-colors duration-150 disabled:cursor-not-allowed disabled:text-[var(--el-text-color-secondary)] hover:[&:not(:disabled)]:bg-[#e7e7ea]"
|
||||
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"
|
||||
>
|
||||
{{ joinDisabled ? '已在通话中' : '加入' }}
|
||||
{{ joinLabel }}
|
||||
</button>
|
||||
</div>
|
||||
</el-popover>
|
||||
|
|
@ -136,15 +141,30 @@ const joinedMembers = computed(() => {
|
|||
|
||||
const joinedCount = computed(() => joinedMembers.value.length)
|
||||
|
||||
/** 加入按钮禁用:自己已经在该房间内(含本端正在 INVITING / RUNNING) */
|
||||
const joinDisabled = computed(() => {
|
||||
/** 本端是否正在该房间通话(处于 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()
|
||||
if (rtcStore.isActive && rtcStore.call?.room === activeCall.value?.room) {
|
||||
return true
|
||||
}
|
||||
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;rtcStore 按 status 自动进 RUNNING */
|
||||
async function handleJoin() {
|
||||
const call = activeCall.value
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ export function useLiveKitRoom() {
|
|||
const micEnabled = ref(true)
|
||||
/** 摄像头开关 */
|
||||
const cameraEnabled = ref(false)
|
||||
/** 扬声器开关;浏览器无系统级 API,通过 audio 元素 muted 属性实现远端音频静音 */
|
||||
const speakerEnabled = ref(true)
|
||||
/** 屏幕共享开关 */
|
||||
const screenShareEnabled = ref(false)
|
||||
/** 当前是否处于「重连中」;瞬断时 UI 显示提示而不强制结束通话 */
|
||||
|
|
@ -99,7 +101,7 @@ export function useLiveKitRoom() {
|
|||
.on(RoomEvent.ConnectionQualityChanged, (quality) => {
|
||||
connectionQuality.value = quality
|
||||
})
|
||||
// 瞬断 → 显示「重连中」;不关通话窗,由 ICE restart 机制恢复
|
||||
// 瞬断 → 显示「重连中」;不关通话窗,由 SDK 内部重连机制恢复
|
||||
.on(RoomEvent.Reconnecting, () => {
|
||||
reconnecting.value = true
|
||||
})
|
||||
|
|
@ -113,7 +115,7 @@ export function useLiveKitRoom() {
|
|||
disconnectedHandlers.forEach((cb) => cb())
|
||||
})
|
||||
|
||||
// 预热 getUserMedia 与 WebSocket 握手并行,省 100~300ms 串行延迟;
|
||||
// 预热 getUserMedia 与 WebSocket 握手并行,省 100~300ms 串行延迟;
|
||||
// 拿到的 stream 仅用于触发权限弹窗 + 设备就绪,握手完成后由 LiveKit 内部重新请求设备发布轨
|
||||
const warmup = prewarmMedia(opts)
|
||||
// 建立 WebSocket 信令 + WebRTC 媒体通道;完成后 localParticipant 可用,已在房参与者会通过 ParticipantConnected 事件批量推送
|
||||
|
|
@ -176,6 +178,11 @@ export function useLiveKitRoom() {
|
|||
cameraEnabled.value = enabled
|
||||
}
|
||||
|
||||
/** 切扬声器;仅切响应式状态,实际静音由模板上 audio 元素 :muted 绑定生效 */
|
||||
function setSpeakerEnabled(enabled: boolean) {
|
||||
speakerEnabled.value = enabled
|
||||
}
|
||||
|
||||
/**
|
||||
* 切屏幕共享;
|
||||
*
|
||||
|
|
@ -264,6 +271,7 @@ export function useLiveKitRoom() {
|
|||
reconnecting.value = false
|
||||
micEnabled.value = true
|
||||
cameraEnabled.value = false
|
||||
speakerEnabled.value = true
|
||||
screenShareEnabled.value = false
|
||||
}
|
||||
|
||||
|
|
@ -275,12 +283,14 @@ export function useLiveKitRoom() {
|
|||
connectionQuality,
|
||||
micEnabled,
|
||||
cameraEnabled,
|
||||
speakerEnabled,
|
||||
screenShareEnabled,
|
||||
reconnecting,
|
||||
connect,
|
||||
disconnect,
|
||||
setMicEnabled,
|
||||
setCameraEnabled,
|
||||
setSpeakerEnabled,
|
||||
setScreenShareEnabled,
|
||||
pickStream,
|
||||
onDisconnected,
|
||||
|
|
|
|||
Loading…
Reference in New Issue