Jelajahi Sumber

Merge branch 'master' of gitee.com:loygra_network_chenrj/sg3dbackgroud

wenhongquan 1 bulan lalu
induk
melakukan
16b4cf220f

File diff ditekan karena terlalu besar
+ 0 - 0
public/jessibuca/decoder.js


TEMPAT SAMPAH
public/jessibuca/decoder.wasm


+ 88 - 0
public/jessibuca/jessibuca.d.ts

@@ -0,0 +1,88 @@
+declare namespace Jessibuca {
+
+    interface Config {
+        container: HTMLMediaElement | string
+
+        videoBuffer?: number,
+        decoder?: string,
+        forceNoOffscreen?: boolean,
+        hiddenAutoPause?: boolean,
+        hasAudio?: boolean,
+        rotate?: boolean,
+        isResize?: boolean,
+        isFullSize?: boolean,
+        isFlv?: boolean,
+        debug?: boolean,
+        timeout?: number,
+        supportDblclickFullscreen?: boolean,
+        showBandwidth?: boolean,
+        operateBtns?: {
+            fullscreen?: boolean,
+            screenshot?: boolean,
+            play?: boolean,
+            audio?: boolean
+            record?: boolean
+        }
+        keepScreenOn?: boolean,
+        isNotMute?: boolean,
+        loadingText?: boolean,
+        background?: string,
+        useWCS?: boolean,
+        useMSE?: boolean
+    }
+
+    interface JessibucaConstructor {
+        new(config?: Config): Jessibuca
+    }
+
+    interface Jessibuca {
+        constructor: JessibucaConstructor,
+
+        setDebug(flag: boolean): void,
+
+        mute(): void,
+
+        cancelMute(): void,
+
+        audioResume(): void,
+
+        setTimeout(): void,
+
+        setScaleMode(mode: number): void,
+
+        pause(): Promise<void>,
+
+        close(): void,
+
+        destroy(): void,
+
+        clearView(): void,
+
+        play(url?: string): Promise<void>,
+
+        resize(): void,
+
+        setBufferTime(time: number): void,
+
+        setRotate(deg: number): void,
+
+        setVolume(volume: number): void,
+
+        hasLoaded(): boolean,
+
+        setKeepScreenOn(): boolean,
+
+        setFullscreen(flag: boolean): void,
+
+        screenshot(filename?: string, format?: string, quality?: number, type?: string)
+
+        isPlaying(): boolean,
+
+        isMute(): boolean,
+
+        isRecording(): boolean,
+
+        on(event: string, callback: Function): void
+
+    }
+}

File diff ditekan karena terlalu besar
+ 0 - 0
public/jessibuca/jessibuca.js


File diff ditekan karena terlalu besar
+ 0 - 0
public/jessibuca/jessibuca.js.map


+ 167 - 30
src/App.vue

@@ -1,35 +1,171 @@
 <script setup>
-import { ref } from 'vue'
+import { ref, onMounted, onUnmounted } from 'vue'
 import TerrainViewer from './components/TerrainViewer.vue'
+import { config } from './config'
 
-const markers = ref([
-  {
-    lon: 113.722,
-    lat: 24.575,
-    title: '监控点 A',
-    type: 'video',
-    videoUrl: 'https://www.w3schools.com/html/mov_bbb.mp4'
-  },
-  {
-    lon: 113.730,
-    lat: 24.568,
-    title: '设备 B',
-    type: 'device',
-    status: '正常',
-    metrics: [
-      { label: '温度', value: 24, unit: '°C' },
-      { label: '湿度', value: 62, unit: '%' },
-      { label: '风速', value: 3.2, unit: 'm/s' }
-    ]
-  },
-  {
-    lon: 113.725,
-    lat: 24.572,
-    title: '报表 C',
-    type: 'html',
-    htmlContent: '<div style="color:#22c55e;font-weight:600">今日产量:1,240 吨</div><div style="margin-top:8px;color:rgba(255,255,255,0.6)">环比昨日 +5.3%</div>'
+const markers = ref([])
+
+// 获取设备数据
+async function fetchDeviceData() {
+  try {
+    const response = await fetch(config.getApiUrl('/service/page/node.json?paged=1&pageSize=100'), {
+      method: 'POST',
+      headers: config.getHeaders(),
+      body: JSON.stringify({})
+    })
+    
+    const result = await response.json()
+    
+    if (result.status === 2 && result.data?.data) {
+      const deviceMarkers = result.data.data
+        .filter(device => device.lonLat) // 过滤掉经纬度为空的设备
+        .map(device => {
+          const [lon, lat] = device.lonLat.split(',').map(Number)
+          const isOnline = device.iot_node_status === 16
+          
+          return {
+            lon,
+            lat,
+            title: device.name || device.scene_name,
+            type: 'device',
+            status: isOnline ? '在线' : '离线',
+            nodeId: device.id,
+            deviceCode: device.device_code,
+            deviceType: device.type,
+            metrics: [
+              
+            ]
+          }
+        })
+      
+      return deviceMarkers
+    }
+  } catch (error) {
+    console.error('获取设备数据失败:', error)
+  }
+  return []
+}
+
+// 获取视频数据
+async function fetchVideoData() {
+  try {
+    const response = await fetch(config.getApiUrl('/service/page/video.json?paged=1&pageSize=100'), {
+      method: 'POST',
+      headers: config.getHeaders(),
+      body: JSON.stringify({})
+    })
+    
+    const result = await response.json()
+    
+    if (result.status === 2 && result.data?.data) {
+      const videoMarkers = result.data.data
+        .filter(video => video.lonlat) // 过滤掉经纬度为空的视频
+        .map(video => {
+          const [lon, lat] = video.lonlat.split(',').map(Number)
+          const videoUrl = video.gb_url + '/rtp/' + video.device_serial + '_' + video.channel + '.live.flv'
+          
+          return {
+            lon,
+            lat,
+            title: video.name || video.scene_name,
+            type: 'video',
+            videoUrl,
+            deviceSerial: video.device_serial,
+            channel: video.channel
+          }
+        })
+      
+      return videoMarkers
+    }
+  } catch (error) {
+    console.error('获取视频数据失败:', error)
+  }
+  return []
+}
+
+// 加载所有数据
+async function loadAllData() {
+  const [deviceMarkers, videoMarkers] = await Promise.all([
+    fetchDeviceData(),
+    fetchVideoData()
+  ])
+  
+  markers.value = [...deviceMarkers, ...videoMarkers]
+  console.log('已加载设备数据:', deviceMarkers.length, '个,视频数据:', videoMarkers.length, '个')
+}
+
+// 获取传感器数据
+async function fetchSensorData(nodeId) {
+  try {
+    const response = await fetch(config.getApiUrl('/service/page/sensor.json?paged=1&pageSize=50'), {
+      method: 'POST',
+      headers: config.getHeaders(),
+      body: JSON.stringify({ node_id: nodeId })
+    })
+    
+    const result = await response.json()
+    
+    if (result.status === 2 && result.data?.data) {
+      return result.data.data.map(sensor => ({
+        name: sensor.name,
+        value: sensor.sdata,
+        unit: sensor.measure_unit || ''
+      }))
+    }
+  } catch (error) {
+    console.error('获取传感器数据失败:', error)
   }
-])
+  return []
+}
+
+// 接收父窗口消息的处理函数
+function handleMessage(event) {
+  // 安全检查:可以验证消息来源
+  // if (event.origin !== 'https://your-parent-domain.com') return
+
+  const { type, data } = event.data || {}
+
+  switch (type) {
+    case 'updateMarkers':
+      // 父窗口更新标记点数据
+      if (Array.isArray(data)) {
+        markers.value = data
+        console.log('已从父窗口接收标记点数据:', data)
+      }
+      break
+    case 'addMarker':
+      // 父窗口添加单个标记点
+      if (data) {
+        markers.value = [...markers.value, data]
+        console.log('已从父窗口添加标记点:', data)
+      }
+      break
+    case 'clearMarkers':
+      // 父窗口清空所有标记点
+      markers.value = []
+      console.log('已清空所有标记点')
+      break
+    case 'ping':
+      // 心跳检测,回复 pong
+      window.parent?.postMessage({ type: 'pong' }, '*')
+      break
+    default:
+      console.log('收到未知消息类型:', type, data)
+  }
+}
+
+onMounted(() => {
+  // 监听父窗口消息
+  window.addEventListener('message', handleMessage)
+  // 通知父窗口 iframe 已准备好
+  window.parent?.postMessage({ type: 'ready' }, '*')
+  // 加载所有数据
+  loadAllData()
+})
+
+onUnmounted(() => {
+  window.removeEventListener('message', handleMessage)
+})
 
 function onMarkerUpdate(newMarkers) {
   markers.value = newMarkers
@@ -37,8 +173,9 @@ function onMarkerUpdate(newMarkers) {
 
 function onMarkerClick(marker) {
   console.log('点击标记点:', marker)
-  // 可以发送消息给外部系统
-  window.parent?.postMessage({ type: 'markerClick', data: marker }, '*')
+  // 传感器数据已在 TerrainViewer 组件中获取
+  // TODO: 通知父窗口功能暂时隐藏
+  // window.parent?.postMessage({ type: 'markerClick', data: marker }, '*')
 }
 </script>
 

+ 147 - 7
src/components/MarkerPopup.vue

@@ -1,10 +1,15 @@
 <script setup>
-import { computed } from 'vue'
+import { computed, ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
 
 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 s = String(props.marker.status || '').toLowerCase()
   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'
   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>
 
 <template>
@@ -22,8 +124,13 @@ const statusColor = computed(() => {
     <div class="popup-body">
       <!-- 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
-          v-if="marker.videoUrl"
+          v-else-if="marker.videoUrl"
           class="popup-media"
           :src="marker.videoUrl"
           controls
@@ -63,9 +170,14 @@ const statusColor = computed(() => {
           <span class="status-dot" :style="{ background: statusColor }"></span>
           <span class="status-text">{{ marker.status || '未知' }}</span>
         </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
-            v-for="(m, i) in marker.metrics"
+            v-for="(m, i) in displayMetrics"
             :key="i"
             class="metric-item"
           >
@@ -133,6 +245,11 @@ const statusColor = computed(() => {
   display: block;
 }
 
+.jessibuca-container {
+  position: relative;
+  overflow: hidden;
+}
+
 .popup-html {
   font-size: 12px;
   color: rgba(255, 255, 255, 0.8);
@@ -146,6 +263,29 @@ const statusColor = computed(() => {
   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 {
   font-size: 13px;
   color: rgba(255, 255, 255, 0.75);
@@ -192,7 +332,7 @@ const statusColor = computed(() => {
 
 .metric-label {
   font-size: 12px;
-  color: rgba(255, 255, 255, 0.55);
+  color: rgba(255, 255, 255, 0.85);
 }
 
 .metric-value {
@@ -203,7 +343,7 @@ const statusColor = computed(() => {
 
 .metric-unit {
   font-size: 11px;
-  color: rgba(255, 255, 255, 0.5);
+  color: rgba(255, 255, 255, 0.85);
   margin-left: 2px;
 }
 </style>

+ 12 - 5
src/components/TerrainViewer.vue

@@ -2,6 +2,7 @@
 import { ref, onMounted, onUnmounted, watch } from 'vue'
 import * as Cesium from 'cesium'
 import MarkerPopup from './MarkerPopup.vue'
+import { config } from '../config'
 
 const props = defineProps({
   metaUrl: { type: String, default: '/terrain-meta.json' },
@@ -22,6 +23,7 @@ const loading = ref(true)
 const error = ref(null)
 const veModel = ref(props.verticalExaggeration)
 const infoText = ref('')
+const isAutoRotating = ref(false)
 
 const activeMarker = ref(null)
 const markerPopupPosition = ref({ x: 0, y: 0 })
@@ -413,6 +415,8 @@ function createMarkers() {
 
 function openMarkerPopup(marker) {
   activeMarker.value = marker
+  realtimeMetrics.value = []  // 清空之前的实时数据
+  loadingMetrics.value = false
   updatePopupPosition()
   sendToParent({
     type: 'markerClick',
@@ -522,6 +526,9 @@ async function loadAll() {
 
     await initViewer()
     loading.value = false
+    
+    // 加载完成后启动旋转动效
+    startAutoRotate()
   } catch (err) {
     console.error('加载失败:', err)
     error.value = '加载失败: ' + (err.message || err)
@@ -566,7 +573,7 @@ onUnmounted(() => {
   <div ref="container" class="tv">
     <div v-if="loading" class="ov ld">
       <div class="sp"></div>
-      <div style="margin-top:10px;color:#333">加载地形数据...</div>
+      <div style="margin-top:10px;color:#fff">加载地形数据...</div>
     </div>
     <div v-if="error" class="ov er">
       <div style="font-size:36px">⚠️</div>
@@ -589,7 +596,7 @@ onUnmounted(() => {
       @click.stop
     >
       <button class="popup-close" @click="activeMarker = null">×</button>
-      <MarkerPopup :marker="activeMarker" />
+      <MarkerPopup :marker="activeMarker" :realtimeMetrics="realtimeMetrics" :loadingMetrics="loadingMetrics" />
     </div>
   </div>
 </template>
@@ -604,9 +611,9 @@ onUnmounted(() => {
 .b { background:rgba(0,0,0,0.06); border:1px solid rgba(0,0,0,0.12); padding:4px 12px; border-radius:4px; cursor:pointer; font-size:12px; color:#333; }
 .b:hover { background:rgba(0,0,0,0.12); }
 .ov { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; z-index:100; }
-.ld { background:rgba(255,255,255,0.85); }
-.er { background:rgba(255,255,255,0.85); }
-.sp { width:40px; height:40px; border:3px solid #ddd; border-top-color:#409eff; border-radius:50%; animation:sp 0.8s linear infinite; }
+.ld { background:rgba(0,0,0,0.95); }
+.er { background:rgba(0,0,0,0.95); }
+.sp { width:40px; height:40px; border:3px solid rgba(255,255,255,0.2); border-top-color:#409eff; border-radius:50%; animation:sp 0.8s linear infinite; }
 @keyframes sp { to { transform:rotate(360deg) } }
 
 /* 标记点弹窗 */

+ 23 - 0
src/config.js

@@ -0,0 +1,23 @@
+// 全局配置文件
+export const config = {
+  // API 基础地址(自动根据浏览器访问地址获取)
+  get baseUrl() {
+    return `http://59.32.228.147:81`
+  },
+  
+  // 用户密钥
+  userKey: 'b01a6a38926a47c2b55fc364b9afeccd',
+  
+  // 获取完整的 API URL
+  getApiUrl(path) {
+    return `${this.baseUrl}${path}`
+  },
+  
+  // 获取请求头
+  getHeaders() {
+    return {
+      'Content-Type': 'application/json',
+      'USER-KEY': this.userKey
+    }
+  }
+}

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini