优化显示
This commit is contained in:
parent
ae36cb183b
commit
0de25a0544
@ -1,53 +1,293 @@
|
||||
<template>
|
||||
<div
|
||||
class="desktop-view-wrapper"
|
||||
:class="{
|
||||
'is-collapsed': isCollapsed,
|
||||
'has-stream': hasMedia,
|
||||
'has-call-only': !!callStream && !desktopStream,
|
||||
}"
|
||||
<div>
|
||||
<!-- Desktop Fullscreen Overlay -->
|
||||
<Transition name="panel-slide">
|
||||
<div v-if="isDesktopActive" class="desktop-overlay">
|
||||
<div class="overlay-header">
|
||||
<div class="header-left">
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<div class="collapse-header" v-if="hasMedia" @click="toggleCollapse">
|
||||
<span>{{ headerTitle }}{{ isCollapsed ? "(已收起)" : "" }}</span>
|
||||
<a-button type="link">
|
||||
<template #icon>
|
||||
<UpOutlined v-if="!isCollapsed" />
|
||||
<DownOutlined v-else />
|
||||
</template>
|
||||
</a-button>
|
||||
<rect x="2" y="3" width="20" height="14" rx="2" />
|
||||
<line x1="8" y1="21" x2="16" y2="21" />
|
||||
<line x1="12" y1="17" x2="12" y2="21" />
|
||||
</svg>
|
||||
<span>远程桌面</span>
|
||||
</div>
|
||||
<div
|
||||
<div class="header-right">
|
||||
<button
|
||||
v-if="desktopStream"
|
||||
class="desktop-view"
|
||||
:class="{ 'is-fullscreen': isFullscreen }"
|
||||
>
|
||||
<div class="video-container" ref="videoContainer">
|
||||
<video ref="videoRef" autoplay playsinline></video>
|
||||
<div class="controls">
|
||||
<a-button
|
||||
type="primary"
|
||||
ghost
|
||||
class="header-btn"
|
||||
@click="toggleFullscreen"
|
||||
:title="isFullscreen ? '退出全屏' : '全屏'"
|
||||
>
|
||||
{{ isFullscreen ? "退出全屏" : "全屏" }}
|
||||
</a-button>
|
||||
<a-button danger ghost @click="endDesktop">结束桌面</a-button>
|
||||
<svg
|
||||
v-if="!isFullscreen"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<polyline points="15 3 21 3 21 9" />
|
||||
<polyline points="9 21 3 21 3 15" />
|
||||
<line x1="21" y1="3" x2="14" y2="10" />
|
||||
<line x1="3" y1="21" x2="10" y2="14" />
|
||||
</svg>
|
||||
<svg
|
||||
v-else
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<polyline points="4 14 10 14 10 20" />
|
||||
<polyline points="20 10 14 10 14 4" />
|
||||
<line x1="14" y1="10" x2="21" y2="3" />
|
||||
<line x1="3" y1="21" x2="10" y2="14" />
|
||||
</svg>
|
||||
<span>{{ isFullscreen ? "退出全屏" : "全屏" }}</span>
|
||||
</button>
|
||||
<button class="header-btn danger" @click="endDesktop">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
<span>结束共享</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overlay-body" ref="videoContainer">
|
||||
<div v-if="desktopStream" class="video-wrapper">
|
||||
<video ref="videoRef" autoplay playsinline></video>
|
||||
</div>
|
||||
<div v-else class="loading-state">
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
class="spin-icon"
|
||||
>
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke-dasharray="31.4 31.4"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<span>等待桌面共享...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isCallActive" class="desktop-call-bar">
|
||||
<div class="call-bar-info">
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"
|
||||
/>
|
||||
</svg>
|
||||
<span>语音通话</span>
|
||||
<span class="call-duration-text">{{ callDurationStr }}</span>
|
||||
</div>
|
||||
<div class="call-bar-actions">
|
||||
<button
|
||||
class="bar-action-btn"
|
||||
:class="{ 'is-muted': isCallMuted }"
|
||||
@click="toggleCallMuted"
|
||||
:title="isCallMuted ? '取消静音' : '静音'"
|
||||
>
|
||||
<svg
|
||||
v-if="!isCallMuted"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"
|
||||
/>
|
||||
<path d="M19 10v2a7 7 0 0 1-14 0v-2" />
|
||||
<line x1="12" y1="19" x2="12" y2="23" />
|
||||
<line x1="8" y1="23" x2="16" y2="23" />
|
||||
</svg>
|
||||
<svg
|
||||
v-else
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<line x1="1" y1="1" x2="23" y2="23" />
|
||||
<path d="M9 9v3a3 3 0 0 0 5.12 2.12" />
|
||||
<path d="M15 9.34V4a3 3 0 0 0-5.94-.6" />
|
||||
<path d="M19 10v2a7 7 0 0 1-8.93 6.69" />
|
||||
<path d="M12 19v4" />
|
||||
<line x1="8" y1="23" x2="16" y2="23" />
|
||||
</svg>
|
||||
<span>{{ isCallMuted ? "已静音" : "静音" }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="bar-action-btn danger"
|
||||
@click="endCall"
|
||||
title="结束通话"
|
||||
>
|
||||
<svg
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
<span>结束</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="callStream" class="call-view">
|
||||
</Transition>
|
||||
|
||||
<!-- Hidden audio for remote call stream -->
|
||||
<audio ref="audioRef" autoplay playsinline></audio>
|
||||
<div class="call-info">
|
||||
<PhoneOutlined />
|
||||
<span>语音通话中</span>
|
||||
|
||||
<!-- Call-Only Floating Panel -->
|
||||
<Transition name="panel-slide">
|
||||
<div v-if="isCallActive && !isDesktopActive" class="call-floating-panel">
|
||||
<div class="call-panel-header">
|
||||
<div class="panel-header-left">
|
||||
<span class="panel-header-icon">
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>语音通话</span>
|
||||
</div>
|
||||
<div class="call-controls">
|
||||
<a-button ghost @click="toggleCallMuted">
|
||||
{{ isCallMuted ? "取消静音" : "静音" }}
|
||||
</a-button>
|
||||
<a-button danger ghost @click="endCall">结束通话</a-button>
|
||||
<button class="panel-close-btn" @click="endCall" title="结束通话">
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="call-panel-body">
|
||||
<div class="call-status-icon">
|
||||
<svg
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
>
|
||||
<path
|
||||
d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="call-status-text">
|
||||
{{ callStream ? "通话中" : "正在建立连接..." }}
|
||||
</div>
|
||||
<div class="call-duration">{{ callDurationStr }}</div>
|
||||
</div>
|
||||
<div class="call-panel-footer">
|
||||
<button
|
||||
class="control-btn mute-btn"
|
||||
:class="{ 'is-muted': isCallMuted }"
|
||||
@click="toggleCallMuted"
|
||||
:title="isCallMuted ? '取消静音' : '静音'"
|
||||
>
|
||||
<svg
|
||||
v-if="!isCallMuted"
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z" />
|
||||
<path d="M19 10v2a7 7 0 0 1-14 0v-2" />
|
||||
<line x1="12" y1="19" x2="12" y2="23" />
|
||||
<line x1="8" y1="23" x2="16" y2="23" />
|
||||
</svg>
|
||||
<svg
|
||||
v-else
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<line x1="1" y1="1" x2="23" y2="23" />
|
||||
<path d="M9 9v3a3 3 0 0 0 5.12 2.12" />
|
||||
<path d="M15 9.34V4a3 3 0 0 0-5.94-.6" />
|
||||
<path d="M19 10v2a7 7 0 0 1-8.93 6.69" />
|
||||
<path d="M12 19v4" />
|
||||
<line x1="8" y1="23" x2="16" y2="23" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
class="control-btn end-call-btn"
|
||||
@click="endCall"
|
||||
title="结束通话"
|
||||
>
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -55,7 +295,6 @@
|
||||
import { computed, nextTick, ref, onMounted, onUnmounted } from "vue";
|
||||
import { peer } from "../utils/peer";
|
||||
import { message } from "ant-design-vue";
|
||||
import { UpOutlined, DownOutlined, PhoneOutlined } from "@ant-design/icons-vue";
|
||||
|
||||
const props = defineProps<{
|
||||
peerId: string;
|
||||
@ -76,67 +315,101 @@ interface MediaEndedEvent extends CustomEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface MediaStartedEvent extends CustomEvent {
|
||||
detail: {
|
||||
peerId: string;
|
||||
};
|
||||
}
|
||||
|
||||
const videoRef = ref<HTMLVideoElement | null>(null);
|
||||
const audioRef = ref<HTMLAudioElement | null>(null);
|
||||
const videoContainer = ref<HTMLElement | null>(null);
|
||||
const desktopStream = ref<MediaStream | null>(null);
|
||||
const callStream = ref<MediaStream | null>(null);
|
||||
const isDesktopActive = ref(false);
|
||||
const isCallActive = ref(false);
|
||||
const isFullscreen = ref(false);
|
||||
const isCollapsed = ref(false);
|
||||
const isCallMuted = ref(false);
|
||||
const callDuration = ref(0);
|
||||
let durationTimer: number | null = null;
|
||||
|
||||
const hasMedia = computed(() => !!desktopStream.value || !!callStream.value);
|
||||
const headerTitle = computed(() => {
|
||||
if (desktopStream.value && callStream.value) return "远程桌面与通话";
|
||||
if (desktopStream.value) return "远程桌面";
|
||||
return "语音通话";
|
||||
const callDurationStr = computed(() => {
|
||||
const mins = Math.floor(callDuration.value / 60);
|
||||
const secs = callDuration.value % 60;
|
||||
return `${String(mins).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
|
||||
});
|
||||
|
||||
// 切换收起/展开状态
|
||||
const toggleCollapse = () => {
|
||||
isCollapsed.value = !isCollapsed.value;
|
||||
const startDurationTimer = () => {
|
||||
callDuration.value = 0;
|
||||
durationTimer = window.setInterval(() => {
|
||||
callDuration.value++;
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const stopDurationTimer = () => {
|
||||
if (durationTimer !== null) {
|
||||
clearInterval(durationTimer);
|
||||
durationTimer = null;
|
||||
}
|
||||
callDuration.value = 0;
|
||||
};
|
||||
|
||||
// 处理远程流
|
||||
const handleStream = async (event: StreamEvent) => {
|
||||
const { stream: remoteStream, type } = event.detail;
|
||||
if (props.peerId !== event.detail.peerId) return;
|
||||
if (type === "desktop") {
|
||||
isDesktopActive.value = true;
|
||||
desktopStream.value = remoteStream;
|
||||
isCollapsed.value = false; // 收到流时自动展开
|
||||
await nextTick();
|
||||
if (videoRef.value) {
|
||||
videoRef.value.srcObject = remoteStream;
|
||||
videoRef.value.muted = true;
|
||||
videoRef.value.play().catch(() => {
|
||||
message.warning("浏览器阻止了自动播放,请点击桌面画面恢复播放");
|
||||
});
|
||||
}
|
||||
} else if (type === "call") {
|
||||
isCallActive.value = true;
|
||||
callStream.value = remoteStream;
|
||||
isCollapsed.value = false;
|
||||
startDurationTimer();
|
||||
await nextTick();
|
||||
if (audioRef.value) {
|
||||
audioRef.value.srcObject = remoteStream;
|
||||
audioRef.value.muted = isCallMuted.value;
|
||||
audioRef.value.play().catch(() => {
|
||||
message.warning("浏览器阻止了自动播放,请点击通话区域恢复声音");
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 处理媒体结束
|
||||
const handleDesktopStarted = (event: MediaStartedEvent) => {
|
||||
if (props.peerId !== event.detail.peerId) return;
|
||||
isDesktopActive.value = true;
|
||||
};
|
||||
|
||||
const handleCallStarted = (event: MediaStartedEvent) => {
|
||||
if (props.peerId !== event.detail.peerId) return;
|
||||
isCallActive.value = true;
|
||||
startDurationTimer();
|
||||
};
|
||||
|
||||
const handleMediaEnded = (event: MediaEndedEvent) => {
|
||||
const { type } = event.detail;
|
||||
if (props.peerId !== event.detail.peerId) return;
|
||||
if (type === "desktop") {
|
||||
isDesktopActive.value = false;
|
||||
desktopStream.value = null;
|
||||
if (videoRef.value) {
|
||||
videoRef.value.srcObject = null;
|
||||
}
|
||||
message.info("远程桌面共享已结束");
|
||||
if (!isCallActive.value) {
|
||||
stopDurationTimer();
|
||||
}
|
||||
} else if (type === "call") {
|
||||
isCallActive.value = false;
|
||||
callStream.value = null;
|
||||
if (audioRef.value) {
|
||||
audioRef.value.srcObject = null;
|
||||
}
|
||||
stopDurationTimer();
|
||||
message.info("语音通话已结束");
|
||||
}
|
||||
};
|
||||
@ -147,6 +420,7 @@ const endDesktop = () => {
|
||||
|
||||
const endCall = () => {
|
||||
peer.endMedia(props.peerId, "call");
|
||||
stopDurationTimer();
|
||||
};
|
||||
|
||||
const toggleCallMuted = () => {
|
||||
@ -156,10 +430,8 @@ const toggleCallMuted = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 切换全屏
|
||||
const toggleFullscreen = async () => {
|
||||
if (!videoContainer.value) return;
|
||||
|
||||
try {
|
||||
if (!isFullscreen.value) {
|
||||
if (videoContainer.value.requestFullscreen) {
|
||||
@ -170,12 +442,11 @@ const toggleFullscreen = async () => {
|
||||
await document.exitFullscreen();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
} catch {
|
||||
message.error("切换全屏失败");
|
||||
}
|
||||
};
|
||||
|
||||
// 监听全屏变化
|
||||
const handleFullscreenChange = () => {
|
||||
isFullscreen.value = !!document.fullscreenElement;
|
||||
};
|
||||
@ -186,132 +457,398 @@ const streamListener: EventListener = (event) => {
|
||||
const mediaEndedListener: EventListener = (event) => {
|
||||
handleMediaEnded(event as MediaEndedEvent);
|
||||
};
|
||||
const desktopStartedListener: EventListener = (event) => {
|
||||
handleDesktopStarted(event as MediaStartedEvent);
|
||||
};
|
||||
const callStartedListener: EventListener = (event) => {
|
||||
handleCallStarted(event as MediaStartedEvent);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
peer.on("stream", streamListener);
|
||||
peer.on("desktop-started", desktopStartedListener);
|
||||
peer.on("call-started", callStartedListener);
|
||||
peer.on("media-ended", mediaEndedListener);
|
||||
document.addEventListener("fullscreenchange", handleFullscreenChange);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
peer.off("stream", streamListener);
|
||||
peer.off("desktop-started", desktopStartedListener);
|
||||
peer.off("call-started", callStartedListener);
|
||||
peer.off("media-ended", mediaEndedListener);
|
||||
document.removeEventListener("fullscreenchange", handleFullscreenChange);
|
||||
|
||||
// 清理视频流
|
||||
stopDurationTimer();
|
||||
desktopStream.value?.getTracks().forEach((track) => track.stop());
|
||||
callStream.value?.getTracks().forEach((track) => track.stop());
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.desktop-view-wrapper {
|
||||
/* Slide Animation */
|
||||
.panel-slide-enter-active,
|
||||
.panel-slide-leave-active {
|
||||
transition: transform 0.35s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
.panel-slide-enter-from,
|
||||
.panel-slide-leave-to {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
|
||||
/* Desktop Overlay - Fullscreen */
|
||||
.desktop-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
background: #000;
|
||||
transition: all 0.3s ease;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.desktop-view-wrapper.has-stream {
|
||||
height: 300px;
|
||||
}
|
||||
|
||||
.desktop-view-wrapper.has-call-only {
|
||||
height: 104px;
|
||||
}
|
||||
|
||||
.desktop-view-wrapper.is-collapsed {
|
||||
height: 40px !important;
|
||||
}
|
||||
|
||||
.collapse-header {
|
||||
height: 40px;
|
||||
background: #1a1a1a;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
background: #0a0a0a;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.overlay-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
padding: 12px 20px;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
flex-shrink: 0;
|
||||
z-index: 10;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #e0e0e0;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.header-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: #ccc;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.collapse-header:hover {
|
||||
background: #2a2a2a;
|
||||
.header-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
border-color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.desktop-view {
|
||||
width: 100%;
|
||||
height: calc(100% - 40px);
|
||||
background: #000;
|
||||
.header-btn.danger:hover {
|
||||
background: rgba(245, 34, 45, 0.15);
|
||||
border-color: #f5222d;
|
||||
color: #f5222d;
|
||||
}
|
||||
|
||||
.overlay-body {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.video-container {
|
||||
.video-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.controls {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 10;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
.loading-state {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.video-container:hover .controls {
|
||||
opacity: 1;
|
||||
.spin-icon {
|
||||
animation: spin 1.5s linear infinite;
|
||||
}
|
||||
|
||||
.is-fullscreen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9999;
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.call-view {
|
||||
height: 64px;
|
||||
padding: 0 16px;
|
||||
background: #111;
|
||||
color: #fff;
|
||||
/* Desktop Call Bar */
|
||||
.desktop-call-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-top: 1px solid #2a2a2a;
|
||||
padding: 10px 20px;
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.call-info,
|
||||
.call-controls {
|
||||
.call-bar-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
:deep(.ant-empty-description) {
|
||||
.call-duration-text {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.call-bar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bar-action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 5px 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: #ccc;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.bar-action-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
:deep(.anticon) {
|
||||
vertical-align: 0;
|
||||
.bar-action-btn.is-muted {
|
||||
background: rgba(250, 173, 20, 0.15);
|
||||
border-color: #faad14;
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
.bar-action-btn.danger:hover {
|
||||
background: rgba(245, 34, 45, 0.15);
|
||||
border-color: #f5222d;
|
||||
color: #f5222d;
|
||||
}
|
||||
|
||||
/* Call-Only Floating Panel */
|
||||
.call-floating-panel {
|
||||
position: fixed;
|
||||
right: 24px;
|
||||
bottom: 24px;
|
||||
width: 320px;
|
||||
z-index: 1000;
|
||||
background: linear-gradient(145deg, #1a1a2e, #16213e);
|
||||
border-radius: 16px;
|
||||
box-shadow:
|
||||
0 8px 32px rgba(0, 0, 0, 0.5),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.06);
|
||||
overflow: hidden;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.call-panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.panel-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.panel-header-icon {
|
||||
display: flex;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.panel-close-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.panel-close-btn:hover {
|
||||
background: rgba(245, 34, 45, 0.2);
|
||||
color: #f5222d;
|
||||
}
|
||||
|
||||
.call-panel-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 32px 24px 24px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.call-status-icon {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(82, 196, 26, 0.1);
|
||||
border-radius: 50%;
|
||||
color: #52c41a;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(82, 196, 26, 0.3);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 12px rgba(82, 196, 26, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.call-status-text {
|
||||
font-size: 15px;
|
||||
color: #ddd;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.call-duration {
|
||||
font-size: 32px;
|
||||
font-weight: 300;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: #fff;
|
||||
letter-spacing: 2px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.call-panel-footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
padding: 16px 24px 20px;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s;
|
||||
}
|
||||
|
||||
.mute-btn {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.mute-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.mute-btn.is-muted {
|
||||
background: rgba(250, 173, 20, 0.2);
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
.mute-btn.is-muted:hover {
|
||||
background: rgba(250, 173, 20, 0.3);
|
||||
}
|
||||
|
||||
.end-call-btn {
|
||||
background: #f5222d;
|
||||
color: #fff;
|
||||
transform: rotate(135deg);
|
||||
}
|
||||
|
||||
.end-call-btn:hover {
|
||||
background: #ff4d4f;
|
||||
transform: rotate(135deg) scale(1.05);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.call-floating-panel {
|
||||
right: 12px;
|
||||
bottom: 12px;
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.call-panel-body {
|
||||
padding: 24px 20px 20px;
|
||||
}
|
||||
|
||||
.call-duration {
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
}
|
||||
|
||||
.overlay-header {
|
||||
padding: 10px 14px;
|
||||
}
|
||||
|
||||
.header-btn span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.desktop-call-bar {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user