From b41559b93ed3daba1e9ce9b14965616bf46666f1 Mon Sep 17 00:00:00 2001 From: xiaosi <2652281683@qq.com> Date: Thu, 20 Aug 2026 16:52:38 +0800 Subject: [PATCH] feat: polish Monitor live player to match jcWeb prototype Align LivePlayer frame chrome, live-dot header controls, and video fill CSS. --- src/components/LivePlayer.vue | 73 +++++++++++++++++++++++---- src/styles/prototype.css | 6 +++ src/views/MonitorView/MonitorView.vue | 42 ++++++++++++--- 3 files changed, 104 insertions(+), 17 deletions(-) diff --git a/src/components/LivePlayer.vue b/src/components/LivePlayer.vue index 237c602..56bd063 100644 --- a/src/components/LivePlayer.vue +++ b/src/components/LivePlayer.vue @@ -1,27 +1,82 @@ diff --git a/src/styles/prototype.css b/src/styles/prototype.css index efc421a..dbeb190 100644 --- a/src/styles/prototype.css +++ b/src/styles/prototype.css @@ -210,6 +210,12 @@ svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: .play-control:hover { transform: translate(-50%, -50%) scale(1.06); } .play-control svg { width: 17px; } .video-frame.streaming .camera-scene { animation: camera-live 4s ease-in-out infinite alternate; } +.video-frame > video { position: absolute; inset: 0; z-index: 1; width: 100%; height: 100%; object-fit: cover; background: #000; border: 0; } +.video-frame.playing .camera-scene { display: none; } +.video-frame.playing .play-control { opacity: 0; pointer-events: none; transition: opacity .15s ease; } +.video-frame.playing:hover .play-control { opacity: 1; pointer-events: auto; } +.video-status { position: absolute; z-index: 5; left: 50%; bottom: 12px; max-width: calc(100% - 24px); padding: 5px 9px; border-radius: 4px; color: #fff; background: rgba(13,28,35,.72); font-size: 11px; text-align: center; transform: translateX(-50%); pointer-events: none; } +.section-title .icon-button:disabled { opacity: .35; cursor: not-allowed; } @keyframes camera-live { to { transform: scale(1.035) translateX(-2px); } } .text-button { color: var(--blue); background: transparent; font-size: 12px; padding: 0; } .metric-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 7px; } diff --git a/src/views/MonitorView/MonitorView.vue b/src/views/MonitorView/MonitorView.vue index 2846b83..2e56c5d 100644 --- a/src/views/MonitorView/MonitorView.vue +++ b/src/views/MonitorView/MonitorView.vue @@ -108,13 +108,22 @@

实时画面

- {{ livePhaseText }} -
- -
- - +
+ + {{ livePhaseText }} + +
+
@@ -255,6 +264,8 @@ const isAdmin = computed(() => userStore.isAdmin()) const selectedId = ref(null) const live = reactive({ session: null, dockId: '', playUrl: '', heartbeatTimer: null }) +const livePlayerRef = ref(null) + const filter = ref('all') const search = ref('') const controlExpanded = ref(false) @@ -475,15 +486,30 @@ const selectedDock = computed(() => (isDrone.value ? devices.docks.find((d) => d const selectedDockEnvironment = computed(() => selectedDock.value?.environment || null) const hasEnvironment = computed(() => Object.values(selectedDockEnvironment.value || {}).some((value) => value != null && value !== '')) const livePhaseText = computed(() => { - if (!live.session) return '未打开' + if (!live.session) return '待机' const phase = live.session.phase - if (phase === 'streaming') return live.playUrl.startsWith('fake://') ? '模拟直播已就绪' : '直播中' + if (phase === 'streaming') return live.playUrl.startsWith('fake://') ? '模拟直播' : '直播中' if (phase === 'starting') return '启动中' if (phase === 'reconnecting') return '重连中' if (phase === 'stopping') return '停止中' if (phase === 'failed') return '启动失败' return '已停止' }) +const canFullscreenLive = computed(() => !!live.playUrl && !live.playUrl.startsWith('fake://')) + +function toggleLive() { + if (live.session) closeLive() + else openLive() +} + +function fullscreenLive() { + if (!canFullscreenLive.value) { + ui.toast('当前无可全屏播放的视频流') + return + } + const ok = livePlayerRef.value?.requestFullscreen?.() + if (!ok) ui.toast('当前浏览器不支持全屏') +} async function openLive() { const dockId = selectedDock.value?.dockId