|
@@ -1,10 +1,15 @@
|
|
|
<script setup>
|
|
<script setup>
|
|
|
-import { computed } from 'vue'
|
|
|
|
|
|
|
+import { computed, ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
|
|
|
|
|
|
|
const props = defineProps({
|
|
const props = defineProps({
|
|
|
- marker: { type: Object, required: true }
|
|
|
|
|
|
|
+ marker: { type: Object, required: true },
|
|
|
|
|
+ realtimeMetrics: { type: Array, default: () => [] }, // 实时获取的 metrics
|
|
|
|
|
+ loadingMetrics: { type: Boolean, default: false } // 加载状态
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+const videoRef = ref(null)
|
|
|
|
|
+const jessibucaPlayer = ref(null)
|
|
|
|
|
+
|
|
|
const statusColor = computed(() => {
|
|
const statusColor = computed(() => {
|
|
|
const s = String(props.marker.status || '').toLowerCase()
|
|
const s = String(props.marker.status || '').toLowerCase()
|
|
|
if (['正常', '在线', '良好', '运行中', 'ok'].some(k => s.includes(k))) return '#22c55e'
|
|
if (['正常', '在线', '良好', '运行中', 'ok'].some(k => s.includes(k))) return '#22c55e'
|
|
@@ -12,6 +17,103 @@ const statusColor = computed(() => {
|
|
|
if (['故障', '离线', '错误', 'error', 'danger'].some(k => s.includes(k))) return '#ef4444'
|
|
if (['故障', '离线', '错误', 'error', 'danger'].some(k => s.includes(k))) return '#ef4444'
|
|
|
return '#9ca3af'
|
|
return '#9ca3af'
|
|
|
})
|
|
})
|
|
|
|
|
+
|
|
|
|
|
+const isFlvVideo = computed(() => {
|
|
|
|
|
+ return props.marker.type === 'video' && props.marker.videoUrl && props.marker.videoUrl.includes('.flv')
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// 使用实时 metrics 或原始 metrics
|
|
|
|
|
+const displayMetrics = computed(() => {
|
|
|
|
|
+ return props.realtimeMetrics.length > 0 ? props.realtimeMetrics : props.marker.metrics
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// 初始化 Jessibuca 播放器
|
|
|
|
|
+async function initJessibucaPlayer() {
|
|
|
|
|
+ if (!videoRef.value || !isFlvVideo.value) return
|
|
|
|
|
+
|
|
|
|
|
+ // 销毁旧的播放器
|
|
|
|
|
+ if (jessibucaPlayer.value) {
|
|
|
|
|
+ jessibucaPlayer.value.destroy()
|
|
|
|
|
+ jessibucaPlayer.value = null
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ await nextTick()
|
|
|
|
|
+
|
|
|
|
|
+ // 动态加载 Jessibuca
|
|
|
|
|
+ if (!window.Jessibuca) {
|
|
|
|
|
+ await loadJessibucaScript()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!window.Jessibuca) {
|
|
|
|
|
+ console.error('Jessibuca 加载失败')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ jessibucaPlayer.value = new window.Jessibuca({
|
|
|
|
|
+ container: videoRef.value,
|
|
|
|
|
+ videoBuffer: 0.2,
|
|
|
|
|
+ isResize: false,
|
|
|
|
|
+ loadingText: '加载中...',
|
|
|
|
|
+ hasAudio: true,
|
|
|
|
|
+ debug: false,
|
|
|
|
|
+ supportDblclickFullscreen: true,
|
|
|
|
|
+ showBandwidth: false,
|
|
|
|
|
+ operateBtns: {
|
|
|
|
|
+ fullscreen: true,
|
|
|
|
|
+ screenshot: false,
|
|
|
|
|
+ play: true,
|
|
|
|
|
+ audio: true,
|
|
|
|
|
+ ptz: false,
|
|
|
|
|
+ zoom: false
|
|
|
|
|
+ },
|
|
|
|
|
+ forceNoOffscreen: true,
|
|
|
|
|
+ isNotMute: false,
|
|
|
|
|
+ timeout: 10,
|
|
|
|
|
+ decoder: './jessibuca/decoder.js'
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ jessibucaPlayer.value.play(props.marker.videoUrl)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 动态加载 Jessibuca 脚本
|
|
|
|
|
+function loadJessibucaScript() {
|
|
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
|
|
+ if (window.Jessibuca) {
|
|
|
|
|
+ resolve()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const script = document.createElement('script')
|
|
|
|
|
+ script.src = './jessibuca/jessibuca.js'
|
|
|
|
|
+ script.onload = () => {
|
|
|
|
|
+ resolve()
|
|
|
|
|
+ }
|
|
|
|
|
+ script.onerror = () => {
|
|
|
|
|
+ reject(new Error('Jessibuca script load failed'))
|
|
|
|
|
+ }
|
|
|
|
|
+ document.head.appendChild(script)
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 监听 marker 变化,重新初始化播放器
|
|
|
|
|
+watch(() => props.marker, () => {
|
|
|
|
|
+ if (isFlvVideo.value) {
|
|
|
|
|
+ setTimeout(initJessibucaPlayer, 100)
|
|
|
|
|
+ }
|
|
|
|
|
+}, { immediate: true })
|
|
|
|
|
+
|
|
|
|
|
+onMounted(() => {
|
|
|
|
|
+ if (isFlvVideo.value) {
|
|
|
|
|
+ setTimeout(initJessibucaPlayer, 100)
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+onUnmounted(() => {
|
|
|
|
|
+ if (jessibucaPlayer.value) {
|
|
|
|
|
+ jessibucaPlayer.value.destroy()
|
|
|
|
|
+ jessibucaPlayer.value = null
|
|
|
|
|
+ }
|
|
|
|
|
+})
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<template>
|
|
<template>
|
|
@@ -22,8 +124,13 @@ const statusColor = computed(() => {
|
|
|
<div class="popup-body">
|
|
<div class="popup-body">
|
|
|
<!-- video -->
|
|
<!-- video -->
|
|
|
<template v-if="marker.type === 'video'">
|
|
<template v-if="marker.type === 'video'">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-if="marker.videoUrl && marker.videoUrl.includes('.flv')"
|
|
|
|
|
+ ref="videoRef"
|
|
|
|
|
+ class="popup-media jessibuca-container"
|
|
|
|
|
+ ></div>
|
|
|
<video
|
|
<video
|
|
|
- v-if="marker.videoUrl"
|
|
|
|
|
|
|
+ v-else-if="marker.videoUrl"
|
|
|
class="popup-media"
|
|
class="popup-media"
|
|
|
:src="marker.videoUrl"
|
|
:src="marker.videoUrl"
|
|
|
controls
|
|
controls
|
|
@@ -63,9 +170,14 @@ const statusColor = computed(() => {
|
|
|
<span class="status-dot" :style="{ background: statusColor }"></span>
|
|
<span class="status-dot" :style="{ background: statusColor }"></span>
|
|
|
<span class="status-text">{{ marker.status || '未知' }}</span>
|
|
<span class="status-text">{{ marker.status || '未知' }}</span>
|
|
|
</div>
|
|
</div>
|
|
|
- <div v-if="marker.metrics?.length" class="device-metrics">
|
|
|
|
|
|
|
+ <!-- 加载状态 -->
|
|
|
|
|
+ <div v-if="loadingMetrics" class="loading-metrics">
|
|
|
|
|
+ <div class="loading-spinner"></div>
|
|
|
|
|
+ <span>获取实时数据...</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-else-if="displayMetrics?.length" class="device-metrics">
|
|
|
<div
|
|
<div
|
|
|
- v-for="(m, i) in marker.metrics"
|
|
|
|
|
|
|
+ v-for="(m, i) in displayMetrics"
|
|
|
:key="i"
|
|
:key="i"
|
|
|
class="metric-item"
|
|
class="metric-item"
|
|
|
>
|
|
>
|
|
@@ -133,6 +245,11 @@ const statusColor = computed(() => {
|
|
|
display: block;
|
|
display: block;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+.jessibuca-container {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
.popup-html {
|
|
.popup-html {
|
|
|
font-size: 12px;
|
|
font-size: 12px;
|
|
|
color: rgba(255, 255, 255, 0.8);
|
|
color: rgba(255, 255, 255, 0.8);
|
|
@@ -146,6 +263,29 @@ const statusColor = computed(() => {
|
|
|
color: rgba(255, 255, 255, 0.4);
|
|
color: rgba(255, 255, 255, 0.4);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+.loading-metrics {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ padding: 24px 0;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: rgba(255, 255, 255, 0.6);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.loading-spinner {
|
|
|
|
|
+ width: 16px;
|
|
|
|
|
+ height: 16px;
|
|
|
|
|
+ border: 2px solid rgba(255, 255, 255, 0.2);
|
|
|
|
|
+ border-top-color: #409eff;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ animation: spin 0.8s linear infinite;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@keyframes spin {
|
|
|
|
|
+ to { transform: rotate(360deg); }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
.popup-text {
|
|
.popup-text {
|
|
|
font-size: 13px;
|
|
font-size: 13px;
|
|
|
color: rgba(255, 255, 255, 0.75);
|
|
color: rgba(255, 255, 255, 0.75);
|
|
@@ -192,7 +332,7 @@ const statusColor = computed(() => {
|
|
|
|
|
|
|
|
.metric-label {
|
|
.metric-label {
|
|
|
font-size: 12px;
|
|
font-size: 12px;
|
|
|
- color: rgba(255, 255, 255, 0.55);
|
|
|
|
|
|
|
+ color: rgba(255, 255, 255, 0.85);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.metric-value {
|
|
.metric-value {
|
|
@@ -203,7 +343,7 @@ const statusColor = computed(() => {
|
|
|
|
|
|
|
|
.metric-unit {
|
|
.metric-unit {
|
|
|
font-size: 11px;
|
|
font-size: 11px;
|
|
|
- color: rgba(255, 255, 255, 0.5);
|
|
|
|
|
|
|
+ color: rgba(255, 255, 255, 0.85);
|
|
|
margin-left: 2px;
|
|
margin-left: 2px;
|
|
|
}
|
|
}
|
|
|
</style>
|
|
</style>
|