style(dashboard1): 优化大屏样式与地图交互
- 引入优设标题黑/圆字体并替换顶部标题为整图 - 统一页面边距与卡片背景为半透明渐变模糊 - 底部导航改用整图背景配透明热区 - 新增地图放大预览与机器人光圈扩散动画 - 视频监控改为白光/红外双路独立拉流
This commit is contained in:
@@ -1,6 +1,15 @@
|
||||
<script lang="ts" setup>
|
||||
import { NImage } from "naive-ui";
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import {
|
||||
computed,
|
||||
nextTick,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
reactive,
|
||||
ref,
|
||||
watch,
|
||||
type Ref,
|
||||
} from "vue";
|
||||
import {
|
||||
getRecentAlarmList,
|
||||
type RosInspectionRecordLineImage,
|
||||
@@ -20,10 +29,17 @@ import { getToken } from "@/utils/auth";
|
||||
import { parseRosMapYaml } from "@/utils/rosMapYaml";
|
||||
import { BASE_URL } from "@/utils/request";
|
||||
import { getRecentAlertPhoto } from "../shared/alertPhotos";
|
||||
// 地图上的机器人标记图标(设计稿 巡检机器人_03,8 倍图,放大时不易模糊)
|
||||
import robotIcon from "@/assets/robot-marker-8x.png";
|
||||
// 未配置地图时展示的默认隧道示意图
|
||||
import emptyMapImage from "@/assets/emptyMap.png";
|
||||
|
||||
const defaultProject = "西江隧道";
|
||||
|
||||
type DriveMode = "阿克曼" | "旋转" | "横移";
|
||||
// 视频监控的摄像头通道:白光镜头 / 红外镜头
|
||||
type CameraChannelKey = "white" | "infrared";
|
||||
const cameraChannelKeys: CameraChannelKey[] = ["white", "infrared"];
|
||||
type DeviceStatus = "空闲" | "巡检" | "离线" | "充电中";
|
||||
type ControlMessageType = "cmd_vel" | "ptz_control" | "lift_control";
|
||||
type PtzAction = "up" | "down" | "left" | "right" | "stop";
|
||||
@@ -517,7 +533,6 @@ const alertLoading = ref(false);
|
||||
const alertError = ref("");
|
||||
const alertTabs = ["局部高温", "电缆表面破损", "桥架断裂", "隧道积水", "墙体渗漏"] as const;
|
||||
const activeAlertTab = ref<(typeof alertTabs)[number]>("局部高温");
|
||||
const activeCameraMode = ref<"white" | "infrared">("white");
|
||||
const activeDeviceId = ref("");
|
||||
const driveModeTabs: DriveMode[] = ["阿克曼", "旋转", "横移"];
|
||||
const activeDriveMode = ref<DriveMode>("阿克曼");
|
||||
@@ -529,14 +544,33 @@ const selectedPresetPoint = ref("预置点1");
|
||||
const controlMode = ref<"人工接管" | "自主巡检">("自主巡检");
|
||||
const maxLiftHeight = 800;
|
||||
const targetLiftHeight = ref("1200");
|
||||
const monitorVideoRef = ref<HTMLVideoElement | null>(null);
|
||||
const navMapSvgRef = ref<SVGSVGElement | null>(null);
|
||||
const streamLoading = ref(false);
|
||||
const streamError = ref("");
|
||||
const controlWsStatus = ref("未连接");
|
||||
const controlError = ref("");
|
||||
let streamPeerConnection: RTCPeerConnection | null = null;
|
||||
let streamRequestId = 0;
|
||||
|
||||
// 视频监控同时展示白光/红外两路摄像头,各自持有独立的播放器与拉流状态
|
||||
const whiteVideoRef = ref<HTMLVideoElement | null>(null);
|
||||
const infraredVideoRef = ref<HTMLVideoElement | null>(null);
|
||||
const cameraVideoRefs: Record<CameraChannelKey, Ref<HTMLVideoElement | null>> = {
|
||||
white: whiteVideoRef,
|
||||
infrared: infraredVideoRef,
|
||||
};
|
||||
const cameraLoading = reactive<Record<CameraChannelKey, boolean>>({
|
||||
white: false,
|
||||
infrared: false,
|
||||
});
|
||||
const cameraError = reactive<Record<CameraChannelKey, string>>({
|
||||
white: "",
|
||||
infrared: "",
|
||||
});
|
||||
// WebRTC 连接对象不参与响应式代理,避免 Proxy 包裹导致原生方法调用异常
|
||||
const cameraStreams: Record<
|
||||
CameraChannelKey,
|
||||
{ requestId: number; peerConnection: RTCPeerConnection | null }
|
||||
> = {
|
||||
white: { requestId: 0, peerConnection: null },
|
||||
infrared: { requestId: 0, peerConnection: null },
|
||||
};
|
||||
let controlSocket: WebSocket | null = null;
|
||||
let controlRequestSeed = 0;
|
||||
let controlReconnectTimer: number | null = null;
|
||||
@@ -624,10 +658,11 @@ const currentDevices = computed(() => currentProject.value?.devices ?? []);
|
||||
const currentRobot = computed(
|
||||
() => currentDevices.value.find((device) => device.id === activeDeviceId.value) ?? currentDevices.value[0] ?? emptyDevice,
|
||||
);
|
||||
const currentMonitorVideo = computed(() => activeCameraMode.value === "white"
|
||||
? currentRobot.value.whiteLightUrl
|
||||
: currentRobot.value.infraredUrl,
|
||||
);
|
||||
// 白光/红外两路摄像头的当前播放地址
|
||||
const cameraUrls = computed(() => ({
|
||||
white: currentRobot.value.whiteLightUrl,
|
||||
infrared: currentRobot.value.infraredUrl,
|
||||
}));
|
||||
const currentScenario = computed(() => navScenario);
|
||||
const hasRobotPose = computed(
|
||||
() => typeof currentRobot.value.poseX === "number" && typeof currentRobot.value.poseY === "number",
|
||||
@@ -682,13 +717,10 @@ const robotMapPoint = computed(() => {
|
||||
currentRobot.value.poseYaw ?? 0,
|
||||
);
|
||||
});
|
||||
const robotMarkerScale = computed(() => {
|
||||
if (!rosMap.value) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 1 / mapScale.value;
|
||||
});
|
||||
// 机器人图标尺寸:宽度固定为地图点位直径的两倍(点位半径 3 → 直径 6 → 图标宽 12),
|
||||
// 与点位共用同一坐标系,缩放时与点保持固定比例
|
||||
const robotIconWidth = computed(() => (rosMap.value ? 12 : 28));
|
||||
const robotIconHeight = computed(() => robotIconWidth.value * (30 / 55));
|
||||
const mapContentTransform = computed(() => {
|
||||
if (!rosMap.value) {
|
||||
return "";
|
||||
@@ -1194,21 +1226,29 @@ function sendControlMessage(type: ControlMessageType, data: Record<string, strin
|
||||
socket.addEventListener("open", sendPayload, { once: true });
|
||||
}
|
||||
|
||||
// 停止当前 WebRTC 拉流并释放播放器资源
|
||||
function stopMonitorStream() {
|
||||
streamRequestId += 1;
|
||||
streamLoading.value = false;
|
||||
// 停止指定摄像头的 WebRTC 拉流并释放播放器资源
|
||||
function stopCameraStream(key: CameraChannelKey) {
|
||||
const stream = cameraStreams[key];
|
||||
stream.requestId += 1;
|
||||
cameraLoading[key] = false;
|
||||
|
||||
if (streamPeerConnection) {
|
||||
streamPeerConnection.close();
|
||||
streamPeerConnection = null;
|
||||
if (stream.peerConnection) {
|
||||
stream.peerConnection.close();
|
||||
stream.peerConnection = null;
|
||||
}
|
||||
|
||||
if (monitorVideoRef.value) {
|
||||
monitorVideoRef.value.srcObject = null;
|
||||
const video = cameraVideoRefs[key].value;
|
||||
|
||||
if (video) {
|
||||
video.srcObject = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 停止全部摄像头拉流
|
||||
function stopAllCameraStreams() {
|
||||
cameraChannelKeys.forEach((key) => stopCameraStream(key));
|
||||
}
|
||||
|
||||
// 将设备返回的 webrtc 播放地址转换为 ZLMediaKit WebRTC 播放接口地址
|
||||
function buildWebrtcApiUrl(streamUrl: string) {
|
||||
const normalizedUrl = streamUrl.replace(/^webrtc:\/\//, "https://");
|
||||
@@ -1235,33 +1275,36 @@ function waitForIceGatheringComplete(peerConnection: RTCPeerConnection) {
|
||||
});
|
||||
}
|
||||
|
||||
// 通过 WebRTC offer/answer 拉取当前机器人视频流
|
||||
async function playMonitorStream() {
|
||||
stopMonitorStream();
|
||||
streamError.value = "";
|
||||
// 通过 WebRTC offer/answer 拉取指定摄像头的视频流
|
||||
async function playCameraStream(key: CameraChannelKey) {
|
||||
stopCameraStream(key);
|
||||
cameraError[key] = "";
|
||||
|
||||
if (!currentMonitorVideo.value) {
|
||||
const streamUrl = cameraUrls.value[key];
|
||||
|
||||
if (!streamUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.RTCPeerConnection) {
|
||||
streamError.value = "当前浏览器不支持 WebRTC 播放";
|
||||
cameraError[key] = "当前浏览器不支持 WebRTC 播放";
|
||||
return;
|
||||
}
|
||||
|
||||
const requestId = streamRequestId;
|
||||
const stream = cameraStreams[key];
|
||||
const requestId = stream.requestId;
|
||||
const peerConnection = new RTCPeerConnection();
|
||||
streamPeerConnection = peerConnection;
|
||||
streamLoading.value = true;
|
||||
stream.peerConnection = peerConnection;
|
||||
cameraLoading[key] = true;
|
||||
|
||||
peerConnection.addTransceiver("video", { direction: "recvonly" });
|
||||
peerConnection.addTransceiver("audio", { direction: "recvonly" });
|
||||
peerConnection.ontrack = (event) => {
|
||||
if (requestId !== streamRequestId || !monitorVideoRef.value) {
|
||||
if (requestId !== stream.requestId || !cameraVideoRefs[key].value) {
|
||||
return;
|
||||
}
|
||||
|
||||
monitorVideoRef.value.srcObject = event.streams[0];
|
||||
cameraVideoRefs[key].value.srcObject = event.streams[0];
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -1269,7 +1312,7 @@ async function playMonitorStream() {
|
||||
await peerConnection.setLocalDescription(offer);
|
||||
await waitForIceGatheringComplete(peerConnection);
|
||||
|
||||
const response = await fetch(buildWebrtcApiUrl(currentMonitorVideo.value), {
|
||||
const response = await fetch(buildWebrtcApiUrl(streamUrl), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/sdp",
|
||||
@@ -1298,7 +1341,7 @@ async function playMonitorStream() {
|
||||
}
|
||||
}
|
||||
|
||||
if (requestId !== streamRequestId) {
|
||||
if (requestId !== stream.requestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1307,38 +1350,59 @@ async function playMonitorStream() {
|
||||
sdp: answer,
|
||||
});
|
||||
} catch (error) {
|
||||
if (requestId === streamRequestId) {
|
||||
streamError.value = error instanceof Error ? error.message : "视频流拉取失败";
|
||||
stopMonitorStream();
|
||||
if (requestId === stream.requestId) {
|
||||
cameraError[key] = error instanceof Error ? error.message : "视频流拉取失败";
|
||||
stopCameraStream(key);
|
||||
}
|
||||
} finally {
|
||||
if (requestId === streamRequestId) {
|
||||
streamLoading.value = false;
|
||||
if (requestId === stream.requestId) {
|
||||
cameraLoading[key] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 地图放大预览:把地图舞台切换为全屏浮层
|
||||
const mapPreviewVisible = ref(false);
|
||||
|
||||
function openMapPreview() {
|
||||
mapPreviewVisible.value = true;
|
||||
}
|
||||
|
||||
function closeMapPreview() {
|
||||
mapPreviewVisible.value = false;
|
||||
}
|
||||
|
||||
// Esc 关闭预览
|
||||
function handleMapPreviewKeydown(event: KeyboardEvent) {
|
||||
if (event.key === "Escape" && mapPreviewVisible.value) {
|
||||
closeMapPreview();
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchDeviceList();
|
||||
fetchRecentAlerts();
|
||||
connectControlSocket();
|
||||
// startInspectionPolling();
|
||||
window.addEventListener("blur", stopContinuousControl);
|
||||
window.addEventListener("keydown", handleMapPreviewKeydown);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
stopMonitorStream();
|
||||
stopAllCameraStreams();
|
||||
stopContinuousControl();
|
||||
// stopInspectionPolling();
|
||||
closeControlSocket();
|
||||
window.removeEventListener("blur", stopContinuousControl);
|
||||
window.removeEventListener("keydown", handleMapPreviewKeydown);
|
||||
});
|
||||
|
||||
// 切换机器人或视频地址变化时,两路摄像头各自重新拉流
|
||||
watch(
|
||||
currentMonitorVideo,
|
||||
cameraUrls,
|
||||
() => {
|
||||
nextTick(() => {
|
||||
playMonitorStream();
|
||||
cameraChannelKeys.forEach((key) => playCameraStream(key));
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -1365,10 +1429,6 @@ function switchAlertTab(tab: (typeof alertTabs)[number]) {
|
||||
fetchRecentAlerts();
|
||||
}
|
||||
|
||||
function switchCameraMode(mode: "white" | "infrared") {
|
||||
activeCameraMode.value = mode;
|
||||
}
|
||||
|
||||
// 点击设备列表中的机器人后,同步切换控制对象和监控视频源
|
||||
function selectDevice(device: DeviceItem) {
|
||||
activeDeviceId.value = device.id;
|
||||
@@ -1654,8 +1714,23 @@ function getStatusClass(status: DeviceItem["status"]) {
|
||||
</div>
|
||||
|
||||
<div class="middle">
|
||||
<div class="nav-map-panel">
|
||||
<div class="nav-map-stage">
|
||||
<!-- card- 前缀复用与其他面板一致的卡片标题样式 -->
|
||||
<div class="card-map nav-map-panel">
|
||||
<div class="title">管廊实时地图</div>
|
||||
<!-- 预览遮罩:点击空白处也可关闭 -->
|
||||
<div v-if="mapPreviewVisible" class="map-preview-mask" @click="closeMapPreview"></div>
|
||||
<div :class="['nav-map-stage', { 'preview-mode': mapPreviewVisible }]">
|
||||
<!-- 地图放大预览:点击后地图舞台切换为全屏浮层 -->
|
||||
<button v-if="!mapPreviewVisible" class="map-preview-btn" type="button" title="预览"
|
||||
aria-label="预览" @click.stop="openMapPreview">
|
||||
<svg class="map-preview-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="map-preview-close" type="button" @click.stop="closeMapPreview">
|
||||
✕
|
||||
</button>
|
||||
<svg v-if="rosMap" ref="navMapSvgRef" :class="['nav-map-svg', 'real-map-svg', { dragging: mapDragState.dragging }]" :viewBox="rosMap.viewBox" preserveAspectRatio="xMidYMid meet"
|
||||
@wheel="handleMapWheel" @pointerdown="startMapDrag" @pointermove="moveMapDrag" @pointerup="stopMapDrag"
|
||||
@pointerleave="stopMapDrag" @pointercancel="stopMapDrag">
|
||||
@@ -1667,11 +1742,17 @@ function getStatusClass(status: DeviceItem["status"]) {
|
||||
<feMergeNode in="SourceGraphic" />
|
||||
</feMerge>
|
||||
</filter>
|
||||
<!-- 路线遮罩:在巡检点位置挖空,避免路线伸进点位圆内 -->
|
||||
<mask id="routePointMask" maskUnits="userSpaceOnUse" x="0" y="0" :width="rosMap.width"
|
||||
:height="rosMap.height">
|
||||
<rect x="0" y="0" :width="rosMap.width" :height="rosMap.height" fill="#fff" />
|
||||
<circle v-for="(point, index) in realCheckPoints" :key="`route-mask-${index}`" :cx="point.x"
|
||||
:cy="point.y" r="5" fill="#000" />
|
||||
</mask>
|
||||
</defs>
|
||||
<g class="real-map-content" :transform="mapContentTransform">
|
||||
<image class="ros-map-image" :href="rosMap.imageUrl" x="0" y="0" :width="rosMap.width" :height="rosMap.height" />
|
||||
<path v-if="realRoutePath" class="route-glow" :d="realRoutePath" />
|
||||
<path v-if="realRoutePath" class="route-line" :d="realRoutePath" />
|
||||
<path v-if="realRoutePath" class="route-line" :d="realRoutePath" mask="url(#routePointMask)" />
|
||||
<g class="checkpoint-layer">
|
||||
<g v-for="(point, index) in realCheckPoints" :key="index" class="checkpoint"
|
||||
:transform="`translate(${point.x}, ${point.y})`">
|
||||
@@ -1680,50 +1761,35 @@ function getStatusClass(status: DeviceItem["status"]) {
|
||||
</g>
|
||||
</g>
|
||||
<g v-if="robotMapPoint" class="robot-layer real-robot-layer"
|
||||
:transform="`translate(${robotMapPoint.x}, ${robotMapPoint.y}) rotate(${robotMapPoint.angle}) scale(${robotMarkerScale})`">
|
||||
<circle class="robot-shadow" r="6.5" />
|
||||
<circle class="robot-body" r="5.8" />
|
||||
<path class="robot-head" d="M -1.6 -5.4 L 4.8 0 L -1.6 5.4 Z" />
|
||||
:transform="`translate(${robotMapPoint.x}, ${robotMapPoint.y}) rotate(${robotMapPoint.angle})`">
|
||||
<!-- 机器人光圈:由内向外扩散的三层圆环(基准半径约车位宽的 0.75 倍) -->
|
||||
<circle class="robot-ring ring-1" :r="robotIconWidth * 0.75" />
|
||||
<circle class="robot-ring ring-2" :r="robotIconWidth * 0.75" />
|
||||
<circle class="robot-ring ring-3" :r="robotIconWidth * 0.75" />
|
||||
<!-- 机器人标记:图标尺寸随地图分辨率等比,并随地图一起缩放 -->
|
||||
<image class="robot-icon" :href="robotIcon" :x="-robotIconWidth / 2" :y="-robotIconHeight / 2"
|
||||
:width="robotIconWidth" :height="robotIconHeight" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
<svg v-else class="nav-map-svg" viewBox="0 0 760 420" preserveAspectRatio="xMidYMid meet">
|
||||
<g class="grid-layer">
|
||||
<line v-for="line in 7" :key="`h-${line}`" :x1="40" :y1="line * 52" :x2="720" :y2="line * 52" />
|
||||
<line v-for="line in 12" :key="`v-${line}`" :x1="line * 56" :y1="40" :x2="line * 56" :y2="380" />
|
||||
</g>
|
||||
<path class="corridor-lane" :d="currentScenario.lanePath" />
|
||||
<path class="corridor-edge" :d="currentScenario.upperEdgePath" />
|
||||
<path class="corridor-edge" :d="currentScenario.lowerEdgePath" />
|
||||
<g class="wall-layer wall-upper">
|
||||
<circle v-for="(point, index) in currentScenario.upperWallPoints" :key="`upper-wall-${index}`"
|
||||
:cx="point.x" :cy="point.y" :r="index % 4 === 0 ? 3.4 : 2.4" />
|
||||
</g>
|
||||
<g class="wall-layer wall-lower">
|
||||
<circle v-for="(point, index) in currentScenario.lowerWallPoints" :key="`lower-wall-${index}`"
|
||||
:cx="point.x" :cy="point.y" :r="index % 5 === 0 ? 3.2 : 2.3" />
|
||||
</g>
|
||||
<path class="route-glow" :d="currentScenario.routePath" />
|
||||
<path class="route-line" :d="currentScenario.routePath" />
|
||||
<g class="checkpoint-layer">
|
||||
<g v-for="point in currentScenario.checkPoints" :key="point.label" class="checkpoint"
|
||||
:transform="`translate(${point.x}, ${point.y})`">
|
||||
<circle r="7"></circle>
|
||||
<text x="12" y="4">{{ point.label }}</text>
|
||||
</g>
|
||||
</g>
|
||||
<g class="robot-layer">
|
||||
<circle class="robot-shadow" r="18" cx="358" cy="258" />
|
||||
<circle class="robot-body" r="10" cx="358" cy="258" />
|
||||
<path class="robot-head" d="M 355 249 L 366 258 L 355 267 Z" />
|
||||
<!-- 未配置地图:使用默认隧道示意图,机器人叠加在图中车辆位置 -->
|
||||
<svg v-else class="nav-map-svg" viewBox="0 0 1398 467" preserveAspectRatio="xMidYMid meet">
|
||||
<image class="empty-map-image" :href="emptyMapImage" x="0" y="0" width="1398" height="467" />
|
||||
<g class="robot-layer" transform="translate(1216, 266)">
|
||||
<!-- 机器人光圈:由内向外扩散的三层圆环 -->
|
||||
<circle class="robot-ring ring-1" r="21" />
|
||||
<circle class="robot-ring ring-2" r="21" />
|
||||
<circle class="robot-ring ring-3" r="21" />
|
||||
<!-- 机器人标记使用设计稿图标(28x15) -->
|
||||
<image class="robot-icon" :href="robotIcon" x="-14" y="-8" width="28" height="15" />
|
||||
</g>
|
||||
</svg>
|
||||
<div v-if="mapLoading" class="map-state-tip">地图加载中...</div>
|
||||
<div v-else-if="mapError" class="map-state-tip">{{ mapError }}</div>
|
||||
<div v-else-if="routeLoading" class="map-state-tip">路线加载中...</div>
|
||||
<div v-else-if="routeError" class="map-state-tip">{{ routeError }}</div>
|
||||
<div class="nav-map-overlay top-left">ROS 实时地图</div>
|
||||
<div class="nav-map-overlay top-right">真实坐标路径跟踪</div>
|
||||
<!-- <div class="nav-map-overlay top-left">ROS 实时地图</div>
|
||||
<div class="nav-map-overlay top-right">真实坐标路径跟踪</div> -->
|
||||
<div v-if="rosMap" class="map-scale-controls">
|
||||
<button type="button" @click="changeMapScale(-0.05)">-</button>
|
||||
<span>{{ Math.round(mapScale * 100) }}%</span>
|
||||
@@ -1745,32 +1811,31 @@ function getStatusClass(status: DeviceItem["status"]) {
|
||||
<div class="card-1 video-card">
|
||||
<div class="card-header">
|
||||
<div class="title">视频监控</div>
|
||||
<div class="camera-switch header-switch">
|
||||
<button :class="['camera-btn', { active: activeCameraMode === 'white' }]" type="button"
|
||||
@click="switchCameraMode('white')">
|
||||
白光
|
||||
</button>
|
||||
<button :class="['camera-btn', { active: activeCameraMode === 'infrared' }]" type="button"
|
||||
@click="switchCameraMode('infrared')">
|
||||
红外
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="video-panel">
|
||||
<!-- 白光镜头:16:9 -->
|
||||
<div class="video-screen">
|
||||
<!-- <img src="/image.png" alt="" class="monitor-video"> -->
|
||||
<video v-if="currentMonitorVideo" ref="monitorVideoRef" :key="currentMonitorVideo"
|
||||
:class="['monitor-video', { infrared: activeCameraMode === 'infrared' }]" autoplay muted
|
||||
playsinline></video>
|
||||
<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">
|
||||
{{ activeCameraMode === "white" ? "白光镜头" : "红外镜头" }}
|
||||
</div>
|
||||
<div class="video-overlay top-right">白光镜头</div>
|
||||
<div class="video-overlay bottom-left">{{ currentRobot.id }}</div>
|
||||
<div v-if="streamLoading" class="video-empty">视频流连接中...</div>
|
||||
<div v-else-if="streamError" class="video-empty">{{ streamError }}</div>
|
||||
<div v-else-if="!currentMonitorVideo" 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="!cameraUrls.white" class="video-empty">当前机器人暂无白光视频地址</div>
|
||||
</div>
|
||||
<!-- 红外镜头:16:9 -->
|
||||
<div 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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user