feat(dashboard1): 支持摄像头画面原生全屏
This commit is contained in:
@@ -2599,13 +2599,85 @@
|
|||||||
box-shadow: inset 0 0 0 1px rgba(62, 177, 248, 0.08);
|
box-shadow: inset 0 0 0 1px rgba(62, 177, 248, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 视频绝对铺满容器:由 object-fit 精确控制缩放,避免高度百分比未解析导致的溢出裁切
|
||||||
.monitor-video {
|
.monitor-video {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
// contain:完整显示画面(比例不符时留边);如需铺满裁切改成 cover
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
display: block;
|
display: block;
|
||||||
// opacity: 0.72;
|
// 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 {
|
.scan-line {
|
||||||
|
|||||||
@@ -1372,6 +1372,36 @@ function closeMapPreview() {
|
|||||||
mapPreviewVisible.value = false;
|
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 关闭预览
|
// Esc 关闭预览
|
||||||
function handleMapPreviewKeydown(event: KeyboardEvent) {
|
function handleMapPreviewKeydown(event: KeyboardEvent) {
|
||||||
if (event.key === "Escape" && mapPreviewVisible.value) {
|
if (event.key === "Escape" && mapPreviewVisible.value) {
|
||||||
@@ -1386,6 +1416,7 @@ onMounted(() => {
|
|||||||
// startInspectionPolling();
|
// startInspectionPolling();
|
||||||
window.addEventListener("blur", stopContinuousControl);
|
window.addEventListener("blur", stopContinuousControl);
|
||||||
window.addEventListener("keydown", handleMapPreviewKeydown);
|
window.addEventListener("keydown", handleMapPreviewKeydown);
|
||||||
|
document.addEventListener("fullscreenchange", handleVideoFullscreenChange);
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
@@ -1395,6 +1426,7 @@ onBeforeUnmount(() => {
|
|||||||
closeControlSocket();
|
closeControlSocket();
|
||||||
window.removeEventListener("blur", stopContinuousControl);
|
window.removeEventListener("blur", stopContinuousControl);
|
||||||
window.removeEventListener("keydown", handleMapPreviewKeydown);
|
window.removeEventListener("keydown", handleMapPreviewKeydown);
|
||||||
|
document.removeEventListener("fullscreenchange", handleVideoFullscreenChange);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 切换机器人或视频地址变化时,两路摄像头各自重新拉流
|
// 切换机器人或视频地址变化时,两路摄像头各自重新拉流
|
||||||
@@ -1814,25 +1846,43 @@ function getStatusClass(status: DeviceItem["status"]) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="video-panel">
|
<div class="video-panel">
|
||||||
<!-- 白光镜头:16:9 -->
|
<!-- 白光镜头:16:9 -->
|
||||||
<div class="video-screen">
|
<div ref="whiteScreenRef" class="video-screen">
|
||||||
<video v-if="cameraUrls.white" ref="whiteVideoRef" :key="cameraUrls.white"
|
<video v-if="cameraUrls.white" ref="whiteVideoRef" :key="cameraUrls.white"
|
||||||
class="monitor-video" autoplay muted playsinline></video>
|
class="monitor-video" autoplay muted playsinline></video>
|
||||||
<div class="scan-line"></div>
|
<div class="scan-line"></div>
|
||||||
<div class="video-overlay top-left">实时视频流</div>
|
|
||||||
<div class="video-overlay top-right">白光镜头</div>
|
<div class="video-overlay top-right">白光镜头</div>
|
||||||
<div class="video-overlay bottom-left">{{ currentRobot.id }}</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-if="cameraLoading.white" class="video-empty">视频流连接中...</div>
|
||||||
<div v-else-if="cameraError.white" class="video-empty">{{ cameraError.white }}</div>
|
<div v-else-if="cameraError.white" class="video-empty">{{ cameraError.white }}</div>
|
||||||
<div v-else-if="!cameraUrls.white" class="video-empty">当前机器人暂无白光视频地址</div>
|
<div v-else-if="!cameraUrls.white" class="video-empty">当前机器人暂无白光视频地址</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 红外镜头:16:9 -->
|
<!-- 红外镜头:16:9 -->
|
||||||
<div class="video-screen">
|
<div ref="infraredScreenRef" class="video-screen">
|
||||||
<video v-if="cameraUrls.infrared" ref="infraredVideoRef" :key="cameraUrls.infrared"
|
<video v-if="cameraUrls.infrared" ref="infraredVideoRef" :key="cameraUrls.infrared"
|
||||||
class="monitor-video infrared" autoplay muted playsinline></video>
|
class="monitor-video infrared" autoplay muted playsinline></video>
|
||||||
<div class="scan-line"></div>
|
<div class="scan-line"></div>
|
||||||
<div class="video-overlay top-left">实时视频流</div>
|
|
||||||
<div class="video-overlay top-right">红外镜头</div>
|
<div class="video-overlay top-right">红外镜头</div>
|
||||||
<div class="video-overlay bottom-left">{{ currentRobot.id }}</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-if="cameraLoading.infrared" class="video-empty">视频流连接中...</div>
|
||||||
<div v-else-if="cameraError.infrared" class="video-empty">{{ cameraError.infrared }}</div>
|
<div v-else-if="cameraError.infrared" class="video-empty">{{ cameraError.infrared }}</div>
|
||||||
<div v-else-if="!cameraUrls.infrared" class="video-empty">当前机器人暂无红外视频地址</div>
|
<div v-else-if="!cameraUrls.infrared" class="video-empty">当前机器人暂无红外视频地址</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user