优化显示
This commit is contained in:
parent
ae36cb183b
commit
0de25a0544
@ -1,53 +1,293 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div>
|
||||||
class="desktop-view-wrapper"
|
<!-- Desktop Fullscreen Overlay -->
|
||||||
:class="{
|
<Transition name="panel-slide">
|
||||||
'is-collapsed': isCollapsed,
|
<div v-if="isDesktopActive" class="desktop-overlay">
|
||||||
'has-stream': hasMedia,
|
<div class="overlay-header">
|
||||||
'has-call-only': !!callStream && !desktopStream,
|
<div class="header-left">
|
||||||
}"
|
<svg
|
||||||
>
|
width="18"
|
||||||
<div class="collapse-header" v-if="hasMedia" @click="toggleCollapse">
|
height="18"
|
||||||
<span>{{ headerTitle }}{{ isCollapsed ? "(已收起)" : "" }}</span>
|
viewBox="0 0 24 24"
|
||||||
<a-button type="link">
|
fill="none"
|
||||||
<template #icon>
|
stroke="currentColor"
|
||||||
<UpOutlined v-if="!isCollapsed" />
|
stroke-width="2"
|
||||||
<DownOutlined v-else />
|
>
|
||||||
</template>
|
<rect x="2" y="3" width="20" height="14" rx="2" />
|
||||||
</a-button>
|
<line x1="8" y1="21" x2="16" y2="21" />
|
||||||
</div>
|
<line x1="12" y1="17" x2="12" y2="21" />
|
||||||
<div
|
</svg>
|
||||||
v-if="desktopStream"
|
<span>远程桌面</span>
|
||||||
class="desktop-view"
|
</div>
|
||||||
:class="{ 'is-fullscreen': isFullscreen }"
|
<div class="header-right">
|
||||||
>
|
<button
|
||||||
<div class="video-container" ref="videoContainer">
|
v-if="desktopStream"
|
||||||
<video ref="videoRef" autoplay playsinline></video>
|
class="header-btn"
|
||||||
<div class="controls">
|
@click="toggleFullscreen"
|
||||||
<a-button
|
:title="isFullscreen ? '退出全屏' : '全屏'"
|
||||||
type="primary"
|
>
|
||||||
ghost
|
<svg
|
||||||
@click="toggleFullscreen"
|
v-if="!isFullscreen"
|
||||||
>
|
width="16"
|
||||||
{{ isFullscreen ? "退出全屏" : "全屏" }}
|
height="16"
|
||||||
</a-button>
|
viewBox="0 0 24 24"
|
||||||
<a-button danger ghost @click="endDesktop">结束桌面</a-button>
|
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>
|
</div>
|
||||||
</div>
|
</Transition>
|
||||||
<div v-if="callStream" class="call-view">
|
|
||||||
<audio ref="audioRef" autoplay playsinline></audio>
|
<!-- Hidden audio for remote call stream -->
|
||||||
<div class="call-info">
|
<audio ref="audioRef" autoplay playsinline></audio>
|
||||||
<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>
|
||||||
|
<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>
|
</div>
|
||||||
<div class="call-controls">
|
</Transition>
|
||||||
<a-button ghost @click="toggleCallMuted">
|
|
||||||
{{ isCallMuted ? "取消静音" : "静音" }}
|
|
||||||
</a-button>
|
|
||||||
<a-button danger ghost @click="endCall">结束通话</a-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -55,7 +295,6 @@
|
|||||||
import { computed, nextTick, ref, onMounted, onUnmounted } from "vue";
|
import { computed, nextTick, ref, onMounted, onUnmounted } from "vue";
|
||||||
import { peer } from "../utils/peer";
|
import { peer } from "../utils/peer";
|
||||||
import { message } from "ant-design-vue";
|
import { message } from "ant-design-vue";
|
||||||
import { UpOutlined, DownOutlined, PhoneOutlined } from "@ant-design/icons-vue";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
peerId: string;
|
peerId: string;
|
||||||
@ -76,67 +315,101 @@ interface MediaEndedEvent extends CustomEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface MediaStartedEvent extends CustomEvent {
|
||||||
|
detail: {
|
||||||
|
peerId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const videoRef = ref<HTMLVideoElement | null>(null);
|
const videoRef = ref<HTMLVideoElement | null>(null);
|
||||||
const audioRef = ref<HTMLAudioElement | null>(null);
|
const audioRef = ref<HTMLAudioElement | null>(null);
|
||||||
const videoContainer = ref<HTMLElement | null>(null);
|
const videoContainer = ref<HTMLElement | null>(null);
|
||||||
const desktopStream = ref<MediaStream | null>(null);
|
const desktopStream = ref<MediaStream | null>(null);
|
||||||
const callStream = ref<MediaStream | null>(null);
|
const callStream = ref<MediaStream | null>(null);
|
||||||
|
const isDesktopActive = ref(false);
|
||||||
|
const isCallActive = ref(false);
|
||||||
const isFullscreen = ref(false);
|
const isFullscreen = ref(false);
|
||||||
const isCollapsed = ref(false);
|
|
||||||
const isCallMuted = ref(false);
|
const isCallMuted = ref(false);
|
||||||
|
const callDuration = ref(0);
|
||||||
|
let durationTimer: number | null = null;
|
||||||
|
|
||||||
const hasMedia = computed(() => !!desktopStream.value || !!callStream.value);
|
const callDurationStr = computed(() => {
|
||||||
const headerTitle = computed(() => {
|
const mins = Math.floor(callDuration.value / 60);
|
||||||
if (desktopStream.value && callStream.value) return "远程桌面与通话";
|
const secs = callDuration.value % 60;
|
||||||
if (desktopStream.value) return "远程桌面";
|
return `${String(mins).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
|
||||||
return "语音通话";
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 切换收起/展开状态
|
const startDurationTimer = () => {
|
||||||
const toggleCollapse = () => {
|
callDuration.value = 0;
|
||||||
isCollapsed.value = !isCollapsed.value;
|
durationTimer = window.setInterval(() => {
|
||||||
|
callDuration.value++;
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopDurationTimer = () => {
|
||||||
|
if (durationTimer !== null) {
|
||||||
|
clearInterval(durationTimer);
|
||||||
|
durationTimer = null;
|
||||||
|
}
|
||||||
|
callDuration.value = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理远程流
|
|
||||||
const handleStream = async (event: StreamEvent) => {
|
const handleStream = async (event: StreamEvent) => {
|
||||||
const { stream: remoteStream, type } = event.detail;
|
const { stream: remoteStream, type } = event.detail;
|
||||||
if (props.peerId !== event.detail.peerId) return;
|
if (props.peerId !== event.detail.peerId) return;
|
||||||
if (type === "desktop") {
|
if (type === "desktop") {
|
||||||
|
isDesktopActive.value = true;
|
||||||
desktopStream.value = remoteStream;
|
desktopStream.value = remoteStream;
|
||||||
isCollapsed.value = false; // 收到流时自动展开
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
if (videoRef.value) {
|
if (videoRef.value) {
|
||||||
videoRef.value.srcObject = remoteStream;
|
videoRef.value.srcObject = remoteStream;
|
||||||
|
videoRef.value.muted = true;
|
||||||
|
videoRef.value.play().catch(() => {
|
||||||
|
message.warning("浏览器阻止了自动播放,请点击桌面画面恢复播放");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else if (type === "call") {
|
} else if (type === "call") {
|
||||||
|
isCallActive.value = true;
|
||||||
callStream.value = remoteStream;
|
callStream.value = remoteStream;
|
||||||
isCollapsed.value = false;
|
startDurationTimer();
|
||||||
await nextTick();
|
await nextTick();
|
||||||
if (audioRef.value) {
|
if (audioRef.value) {
|
||||||
audioRef.value.srcObject = remoteStream;
|
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 handleMediaEnded = (event: MediaEndedEvent) => {
|
||||||
const { type } = event.detail;
|
const { type } = event.detail;
|
||||||
if (props.peerId !== event.detail.peerId) return;
|
if (props.peerId !== event.detail.peerId) return;
|
||||||
if (type === "desktop") {
|
if (type === "desktop") {
|
||||||
|
isDesktopActive.value = false;
|
||||||
desktopStream.value = null;
|
desktopStream.value = null;
|
||||||
if (videoRef.value) {
|
if (videoRef.value) {
|
||||||
videoRef.value.srcObject = null;
|
videoRef.value.srcObject = null;
|
||||||
}
|
}
|
||||||
message.info("远程桌面共享已结束");
|
message.info("远程桌面共享已结束");
|
||||||
|
if (!isCallActive.value) {
|
||||||
|
stopDurationTimer();
|
||||||
|
}
|
||||||
} else if (type === "call") {
|
} else if (type === "call") {
|
||||||
|
isCallActive.value = false;
|
||||||
callStream.value = null;
|
callStream.value = null;
|
||||||
if (audioRef.value) {
|
if (audioRef.value) {
|
||||||
audioRef.value.srcObject = null;
|
audioRef.value.srcObject = null;
|
||||||
}
|
}
|
||||||
|
stopDurationTimer();
|
||||||
message.info("语音通话已结束");
|
message.info("语音通话已结束");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -147,6 +420,7 @@ const endDesktop = () => {
|
|||||||
|
|
||||||
const endCall = () => {
|
const endCall = () => {
|
||||||
peer.endMedia(props.peerId, "call");
|
peer.endMedia(props.peerId, "call");
|
||||||
|
stopDurationTimer();
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleCallMuted = () => {
|
const toggleCallMuted = () => {
|
||||||
@ -156,10 +430,8 @@ const toggleCallMuted = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 切换全屏
|
|
||||||
const toggleFullscreen = async () => {
|
const toggleFullscreen = async () => {
|
||||||
if (!videoContainer.value) return;
|
if (!videoContainer.value) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!isFullscreen.value) {
|
if (!isFullscreen.value) {
|
||||||
if (videoContainer.value.requestFullscreen) {
|
if (videoContainer.value.requestFullscreen) {
|
||||||
@ -170,12 +442,11 @@ const toggleFullscreen = async () => {
|
|||||||
await document.exitFullscreen();
|
await document.exitFullscreen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch {
|
||||||
message.error("切换全屏失败");
|
message.error("切换全屏失败");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 监听全屏变化
|
|
||||||
const handleFullscreenChange = () => {
|
const handleFullscreenChange = () => {
|
||||||
isFullscreen.value = !!document.fullscreenElement;
|
isFullscreen.value = !!document.fullscreenElement;
|
||||||
};
|
};
|
||||||
@ -186,132 +457,398 @@ const streamListener: EventListener = (event) => {
|
|||||||
const mediaEndedListener: EventListener = (event) => {
|
const mediaEndedListener: EventListener = (event) => {
|
||||||
handleMediaEnded(event as MediaEndedEvent);
|
handleMediaEnded(event as MediaEndedEvent);
|
||||||
};
|
};
|
||||||
|
const desktopStartedListener: EventListener = (event) => {
|
||||||
|
handleDesktopStarted(event as MediaStartedEvent);
|
||||||
|
};
|
||||||
|
const callStartedListener: EventListener = (event) => {
|
||||||
|
handleCallStarted(event as MediaStartedEvent);
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
peer.on("stream", streamListener);
|
peer.on("stream", streamListener);
|
||||||
|
peer.on("desktop-started", desktopStartedListener);
|
||||||
|
peer.on("call-started", callStartedListener);
|
||||||
peer.on("media-ended", mediaEndedListener);
|
peer.on("media-ended", mediaEndedListener);
|
||||||
document.addEventListener("fullscreenchange", handleFullscreenChange);
|
document.addEventListener("fullscreenchange", handleFullscreenChange);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
peer.off("stream", streamListener);
|
peer.off("stream", streamListener);
|
||||||
|
peer.off("desktop-started", desktopStartedListener);
|
||||||
|
peer.off("call-started", callStartedListener);
|
||||||
peer.off("media-ended", mediaEndedListener);
|
peer.off("media-ended", mediaEndedListener);
|
||||||
document.removeEventListener("fullscreenchange", handleFullscreenChange);
|
document.removeEventListener("fullscreenchange", handleFullscreenChange);
|
||||||
|
stopDurationTimer();
|
||||||
// 清理视频流
|
|
||||||
desktopStream.value?.getTracks().forEach((track) => track.stop());
|
desktopStream.value?.getTracks().forEach((track) => track.stop());
|
||||||
callStream.value?.getTracks().forEach((track) => track.stop());
|
callStream.value?.getTracks().forEach((track) => track.stop());
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<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;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
z-index: 100;
|
bottom: 0;
|
||||||
background: #000;
|
z-index: 1000;
|
||||||
transition: all 0.3s ease;
|
background: #0a0a0a;
|
||||||
height: 0;
|
display: flex;
|
||||||
overflow: hidden;
|
flex-direction: column;
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
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;
|
cursor: pointer;
|
||||||
user-select: none;
|
transition: all 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapse-header:hover {
|
.header-btn:hover {
|
||||||
background: #2a2a2a;
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: #fff;
|
||||||
|
border-color: rgba(255, 255, 255, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.desktop-view {
|
.header-btn.danger:hover {
|
||||||
width: 100%;
|
background: rgba(245, 34, 45, 0.15);
|
||||||
height: calc(100% - 40px);
|
border-color: #f5222d;
|
||||||
background: #000;
|
color: #f5222d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-body {
|
||||||
|
flex: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-container {
|
.video-wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
video {
|
video {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.controls {
|
.loading-state {
|
||||||
position: absolute;
|
|
||||||
bottom: 20px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
z-index: 10;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.3s;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
color: #888;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-container:hover .controls {
|
.spin-icon {
|
||||||
opacity: 1;
|
animation: spin 1.5s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.is-fullscreen {
|
@keyframes spin {
|
||||||
position: fixed;
|
to {
|
||||||
top: 0;
|
transform: rotate(360deg);
|
||||||
left: 0;
|
}
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
z-index: 9999;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.call-view {
|
/* Desktop Call Bar */
|
||||||
height: 64px;
|
.desktop-call-bar {
|
||||||
padding: 0 16px;
|
|
||||||
background: #111;
|
|
||||||
color: #fff;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
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-bar-info {
|
||||||
.call-controls {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
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;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.anticon) {
|
.bar-action-btn.is-muted {
|
||||||
vertical-align: 0;
|
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>
|
</style>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user