| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687 |
- <template>
- <Teleport to="body">
- <div v-if="state.open" class="video-monitor-backdrop">
- <section class="video-monitor" role="dialog" aria-modal="true" aria-labelledby="video-monitor-title" data-video-monitor>
- <header>
- <div><h2 id="video-monitor-title">{{ state.device?.deviceName || '视频监控设备' }}</h2><p>{{ state.device?.deviceCode }}</p></div>
- <button type="button" aria-label="关闭监控" @click="video.close">关闭</button>
- </header>
- <div class="video-monitor__tabs"><strong>实时视频</strong><button type="button" disabled title="历史录像能力尚未接入">历史录像</button></div>
- <p class="video-monitor__status" role="status">{{ labels[state.phase] || '未播放' }}</p>
- <div class="video-monitor__body">
- <aside><h3>云台控制</h3><button type="button" disabled title="停止保障尚未验证,不提供物理控制">方向/变倍</button><p>能力尚未开放</p></aside>
- <div class="video-monitor__screen">
- <VideoPlayer v-if="state.media" :media="state.media" :options="playerOptions" @ready="video.ready" @error="video.fail" />
- <p v-else>{{ state.error || '等待视频连接' }}</p>
- </div>
- </div>
- <p v-if="state.error && state.media" role="alert">{{ state.error }}</p>
- <p v-if="state.cleanupError" class="video-monitor__warning" role="alert">{{ state.cleanupError }}</p>
- <footer>
- <button v-if="!state.media && !['starting','connecting'].includes(state.phase)" type="button" :disabled="Boolean(state.cleanupError)" @click="video.retry">重新播放</button>
- <button v-if="state.cleanupError" type="button" @click="video.retryCleanup">重试清理</button>
- <span>默认静音 · 不提供截图、下载或录制</span>
- </footer>
- </section>
- </div>
- </Teleport>
- </template>
- <script setup>
- import { watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
- import { storeToRefs } from 'pinia'
- import { useVideoStore } from '../stores/videoStore.js'
- import { useUiStore } from '../stores/uiStore.js'
- import { useDetailStore } from '../stores/detailStore.js'
- import { useEnhancementDevicesStore } from '../stores/enhancementDevicesStore.js'
- import VideoPlayer from './VideoPlayer.vue'
- const video = useVideoStore(), ui = useUiStore(), detail = useDetailStore(), enhancement = useEnhancementDevicesStore()
- const { state, playerOptions } = storeToRefs(video)
- const labels = { idle: '未播放', starting: '正在建立视频会话', connecting: '等待视频画面', playing: '正在播放', failed: '播放失败' }
- watch(() => ui.mode, () => { void video.close() })
- watch(() => [state.value.ownerSource, detail.selection.deviceCode, enhancement.selected?.code], ([ownerSource, detailCode, enhancementCode]) => {
- const currentCode = ownerSource === 'enhancement' ? enhancementCode : detailCode
- if (state.value.open && currentCode !== state.value.device?.deviceCode) void video.close()
- })
- let trigger
- watch(() => state.value.open, async (open) => {
- if (!open) { if (trigger?.isConnected) trigger.focus?.(); return }
- trigger = document.activeElement
- await nextTick()
- if (state.value.open) document.querySelector('[data-video-monitor] [aria-label="关闭监控"]')?.focus()
- })
- const keyboard = (event) => {
- if (!state.value.open) return
- if (event.key === 'Escape') { event.preventDefault(); void video.close(); return }
- if (event.key !== 'Tab') return
- const items = Array.from(document.querySelectorAll('[data-video-monitor] button:not(:disabled), [data-video-monitor] video'))
- if (!items.length) return
- const index = items.indexOf(document.activeElement)
- if (index < 0 || (event.shiftKey && index === 0) || (!event.shiftKey && index === items.length - 1)) {
- event.preventDefault(); items[event.shiftKey ? items.length - 1 : 0].focus?.()
- }
- }
- const hidden = () => { if (document.hidden) void video.close() }
- const leave = () => { void video.close() }
- onMounted(() => { document.addEventListener('visibilitychange', hidden); document.addEventListener('keydown', keyboard); window.addEventListener('pagehide', leave) })
- onBeforeUnmount(() => { document.removeEventListener('visibilitychange', hidden); document.removeEventListener('keydown', keyboard); window.removeEventListener('pagehide', leave); void video.close() })
- </script>
- <style scoped>
- .video-monitor-backdrop { position: fixed; inset: 0; z-index: 15000; display: grid; place-items: center; padding: 24px; background: #0009; }
- .video-monitor { width: min(76vw, 1400px); max-height: 92vh; overflow: auto; color: #e0f6ff; background: #092635f5; border: 1px solid #3a91a5; border-radius: 8px; padding: 20px; box-shadow: 0 20px 90px #0009; font-family: sans-serif; }
- .video-monitor header { display: flex; justify-content: space-between; gap: 20px; align-items: start; }
- .video-monitor h2, .video-monitor h3 { margin: 0 0 8px; }
- .video-monitor header p { margin: 0; color: #9dbbc8; overflow-wrap: anywhere; }
- .video-monitor button { padding: 7px 18px; color: #d8f8ff; background: #14556b; border: 1px solid #448ba0; border-radius: 4px; cursor: pointer; }
- .video-monitor button:disabled { color: #8399a3; background: #263c46; border-color: #49616b; cursor: not-allowed; }
- .video-monitor__tabs { display: flex; gap: 20px; align-items: center; margin-top: 18px; }
- .video-monitor__status { color: #a3d5e3; }
- .video-monitor__body { display: grid; grid-template-columns: 150px minmax(0, 1fr); gap: 18px; }
- .video-monitor aside { color: #a7c3d0; background: #09212c; padding: 15px; }
- .video-monitor aside p, .video-monitor footer { font-size: 13px; color: #9cb9c5; }
- .video-monitor__screen { min-height: 300px; aspect-ratio: 16 / 9; display: grid; place-items: center; background: #02090e; }
- .video-monitor__screen > :deep(*) { width: 100%; height: 100%; }
- .video-monitor__screen > p { display: grid; place-items: center; margin: 0; }
- .video-monitor__warning { color: #ffc478; }
- .video-monitor footer { display: flex; gap: 16px; align-items: center; margin-top: 15px; }
- @media (max-width: 720px) { .video-monitor { width: 100%; } .video-monitor__body { grid-template-columns: 1fr; } .video-monitor aside { display: none; } }
- </style>
|