feat(dashboard1): 支持摄像头画面原生全屏

This commit is contained in:
2026-09-21 16:42:27 +08:00
parent 37c67ce25b
commit 6e31cc10f2
2 changed files with 127 additions and 5 deletions

View File

@@ -2599,13 +2599,85 @@
box-shadow: inset 0 0 0 1px rgba(62, 177, 248, 0.08);
}
// 视频绝对铺满容器:由 object-fit 精确控制缩放,避免高度百分比未解析导致的溢出裁切
.monitor-video {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
// contain:完整显示画面(比例不符时留边);如需铺满裁切改成 cover
object-fit: contain;
display: block;
// opacity: 0.72;
filter: saturate(0.85) contrast(1.05);
// filter: saturate(0.85) contrast(1.05);
}
// 全屏按钮与退出全屏按钮:公共外观
.video-fullscreen-btn,
.video-fullscreen-close {
position: absolute;
z-index: 6;
width: 30px;
height: 28px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid rgba(86, 205, 255, 0.22);
background: rgba(9, 43, 82, 0.78);
color: rgba(222, 249, 255, 0.92);
cursor: pointer;
backdrop-filter: blur(1px);
transition: border-color 0.2s ease, color 0.2s ease, box-shadow 0.2s ease;
&:hover {
border-color: rgba(122, 228, 255, 0.5);
color: #eefcff;
box-shadow: 0 0 12px rgba(77, 190, 255, 0.22);
}
}
// 进入全屏按钮:画面右下角
.video-fullscreen-btn {
right: 10px;
bottom: 10px;
}
// 退出全屏按钮:画面右上角
.video-fullscreen-close {
right: 10px;
top: 10px;
}
.video-fullscreen-icon {
width: 16px;
height: 16px;
}
// 浏览器原生全屏:容器铺满屏幕,画面按 contain 完整显示
.video-screen:fullscreen {
width: 100%;
height: 100%;
aspect-ratio: auto;
background: rgba(3, 12, 24, 0.99);
border: none;
}
// 全屏时叠加文字(右上角镜头标签、左下角编号)整体放大,字号随屏幕宽度自适应
.video-screen:fullscreen .video-overlay {
font-size: clamp(18px, 1.3vw, 36px);
letter-spacing: 1.2px;
}
// 镜头标签左移并下移,避免与右上角的退出按钮重叠
.video-screen:fullscreen .video-overlay.top-right {
top: 16px;
right: 64px;
}
.video-screen:fullscreen .video-overlay.bottom-left {
left: 24px;
bottom: 18px;
}
.scan-line {

View File

@@ -1372,6 +1372,36 @@ function closeMapPreview() {
mapPreviewVisible.value = false;
}
// 视频画面全屏:使用浏览器原生 Fullscreen API,进入浏览器顶层,避免被页面布局/包含块影响
const whiteScreenRef = ref<HTMLElement | null>(null);
const infraredScreenRef = ref<HTMLElement | null>(null);
const videoScreenRefs: Record<CameraChannelKey, Ref<HTMLElement | null>> = {
white: whiteScreenRef,
infrared: infraredScreenRef,
};
// 当前处于全屏的摄像头通道,null 表示未全屏
const videoFullscreenKey = ref<CameraChannelKey | null>(null);
// 进入/退出指定摄像头画面的全屏
async function toggleVideoFullscreen(key: CameraChannelKey) {
const element = videoScreenRefs[key].value;
if (!element) {
return;
}
if (document.fullscreenElement === element) {
await document.exitFullscreen();
return;
}
await element.requestFullscreen();
}
// 同步原生全屏状态,用于切换按钮图标
function handleVideoFullscreenChange() {
const current = document.fullscreenElement;
videoFullscreenKey.value =
cameraChannelKeys.find((key) => videoScreenRefs[key].value === current) ?? null;
}
// Esc 关闭预览
function handleMapPreviewKeydown(event: KeyboardEvent) {
if (event.key === "Escape" && mapPreviewVisible.value) {
@@ -1386,6 +1416,7 @@ onMounted(() => {
// startInspectionPolling();
window.addEventListener("blur", stopContinuousControl);
window.addEventListener("keydown", handleMapPreviewKeydown);
document.addEventListener("fullscreenchange", handleVideoFullscreenChange);
});
onBeforeUnmount(() => {
@@ -1395,6 +1426,7 @@ onBeforeUnmount(() => {
closeControlSocket();
window.removeEventListener("blur", stopContinuousControl);
window.removeEventListener("keydown", handleMapPreviewKeydown);
document.removeEventListener("fullscreenchange", handleVideoFullscreenChange);
});
// 切换机器人或视频地址变化时,两路摄像头各自重新拉流
@@ -1814,25 +1846,43 @@ function getStatusClass(status: DeviceItem["status"]) {
</div>
<div class="video-panel">
<!-- 白光镜头:16:9 -->
<div class="video-screen">
<div ref="whiteScreenRef" class="video-screen">
<video v-if="cameraUrls.white" ref="whiteVideoRef" :key="cameraUrls.white"
class="monitor-video" autoplay muted playsinline></video>
<div class="scan-line"></div>
<div class="video-overlay top-left">实时视频流</div>
<div class="video-overlay top-right">白光镜头</div>
<div class="video-overlay bottom-left">{{ currentRobot.id }}</div>
<!-- 全屏按钮:点击后进入/退出浏览器原生全屏 -->
<button v-if="videoFullscreenKey !== 'white'" class="video-fullscreen-btn" type="button" title="全屏"
aria-label="全屏" @click.stop="toggleVideoFullscreen('white')">
<svg class="video-fullscreen-icon" viewBox="0 0 24 24" aria-hidden="true">
<path d="M9 4H4v5M15 4h5v5M9 20H4v-5M15 20h5v-5" fill="none" stroke="currentColor"
stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
<button v-else class="video-fullscreen-close" type="button" title="退出全屏" aria-label="退出全屏"
@click.stop="toggleVideoFullscreen('white')">✕</button>
<div v-if="cameraLoading.white" class="video-empty">视频流连接中...</div>
<div v-else-if="cameraError.white" class="video-empty">{{ cameraError.white }}</div>
<div v-else-if="!cameraUrls.white" class="video-empty">当前机器人暂无白光视频地址</div>
</div>
<!-- 红外镜头:16:9 -->
<div class="video-screen">
<div ref="infraredScreenRef" class="video-screen">
<video v-if="cameraUrls.infrared" ref="infraredVideoRef" :key="cameraUrls.infrared"
class="monitor-video infrared" autoplay muted playsinline></video>
<div class="scan-line"></div>
<div class="video-overlay top-left">实时视频流</div>
<div class="video-overlay top-right">红外镜头</div>
<div class="video-overlay bottom-left">{{ currentRobot.id }}</div>
<!-- 全屏按钮:点击后进入/退出浏览器原生全屏 -->
<button v-if="videoFullscreenKey !== 'infrared'" class="video-fullscreen-btn" type="button" title="全屏"
aria-label="全屏" @click.stop="toggleVideoFullscreen('infrared')">
<svg class="video-fullscreen-icon" viewBox="0 0 24 24" aria-hidden="true">
<path d="M9 4H4v5M15 4h5v5M9 20H4v-5M15 20h5v-5" fill="none" stroke="currentColor"
stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</button>
<button v-else class="video-fullscreen-close" type="button" title="退出全屏" aria-label="退出全屏"
@click.stop="toggleVideoFullscreen('infrared')">✕</button>
<div v-if="cameraLoading.infrared" class="video-empty">视频流连接中...</div>
<div v-else-if="cameraError.infrared" class="video-empty">{{ cameraError.infrared }}</div>
<div v-else-if="!cameraUrls.infrared" class="video-empty">当前机器人暂无红外视频地址</div>