Browse Source

feat: polish Monitor live player to match jcWeb prototype

Align LivePlayer frame chrome, live-dot header controls, and video fill CSS.
main
xiaosi 1 month ago
parent
commit
b41559b93e
  1. 73
      src/components/LivePlayer.vue
  2. 6
      src/styles/prototype.css
  3. 42
      src/views/MonitorView/MonitorView.vue

73
src/components/LivePlayer.vue

@ -1,27 +1,82 @@
<template> <template>
<div class="live-player">
<video v-if="playable" ref="video" controls autoplay playsinline :src="playUrl" @error="$emit('error')"></video>
<div v-else class="video-empty">
<span>{{ message }}</span>
<div
ref="frameEl"
class="video-frame"
:class="{ streaming: isLiveShell, playing: playable }"
>
<video
v-if="playable"
ref="videoEl"
controls
autoplay
playsinline
:src="playUrl"
@error="$emit('error')"
/>
<div v-else class="camera-scene">
<div class="horizon" />
<div class="dock-model"><span /><span /><i /></div>
</div> </div>
<div class="video-overlay">
<span>{{ cameraLabel }}</span>
<small>{{ qualityText }}</small>
</div>
<div v-if="statusMessage" class="video-status">{{ statusMessage }}</div>
<button
class="play-control"
type="button"
:aria-label="active ? '关闭实时画面' : '打开实时画面'"
@click="$emit('toggle')"
>
<svg><use :href="active ? '#i-stop' : '#i-play'" /></svg>
</button>
</div> </div>
</template> </template>
<script setup> <script setup>
import { computed } from 'vue'
import { computed, ref } from 'vue'
const props = defineProps({ const props = defineProps({
playUrl: { type: String, default: '' }, playUrl: { type: String, default: '' },
phase: { type: String, default: '' }
phase: { type: String, default: '' },
active: { type: Boolean, default: false },
cameraLabel: { type: String, default: '机巢摄像头' },
}) })
defineEmits(['error'])
defineEmits(['toggle', 'error'])
const frameEl = ref(null)
const videoEl = ref(null)
const playable = computed(() => props.playUrl && !props.playUrl.startsWith('fake://')) const playable = computed(() => props.playUrl && !props.playUrl.startsWith('fake://'))
const message = computed(() => {
const isLiveShell = computed(() => props.active && !playable.value)
const qualityText = computed(() => {
if (playable.value) return 'LIVE · 播放中'
if (props.playUrl?.startsWith('fake://')) return '控制面已验证'
if (props.phase === 'starting' || props.phase === 'reconnecting') return '启动中'
if (props.phase === 'failed') return '启动失败'
return 'H.264 · 720P'
})
const statusMessage = computed(() => {
if (playable.value) return ''
if (props.phase === 'starting' || props.phase === 'reconnecting') return '正在等待直播流就绪' if (props.phase === 'starting' || props.phase === 'reconnecting') return '正在等待直播流就绪'
if (props.phase === 'failed') return '直播启动失败' if (props.phase === 'failed') return '直播启动失败'
if (props.playUrl?.startsWith('fake://')) return '控制面已验证,未配置本地媒体流' if (props.playUrl?.startsWith('fake://')) return '控制面已验证,未配置本地媒体流'
return '暂无实时视频数据'
if (props.active) return '暂无实时视频数据'
return ''
}) })
function requestFullscreen() {
const el = playable.value ? videoEl.value : frameEl.value
if (!el?.requestFullscreen) return false
el.requestFullscreen()
return true
}
defineExpose({ requestFullscreen, frameEl, videoEl })
</script> </script>

6
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:hover { transform: translate(-50%, -50%) scale(1.06); }
.play-control svg { width: 17px; } .play-control svg { width: 17px; }
.video-frame.streaming .camera-scene { animation: camera-live 4s ease-in-out infinite alternate; } .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); } } @keyframes camera-live { to { transform: scale(1.035) translateX(-2px); } }
.text-button { color: var(--blue); background: transparent; font-size: 12px; padding: 0; } .text-button { color: var(--blue); background: transparent; font-size: 12px; padding: 0; }
.metric-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 7px; } .metric-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 7px; }

42
src/views/MonitorView/MonitorView.vue

@ -108,13 +108,22 @@
<section v-if="!isDrone" class="video-section"> <section v-if="!isDrone" class="video-section">
<div class="section-title"> <div class="section-title">
<h3>实时画面</h3> <h3>实时画面</h3>
<span class="video-unavailable">{{ livePhaseText }}</span>
</div>
<LivePlayer :play-url="live.playUrl" :phase="live.session?.phase" @error="ui.toast('视频播放失败')" />
<div class="video-actions">
<button v-if="!live.session" type="button" @click="openLive">打开实时画面</button>
<button v-else type="button" @click="closeLive">关闭实时画面</button>
<div>
<span class="live-dot" :class="{ active: !!live.session }" />
<b>{{ livePhaseText }}</b>
<button class="icon-button" type="button" title="全屏播放" :disabled="!canFullscreenLive" @click="fullscreenLive">
<svg><use href="#i-maximize" /></svg>
</button>
</div>
</div> </div>
<LivePlayer
ref="livePlayerRef"
:play-url="live.playUrl"
:phase="live.session?.phase"
:active="!!live.session"
@toggle="toggleLive"
@error="ui.toast('视频播放失败')"
/>
</section> </section>
<!-- Dock only: status metrics --> <!-- Dock only: status metrics -->
@ -255,6 +264,8 @@ const isAdmin = computed(() => userStore.isAdmin())
const selectedId = ref(null) const selectedId = ref(null)
const live = reactive({ session: null, dockId: '', playUrl: '', heartbeatTimer: null }) const live = reactive({ session: null, dockId: '', playUrl: '', heartbeatTimer: null })
const livePlayerRef = ref(null)
const filter = ref('all') const filter = ref('all')
const search = ref('') const search = ref('')
const controlExpanded = ref(false) 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 selectedDockEnvironment = computed(() => selectedDock.value?.environment || null)
const hasEnvironment = computed(() => Object.values(selectedDockEnvironment.value || {}).some((value) => value != null && value !== '')) const hasEnvironment = computed(() => Object.values(selectedDockEnvironment.value || {}).some((value) => value != null && value !== ''))
const livePhaseText = computed(() => { const livePhaseText = computed(() => {
if (!live.session) return '未打开'
if (!live.session) return '待机'
const phase = live.session.phase 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 === 'starting') return '启动中'
if (phase === 'reconnecting') return '重连中' if (phase === 'reconnecting') return '重连中'
if (phase === 'stopping') return '停止中' if (phase === 'stopping') return '停止中'
if (phase === 'failed') return '启动失败' if (phase === 'failed') return '启动失败'
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() { async function openLive() {
const dockId = selectedDock.value?.dockId const dockId = selectedDock.value?.dockId

Loading…
Cancel
Save