dedsudiyu il y a 1 semaine
Parent
commit
205166bfaf

+ 9 - 0
.claude/settings.local.json

@@ -0,0 +1,9 @@
+{
+  "permissions": {
+    "allow": [
+      "Bash(npm install:*)",
+      "mcp__ide__getDiagnostics",
+      "Bash(npm run:*)"
+    ]
+  }
+}

Fichier diff supprimé car celui-ci est trop grand
+ 5289 - 2327
package-lock.json


+ 5 - 0
package.json

@@ -9,9 +9,11 @@
   "dependencies": {
     "axios": "^0.27.2",
     "core-js": "^3.25.0",
+    "crypto-js": "^4.2.0",
     "echarts": "^5.4.3",
     "element-ui": "^2.15.14",
     "js-md5": "^0.8.3",
+    "mqtt": "^4.3.7",
     "vue": "^2.7.14",
     "vue-router": "^3.6.5",
     "vuex": "^3.6.2"
@@ -21,8 +23,11 @@
     "@vue/cli-plugin-router": "~5.0.8",
     "@vue/cli-plugin-vuex": "~5.0.8",
     "@vue/cli-service": "~5.0.8",
+    "buffer": "^6.0.3",
+    "process": "^0.11.10",
     "sass": "~1.55.0",
     "sass-loader": "^13.1.0",
+    "url": "^0.11.4",
     "vue-template-compiler": "^2.7.14"
   },
   "browserslist": [

+ 106 - 0
src/App.vue

@@ -5,18 +5,44 @@
         <router-view />
       </div>
     </div>
+    <AlertModal ref="alertModal" />
   </div>
 </template>
 
 <script>
+import AlertModal from '@/components/AlertModal.vue'
+import mqtt from 'mqtt'
+import { getMqttConfig, getTriggerInfo } from '@/api'
+import { Decrypt } from '@/utils/decrypt'
+
 export default {
   name: 'App',
+  components: { AlertModal },
+  data() {
+    return { mqttClient: null }
+  },
+  computed: {
+    token() { return this.$store.state.token }
+  },
+  watch: {
+    token(val) {
+      if (val) {
+        this.initMqttConfig().then(() => this.connectMqtt())
+      } else {
+        if (this.mqttClient) { this.mqttClient.end(); this.mqttClient = null }
+      }
+    }
+  },
   mounted() {
     this.fitScreen()
     window.addEventListener('resize', this.fitScreen)
+    if (this.$store.state.token) {
+      this.initMqttConfig().then(() => this.connectMqtt())
+    }
   },
   beforeDestroy() {
     window.removeEventListener('resize', this.fitScreen)
+    if (this.mqttClient) this.mqttClient.end()
   },
   methods: {
     fitScreen() {
@@ -26,6 +52,86 @@ export default {
       const vw = window.innerWidth, vh = window.innerHeight
       const sx = vw / sw, sy = vh / sh
       scr.style.transform = `translate(-50%, -50%) scale(${sx}, ${sy})`
+    },
+    async initMqttConfig() {
+      try {
+        const res = await getMqttConfig()
+        const config = JSON.parse(res.data.configValue)
+        let urlText = window.location.href.split('://')[0]+'://';
+        let outerNet = window.location.href.indexOf(config.ipIdentify) == -1;
+        if(outerNet){//外网
+          localStorage.setItem('mqttUrl', 'wss://'+Decrypt(config.mqttExtranetUrl))
+          localStorage.setItem('mqttUser', Decrypt(config.mqttExtranetUser))
+          localStorage.setItem('mqttPassword', Decrypt(config.mqttExtranetPassword))
+        }else{
+          localStorage.setItem('mqttUrl', 'ws://'+Decrypt(config.mqttIntranetUrl))
+          localStorage.setItem('mqttUser', Decrypt(config.mqttIntranetUser))
+          localStorage.setItem('mqttPassword', Decrypt(config.mqttIntranetPassword))
+        }
+      } catch (e) {
+        console.error('获取MQTT配置失败:', e)
+      }
+    },
+    connectMqtt() {
+      const url = localStorage.getItem('mqttUrl')
+      const username = localStorage.getItem('mqttUser')
+      const password = localStorage.getItem('mqttPassword')
+      if (!url) return
+      if (!/^(ws|wss|mqtt|mqtts):\/\//.test(url)) {
+        console.error('MQTT URL 格式错误,缺少协议头:', url)
+        return
+      }
+
+      this.mqttClient = mqtt.connect(url, { username, password })
+
+      this.mqttClient.on('connect', () => {
+        // this.mqttClient.subscribe('#')
+        this.mqttClient.subscribe('lab/risk/plan/change', (err) => {
+          if (!err) {
+            console.log("预案-订阅成功:lab/risk/plan/change");
+          }else{
+            // console.log("预案-连接错误:" + err);
+          }
+        });
+      })
+
+      this.mqttClient.on('message', async (topic, message) => {
+        try {
+          const payload = JSON.parse(message.toString())
+          console.log('payload',payload);
+          if (payload) {
+            const res = await getTriggerInfo()
+            const list = res.data || []
+            if (list.length > 0) {
+              const e = list[0]
+              const obj = JSON.parse(e.triggerUploadData)[0]
+              const deviceValue = obj.deviceValue
+              const unit = obj.unit
+              this.$refs.alertModal.show({
+                lab: e.subName + (e.roomNum ? `(${e.roomNum})` : ''),
+                building: [e.schoolName, e.buildName, e.floorName].filter(Boolean).join(' · '),
+                unit: e.deptName,
+                param: e.eventName,
+                value: deviceValue+''+(unit=='-'?'':unit),
+                person: e.adminName,
+                phone: e.adminPhone,
+                time: e.eventStartTime ? e.eventStartTime.replace('T', ' ') : '',
+                buildId:e.buildId,
+                floorId:e.floorId,
+                subId:e.subId,
+              })
+            }else{
+              this.$refs.alertModal.close();
+            }
+          }
+        } catch (e) {
+          console.error('MQTT消息处理失败:', e)
+        }
+      })
+
+      this.mqttClient.on('error', (err) => {
+        console.error('MQTT连接错误:', err)
+      })
     }
   }
 }

+ 54 - 12
src/api/index.js

@@ -25,23 +25,20 @@ function clearLabBasicCache() {
 }
 
 export async function getLabBasicStats() {
-  const raw = await fetchLabBasic()
+  const [raw, levelCfg] = await Promise.all([fetchLabBasic(), getLevelTitleConfig()])
   clearLabBasicCache()
+  const counts = [raw.levelOneCount, raw.levelTwoCount, raw.levelThreeCount, raw.levelFourCount]
+  const levels = levelCfg.map((l, i) => ({ name: l.name, value: counts[i] || 0, color: l.color }))
   return wrapResult({
     totalLabs: raw.labTotal,
-    levels: [
-      { name: 'I级(红)', value: raw.levelOneCount, color: '#f44336' },
-      { name: 'II级(橙)', value: raw.levelTwoCount, color: '#ff9800' },
-      { name: 'III级(黄)', value: raw.levelThreeCount, color: '#ffc107' },
-      { name: 'IV级(蓝)', value: raw.levelFourCount, color: '#2196f3' }
-    ],
+    levels,
     usage: { using: raw.useTotal, abnormal: raw.exceptionalTotal, idle: raw.availableTotal }
   })
 }
 
 // ======================== 2. 实验室安全分级统计(复用 labBasicStatistics 数据)========================
 export async function getSafetyLevelStats() {
-  const raw = await fetchLabBasic()
+  const [raw, levelCfg] = await Promise.all([fetchLabBasic(), getLevelTitleConfig()])
   clearLabBasicCache()
   const departments = (raw.labLevelVOList || []).map(d => ({
     name: d.deptName,
@@ -51,7 +48,7 @@ export async function getSafetyLevelStats() {
     level3: d.levelThreeCount || 0,
     level4: d.levelFourCount || 0
   }))
-  return wrapResult({ departments })
+  return wrapResult({ departments, levelCfg })
 }
 
 // ======================== 3. 实验室进入人数统计及走势 ========================
@@ -168,14 +165,14 @@ function transformTree(nodes) {
 
 export async function getBuildingTree(params = {}) {
   const res = await request.post('/laboratory/labScreen/monitorTree', params)
-  if (res.code === 200 && res.data) return wrapResult({ tree: transformTree(res.data) })
+  if (res.code === 200 && res.data) return wrapResult(res.data)
   throw new Error(res.message || '接口异常')
 }
 
 // ======================== 9. 视频列表 ========================
 export async function getVideoList(params = {}) {
   const res = await request.post('/laboratory/labScreen/cameraStream', {
-    subIds: params.subIds || [], streamType: 0, protocol: 'ws', source: 3,
+    subIds: params.subIds || [], streamType: 0, protocol: params.protocol || 'ws', source: params.source || 3,
     page: params.page || 1, pageSize: params.pageSize || 9
   })
   if (res.code === 200 && res.data && res.data.records) {
@@ -189,6 +186,37 @@ export async function getVideoList(params = {}) {
   }
   throw new Error(res.message || '接口异常')
 }
+// ======================== 9.1 根据设备ID查询视频流 ========================
+
+
+export async function iotCameraGetPreviewURLs(params = {}) {
+  const res = await request.get('/iot/camera/getPreviewURLs',{params})
+  if (res.code === 200 && res.data) return wrapResult(res.data)
+  throw new Error(res.message || '接口异常')
+}
+
+// ======================== 13. 实验室分级配置 ========================
+let _levelConfigCache = null
+export async function getLevelTitleConfig() {
+  if (_levelConfigCache) return _levelConfigCache
+  const res = await request.post('/laboratory/labScreen/getLevelTitleList')
+  if (res.code === 200 && res.data) {
+    // 按 levelGrade 排序,返回 [{grade, name, color}]
+    _levelConfigCache = res.data
+      .sort((a, b) => a.levelGrade - b.levelGrade)
+      .map(l => ({ grade: l.levelGrade, name: l.titleName, color: l.titleColor }))
+    setTimeout(() => { _levelConfigCache = null }, 5 * 60 * 1000)
+    return _levelConfigCache
+  }
+  throw new Error(res.message || '接口异常')
+}
+
+// ======================== 系统配置接口 ========================
+export async function getMqttConfig() {
+  const res = await request.post('/system/config/info/getConfigByType', { category: 2, configType: 5 })
+  if (res.code === 200 && res.data) return wrapResult(res.data)
+  throw new Error(res.message || '接口异常')
+}
 
 // ======================== 登录接口 ========================
 export async function loginApi(params) {
@@ -199,7 +227,21 @@ export async function loginApi(params) {
 
 // ======================== 7. 风险预警-触发预案信息 ========================
 export async function getTriggerInfo() {
-  const res = await request.get('/laboratory/large/selectTriggerInfo')
+  const res = await request.get('/laboratory/labScreen/selectTriggerInfo')
   if (res.code === 200 && res.data) return wrapResult(res.data)
   throw new Error(res.message || '接口异常')
 }
+
+// ======================== 12. 根据条件查询楼道实验室信息 ========================
+export async function laboratoryLabScreenRooms(params) {
+  const res = await request.post('/laboratory/labScreen/rooms', params)
+  if (res.code === 200 && res.data) return wrapResult(res.data)
+  throw new Error(res.message || res.msg || '接口异常')
+}
+
+// ======================== 11. 二级学院下拉列表 ========================
+export async function laboratoryLabScreenDeptDropList(params) {
+  const res = await request.post('/laboratory/labScreen/deptDropList', params)
+  if (res.code === 200 && res.data) return wrapResult(res.data)
+  throw new Error(res.message || res.msg || '接口异常')
+}

BIN
src/assets/icon_0.png


BIN
src/assets/icon_1.png


+ 53 - 5
src/components/AlertModal.vue

@@ -51,10 +51,11 @@
                 <span class="rec-indicator"><span class="rec-dot"></span> REC</span>
                 <span class="rec-name">{{ info.lab || '' }}</span>
               </div>
-              <div class="video-placeholder">
+              <div class="video-placeholder" v-if="!videoData">
                 <i class="el-icon-video-camera"></i>
                 <span>实时监控画面</span>
               </div>
+              <H5PlayerVideo style="margin:10px 0 0 12px;" v-if="videoData" :videoProps="videoData"></H5PlayerVideo>
             </div>
           </div>
         </div>
@@ -63,16 +64,25 @@
         </div>
       </div>
     </div>
+    <fullH5PlayerVideo v-if="fullVideoType" :fullVideoProps="fullVideoProps"></fullH5PlayerVideo>
   </transition>
 </template>
 
 <script>
+import H5PlayerVideo from '@/components/H5PlayerVideo/H5PlayerVideo.vue'
+import fullH5PlayerVideo from '@/components/fullH5PlayerVideo/fullH5PlayerVideo.vue'
+import { getVideoList } from '@/api'
 export default {
   name: 'AlertModal',
+  components: { H5PlayerVideo,fullH5PlayerVideo },
   data() {
     return {
       visible: false,
-      info: {}
+      info: {},
+      //全屏视频参数
+      videoData:null,
+      fullVideoProps:{},
+      fullVideoType:false,
     }
   },
   methods: {
@@ -85,11 +95,47 @@ export default {
       }
       this.info = d
       this.visible = true
+      console.log(d)
+      this.getVideoList(d);
     },
     close() {
       this.visible = false
       this.$emit('close')
-    }
+    },
+    async getVideoList(data) {
+      console.log(data)
+      try {
+        let obj = {
+          "subIds": [data.subId],
+          "streamType": 1,
+          "protocol": window.location.href.indexOf('https') !== -1 ? 'wss' : 'ws',
+          "source": 4,
+          "page": 1,
+          "pageSize": 10
+        }
+        const res = await getVideoList(obj)
+        if(res.data.list[0]){
+          this.$set(this,'videoData',{
+            width: 400, //(宽度:非必传-默认600)
+            height: 210, //(高度:非必传-默认338)
+            url: res.data.list[0].streamUrl,
+            cameraIndexCode: res.data.list[0].deviceNo,
+          });
+        }
+      } catch (e) {
+        console.error('AlertModal:', e)
+      }
+    },
+    //全屏开启
+    stopTime(cameraIndexCode){
+      this.$set(this,'fullVideoProps',{cameraIndexCode:cameraIndexCode});
+      this.$set(this,'fullVideoType',true);
+    },
+    //全屏关闭
+    outFullScreen(){
+      this.$set(this,'fullVideoType',false);
+      this.$set(this,'fullVideoProps',{});
+    },
   }
 }
 </script>
@@ -119,7 +165,7 @@ export default {
 }
 
 .alert-modal {
-  width: 750px;
+  width: 900px;
   background: rgba(18, 6, 10, 0.97);
   border: 1px solid rgba(255, 50, 50, 0.4);
   border-radius: 4px;
@@ -161,7 +207,7 @@ export default {
 }
 
 .alert-right {
-  width: 280px;
+  width: 400px;
   flex-shrink: 0;
 }
 
@@ -216,9 +262,11 @@ export default {
   border-radius: 4px;
   overflow: hidden;
   background: rgba(0, 0, 0, 0.3);
+  position: relative;
 }
 
 .video-header {
+  position: absolute;
   display: flex;
   align-items: center;
   gap: 8px;

+ 1 - 1
src/components/H5PlayerVideo/H5PlayerVideo.vue

@@ -185,7 +185,7 @@
     }
     position: relative;
     .full-screen-button {
-      background: url("../../assets/ZDimages/basicsModules/icon_0.png");
+      background: url("../../assets/icon_0.png");
       background-size: 100%;
       position: absolute;
       font-size: 20px;

+ 21 - 19
src/components/LabStats/SafetyLevel.vue

@@ -31,14 +31,13 @@ export default {
     async loadData() {
       try {
         const res = await getSafetyLevelStats()
+        const { departments, levelCfg } = res.data
         if (this.chart) {
-          const depts = res.data.departments
-          this.chart.setOption({ xAxis: { data: depts.map(d => d.name) }, series: [
-            { data: depts.map(d => d.level1) },
-            { data: depts.map(d => d.level2) },
-            { data: depts.map(d => d.level3) },
-            { data: depts.map(d => d.level4) }
-          ]})
+          this.chart.setOption({
+            legend: { data: levelCfg.map(l => l.name) },
+            xAxis: { data: departments.map(d => d.name) },
+            series: levelCfg.map((l, i) => ({ name: l.name, data: departments.map(d => d['level' + (i + 1)]) }))
+          })
         } else {
           this.$nextTick(() => this.initChart(res.data))
         }
@@ -48,9 +47,10 @@ export default {
     },
     initChart(data) {
       this.chart = echarts.init(this.$refs.chart)
-      const depts = data.departments
-      const names = depts.map(d => d.name)
-      const totals = depts.map(d => d.total)
+      const { departments, levelCfg } = data
+      const names = departments.map(d => d.name)
+      const totals = departments.map(d => d.total)
+      const levelKeys = ['level1', 'level2', 'level3', 'level4']
 
       const option = {
         tooltip: {
@@ -61,7 +61,7 @@ export default {
           textStyle: { color: '#d8f4ff' }
         },
         legend: {
-          data: ['I级', 'II级', 'III级', 'IV级'],
+          data: levelCfg.map(l => l.name),
           top: 0,
           textStyle: { color: '#7eacc8', fontSize: 11 },
           itemWidth: 10,
@@ -87,21 +87,23 @@ export default {
           splitLine: { lineStyle: { color: 'rgba(72,180,255,0.08)' } }
         },
         dataZoom: [{ type: 'slider', show: false, xAxisIndex: 0, startValue: 0, endValue: 5 }],
-        series: [
-          { name: 'I级', type: 'bar', stack: 'total', barWidth: 20, data: depts.map(d => d.level1), itemStyle: { color: '#ff4d4f' } },
-          { name: 'II级', type: 'bar', stack: 'total', data: depts.map(d => d.level2), itemStyle: { color: '#ff8c00' } },
-          { name: 'III级', type: 'bar', stack: 'total', data: depts.map(d => d.level3), itemStyle: { color: '#ffcc00' } },
-          { name: 'IV级', type: 'bar', stack: 'total', data: depts.map(d => d.level4), itemStyle: { color: '#3a7bff', borderRadius: [3,3,0,0] } }
-        ]
+        series: levelCfg.map((l, i) => ({
+          name: l.name,
+          type: 'bar',
+          stack: 'total',
+          barWidth: i === 0 ? 20 : undefined,
+          data: departments.map(d => d[levelKeys[i]]),
+          itemStyle: { color: l.color, borderRadius: i === levelCfg.length - 1 ? [3, 3, 0, 0] : undefined }
+        }))
       }
       this.chart.setOption(option)
 
       // 自动滚动X轴
-      if (depts.length > 6) {
+      if (departments.length > 6) {
         let idx = 0
         this.scrollTimer = setInterval(() => {
           idx++
-          if (idx > depts.length - 5) idx = 0
+          if (idx > departments.length - 5) idx = 0
           this.chart.dispatchAction({ type: 'dataZoom', startValue: idx, endValue: idx + 5 })
         }, 5000)
       }

+ 264 - 23
src/components/VideoMonitor/BuildingNav.vue

@@ -4,21 +4,45 @@
     <div class="corner-deco bl"></div><div class="corner-deco br"></div>
     <div class="panel-title">建筑结构导航</div>
     <div class="panel-content">
-      <input class="video-search" type="text" placeholder="搜索楼栋 / 楼层..." />
+      <el-input class="video-select" 
+        @keyup.enter.native="handleEnter"
+        @clear="handleEnter"
+        clearable
+        v-model="queryParams.searchValue" 
+        type="text" placeholder="搜索楼栋/楼层...." >
+      </el-input>
+      <el-select class="video-select" 
+        @change="handleEnter"
+        @clear="handleEnter"
+        clearable
+        v-model="queryParams.deptId" 
+        placeholder="请选择二级单位">
+        <el-option
+          v-for="item in options"
+          :key="item.deptId"
+          :label="item.deptName"
+          :value="item.deptId">
+        </el-option>
+      </el-select>
       <div class="tree-container">
         <el-tree
-          :data="treeData"
-          :props="{ label: 'label', children: 'children' }"
-          node-key="id"
-          default-expand-all
-          highlight-current
-          @node-click="handleNodeClick"
           class="dark-tree"
-        >
-          <template #default="{ node }">
+          @node-click="nodeClickButton"
+          :current-node-key="treeId"
+          :check-on-click-node="false"
+          check-strictly
+          highlight-current
+          :default-expanded-keys="defaultKey"
+          node-key="treeId"
+          :data="deptOptions"
+          :props="defaultProps"
+          ref="tree"
+          :load="loadNode"
+          accordion
+          lazy>
+          <template #default="{ node, data }">
             <span class="tree-node-label">
-              <i :class="node.isLeaf ? 'el-icon-video-camera' : 'el-icon-folder-opened'"></i>
-              <span>{{ node.label }}</span>
+              <span>{{ data.deptName }}</span>
             </span>
           </template>
         </el-tree>
@@ -28,25 +52,216 @@
 </template>
 
 <script>
-import { getBuildingTree } from '@/api'
+import { getBuildingTree, laboratoryLabScreenRooms, laboratoryLabScreenDeptDropList } from '@/api'
 
 export default {
   name: 'BuildingNav',
   data() {
-    return { treeData: [] }
-  },
-  async mounted() {
-    try {
-      const res = await getBuildingTree()
-      this.treeData = res.data.tree
-    } catch (e) {
-      console.error('BuildingNav:', e)
+    return { 
+      defaultProps: {
+        children: "childTreeList",
+        label: "deptName",
+        isLeaf:"leaf",
+      },
+      queryParams:{
+        searchValue: "",
+        deptId: '',
+      },
+      deptOptions: [], 
+      options:[],
+      treeId:null,
+      defaultKey:null,
     }
   },
+  mounted() {
+    this.getBuildingTree();
+    this.laboratoryLabScreenDeptDropList();
+  },
   methods: {
-    handleNodeClick(data) {
-      this.$emit('node-click', data)
-    }
+    handleEnter(){
+      this.getBuildingTree();
+    },
+    //二级学院下拉列表
+    async laboratoryLabScreenDeptDropList() {
+      let self = this;
+      try {
+        const res = await laboratoryLabScreenDeptDropList({})
+        this.$set(this,'options',res.data[0].child);
+      } catch (e) {
+        console.error('BuildingNav:', e)
+      }
+    },
+    async getBuildingTree() {
+      let self = this;
+      let obj = JSON.parse(JSON.stringify(this.queryParams))
+      try {
+        const res = await getBuildingTree(obj)
+        let list = this.forTreeId(res.data);
+        this.$set(self,'deptOptions',list)
+        this.$nextTick(()=>{
+          let checkData = []
+          if(this.queryParams.searchValue){
+            checkData = list[0].childTreeList[0].childTreeList[0];
+            //展开列表
+            self.$set(self,'defaultKey',[list[0].treeId,list[0].childTreeList[0].treeId]);
+            setTimeout(function(){
+              self.$set(self,'defaultKey',[list[0].treeId,list[0].childTreeList[0].childTreeList[0].treeId]);
+            },200)
+          }else{
+            checkData = list[0].childTreeList[0];
+            //展开列表
+            self.$set(self,'defaultKey',[list[0].treeId,list[0].childTreeList[0].treeId]);
+          }
+          setTimeout(function(){
+            //选中列表
+            self.$refs.tree.setCurrentKey(checkData.treeId);
+            self.$set(self,'treeId',checkData.treeId);
+            //当前位置展示
+            self.checkAddress(checkData.treeId);
+            //父级视屏数据
+            self.getSubId(checkData);
+          },600);
+        })
+      } catch (e) {
+        console.error('BuildingNav:', e)
+      }
+    },
+    nodeClickButton(e,data){
+      console.log('e===>',e);
+      console.log('data===>',data);
+      let self = this;
+      this.$nextTick(() => {
+        if (!e.level||e.level == 2||e.level == 3) {
+          if(this.treeId != e.treeId){
+            this.treeId = e.treeId;
+            //等待后续逻辑-面板展示-实验室信息-视屏信息
+            //当前位置展示
+            this.checkAddress(e.treeId);
+            //查询楼栋/楼层下所有实验室ID
+            this.getSubId(e);
+          }
+        }
+        this.$refs.tree.setCurrentKey(this.treeId);
+      });
+    },
+    forTreeId(list){
+      let self = this;
+      list.forEach((item,index)=>{
+        item.treeId = item.id;
+        item.deptName = item.name;
+        item.level = item.type;
+        item.childTreeList = item.buildFloorVoList;
+        delete item.id
+        delete item.name
+        delete item.type
+        delete item.buildFloorVoList
+        if(item.childTreeList[0]){
+          self.forTreeId(item.childTreeList);
+        }
+      })
+      return list
+    },
+    //手动加载
+    loadNode(node, resolve) {
+      console.log('node',node);
+      console.log('resolve',resolve);
+      let self = this;
+      if (node.data){
+        if(node.data.level == 3){
+          let obj = {
+            deptId:this.queryParams.deptId,
+            searchValue:this.queryParams.searchValue,
+          }
+          obj.floorId = node.data.treeId;
+          laboratoryLabScreenRooms(obj).then(response => {
+            for(let i=0;i<response.data.length;i++){
+              response.data[i].deptName = response.data[i].formatRoomName;
+              response.data[i].leaf = true;
+            }
+            resolve(response.data[0]?response.data:[]);
+          })
+        }else{
+          if(node.data.childTreeList){
+            if(node.data.childTreeList[0]){
+              if(node.data.level != 2){
+                node.data.childTreeList.forEach((item)=>{
+                  if(item.childTreeList){
+                    if(!item.childTreeList[0]){
+                      item.leaf = true;
+                    }
+                  }else{
+                    item.leaf = true;
+                  }
+                })
+              }
+              resolve(node.data.childTreeList);
+            }else{
+              resolve([]);
+            }
+          }else{
+            resolve([]);
+          }
+        }
+      }
+
+    },
+    //实验室ID查询
+    getSubId(e){
+      let self = this;
+      let obj = {
+        deptId:this.queryParams.deptId,
+        levelIds:[],
+      }
+      if(!e.level){
+        //实验室
+        this.$parent.setVideoData([e.treeId]);
+      }else if(e.level == 2){
+        //楼栋
+        obj.buildingId = e.treeId;
+        laboratoryLabScreenRooms(obj).then(response => {
+          let  list = [];
+          for(let i=0;i<response.data.length;i++){
+            list.push(response.data[i].treeId)
+          }
+          this.$parent.setVideoData(list);
+        })
+      }else if(e.level == 3){
+        //楼层
+        obj.floorId = e.treeId;
+        laboratoryLabScreenRooms(obj).then(response => {
+          let  list = [];
+          for(let i=0;i<response.data.length;i++){
+            list.push(response.data[i].treeId)
+          }
+          this.$parent.setVideoData(list);
+        })
+      }
+    },
+    //选中位置联查
+    checkAddress(id){
+      let list = this.forAddress(this.$refs.tree._data.root.childNodes,id);
+      this.$parent.setAddress(list);
+    },
+    forAddress(list,id){
+      let self = this;
+      let nameList = [];
+      let name = []
+      for(let i=0;i<list.length;i++){
+        if(list[i].data.treeId == id){
+          return list[i].data.deptName
+        }else{
+          if(list[i].childNodes){
+            if(list[i].childNodes[0]){
+              name = self.forAddress(list[i].childNodes,id);
+              if(name){
+                nameList = [list[i].data.deptName].concat(name)
+                return nameList
+              }
+            }
+          }
+        }
+      }
+    },
   }
 }
 </script>
@@ -86,6 +301,32 @@ export default {
     background: rgba(72,215,255,0.1);
   }
 }
+.video-select {
+  width: 100%;
+  border-radius: 4px;
+  // background: rgba(72,215,255,0.06);
+  // border: 1px solid $border-color;
+  color: $text-primary;
+  outline: none;
+  margin-bottom: 10px;
+  flex-shrink: 0;
+  transition: all 0.3s ease;
+
+  &::placeholder { color: $text-secondary; }
+
+  &:focus {
+    border-color: $accent;
+    box-shadow: 0 0 12px rgba(72,215,255,0.2), inset 0 0 8px rgba(72,215,255,0.06);
+    background: rgba(72,215,255,0.1);
+  }
+  ::v-deep .el-input__inner{
+    height: 33px;
+    font-size: 13px;
+    border: 1px solid $border-color;
+    background: rgba(72,215,255,0.06);
+    color:#d8f4ff;
+  }
+}
 
 .tree-container {
   flex: 1;

+ 90 - 18
src/components/VideoMonitor/VideoGrid.vue

@@ -4,11 +4,12 @@
     <div class="corner-deco bl"></div><div class="corner-deco br"></div>
     <div class="panel-title">
       实时视频监控
-      <span class="video-count">共 {{ total }} 路</span>
+      <span class="video-count">共 {{ videoTotal }} 路</span>
     </div>
     <div class="panel-content">
       <div class="grid-container">
-        <div
+        <H5PlayerVideo v-for="(item,index) in videoList" :key="index" :videoProps="item"></H5PlayerVideo>
+        <!-- <div
           v-for="(video, idx) in videoList"
           :key="video.id"
           class="cam-frame"
@@ -30,55 +31,126 @@
             <span v-if="video.ai" class="cam-ai-badge">AI</span>
             <span class="cam-status" :class="video.status">{{ video.status === 'online' ? 'REC' : 'OFF' }}</span>
           </div>
-        </div>
+        </div> -->
       </div>
       <div class="grid-pagination">
-        <button class="page-btn" :disabled="currentPage <= 1" @click="changePage(-1)">
+        <button class="page-btn" :disabled="videoQueryParams.page <= 1" @click="changePage(-1)">
           <i class="el-icon-arrow-left"></i>
         </button>
-        <span class="page-info">{{ currentPage }} / {{ totalPages }}</span>
-        <button class="page-btn" :disabled="currentPage >= totalPages" @click="changePage(1)">
+        <span class="page-info">{{ videoQueryParams.page }} / {{ totalPages }}</span>
+        <button class="page-btn" :disabled="videoQueryParams.page >= totalPages" @click="changePage(1)">
           <i class="el-icon-arrow-right"></i>
         </button>
       </div>
     </div>
+    <fullH5PlayerVideo v-if="fullVideoType" :fullVideoProps="fullVideoProps"></fullH5PlayerVideo>
   </div>
 </template>
 
 <script>
 import { getVideoList } from '@/api'
+import H5PlayerVideo from '@/components/H5PlayerVideo/H5PlayerVideo.vue'
+import fullH5PlayerVideo from '@/components/fullH5PlayerVideo/fullH5PlayerVideo.vue'
 
 export default {
   name: 'VideoGrid',
+  components: { H5PlayerVideo,fullH5PlayerVideo },
   data() {
     return {
-      videoList: [],
-      total: 0,
-      currentPage: 1
+      videoQueryParams:{
+        page:1,
+        pageSize:4,
+        streamType:1,
+        source:0,
+      },
+      videoType:false,
+      videoList:[],
+      videoTotal:0,
+      //全屏视频参数
+      fullVideoProps:{},
+      fullVideoType:false,
     }
   },
   computed: {
     totalPages() {
-      return Math.ceil(this.total / 9) || 1
+      return Math.ceil(this.videoTotal / 9) || 1
     }
   },
   mounted() {
-    this.loadVideos()
+    
   },
   methods: {
-    async loadVideos() {
+    //刷新视屏
+    getVideoData(data){
+      let obj = {
+        page:1,
+        pageSize:9,
+        passageway:'',
+        protocol:window.location.href.indexOf('https') !== -1?'wss':'ws',
+        streamType:1,
+        source:4,
+        subIds:data,
+      };
+      this.$set(this,'videoQueryParams',obj);
+      this.$nextTick(()=>{
+        this.videoInitialize()
+      })
+    },
+    async videoInitialize() {
+      let self = this;
+      self.$set(self, 'videoType', false);
+      self.$set(self, 'videoList', []);
       try {
-        const res = await getVideoList({ page: this.currentPage })
-        this.videoList = res.data.list
-        this.total = res.data.total
+        const res = await getVideoList(this.videoQueryParams)
+        let list = [];
+        res.data.total = 100;
+        res.data.records = [
+          {streamUrl:'1', deviceNo:'1'},
+          {streamUrl:'2', deviceNo:'2'},
+          {streamUrl:'3', deviceNo:'3'},
+          {streamUrl:'4', deviceNo:'4'},
+          {streamUrl:'5', deviceNo:'5'},
+          {streamUrl:'6', deviceNo:'6'},
+          {streamUrl:'7', deviceNo:'7'},
+          {streamUrl:'8', deviceNo:'8'},
+          {streamUrl:'9', deviceNo:'9'},
+        ];
+        for(let i=0;i<res.data.records.length;i++){
+          list.push(
+            {
+              width: 510, //(宽度:非必传-默认600)
+              height: 275, //(高度:非必传-默认338)
+              url: res.data.records[i].streamUrl,
+              cameraIndexCode: res.data.records[i].deviceNo,
+            }
+          )
+        }
+        this.$set(this,'videoList',list)
+        this.$set(this,'videoTotal',res.data.total);
+        this.$nextTick(()=>{
+          setTimeout(function(){
+            self.$set(self, 'videoType', true);
+          },1000);
+        })
       } catch (e) {
         console.error('VideoGrid:', e)
       }
     },
     changePage(delta) {
-      this.currentPage += delta
-      this.loadVideos()
-    }
+      this.videoQueryParams.page += delta
+      this.videoInitialize()
+    },
+    //全屏开启-关闭轮播
+    stopTime(cameraIndexCode){
+      this.$set(this,'fullVideoProps',{cameraIndexCode:cameraIndexCode});
+      this.$set(this,'fullVideoType',true);
+    },
+    //全屏关闭-开启轮播
+    outFullScreen(){
+      let self = this;
+      this.$set(this,'fullVideoType',false);
+      this.$set(this,'fullVideoProps',{});
+    },
   }
 }
 </script>

+ 2 - 2
src/components/fullH5PlayerVideo/fullH5PlayerVideo.vue

@@ -6,7 +6,7 @@
 </template>
 
 <script>
-import { iotCameraGetPreviewURLs } from '@/api/basicsModules/index'
+import { iotCameraGetPreviewURLs } from '@/api/index'
   export default {
     name: 'fullH5PlayerVideo',
     props: {
@@ -200,7 +200,7 @@ import { iotCameraGetPreviewURLs } from '@/api/basicsModules/index'
     }
     /*position: relative;*/
     .full-failed-button {
-      background: url("../../assets/ZDimages/basicsModules/icon_1.png");
+      background: url("../../assets/icon_1.png");
       background-size: 100%;
       position: fixed;
       font-size: 20px;

+ 12 - 0
src/styles/global.scss

@@ -216,3 +216,15 @@ html, body {
   0%, 100% { box-shadow: 0 0 6px $accent; opacity: 0.7; }
   50% { box-shadow: 0 0 14px $accent, 0 0 24px rgba(72,215,255,0.3); opacity: 1; }
 }
+
+.el-select-dropdown{
+  // background-color: rgba(72,215,255,0.06);
+  background-color: #092143;
+  border: 1px solid $border-color;
+}
+.el-select-dropdown__item{
+  color:#7eacc8;
+}
+.el-select-dropdown{
+  max-width: 200px;
+}

+ 12 - 0
src/utils/decrypt.js

@@ -0,0 +1,12 @@
+import CryptoJS from 'crypto-js'
+
+const key = CryptoJS.enc.Utf8.parse("J4ny0Ja678Y7P2so");
+const iv = CryptoJS.enc.Utf8.parse('pTNorfvZW2UZJbd0');
+
+export function Decrypt(word) {
+  let encryptedHexStr = CryptoJS.enc.Hex.parse(word);
+  let srcs = CryptoJS.enc.Base64.stringify(encryptedHexStr);
+  let decrypt = CryptoJS.AES.decrypt(srcs, key, { iv: iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 });
+  let decryptedStr = decrypt.toString(CryptoJS.enc.Utf8);
+  return decryptedStr.toString();
+}

+ 2 - 28
src/views/LabStatus.vue

@@ -19,7 +19,6 @@
         <EquipmentStats class="section s-equip" />
       </div>
     </div>
-    <AlertModal ref="alertModal" />
   </div>
 </template>
 
@@ -32,37 +31,12 @@ import EnvSensing from '@/components/LabStats/EnvSensing.vue'
 import RiskWarning from '@/components/LabStats/RiskWarning.vue'
 import DeviceStats from '@/components/LabStats/DeviceStats.vue'
 import EquipmentStats from '@/components/LabStats/EquipmentStats.vue'
-import AlertModal from '@/components/AlertModal.vue'
 
 export default {
   name: 'LabStatus',
   components: {
-    ScreenHeader,
-    BasicStats,
-    SafetyLevel,
-    PersonStats,
-    EnvSensing,
-    RiskWarning,
-    DeviceStats,
-    EquipmentStats,
-    AlertModal
-  },
-  mounted() {
-    // 15秒后演示触发预警弹窗
-    this.alertTimer = setTimeout(() => {
-      this.$refs.alertModal.show({
-        lab: '化学实验室A101(A101)',
-        building: '科研楼A-1层',
-        unit: '化学品安全研究所',
-        param: 'TVOC 超标',
-        value: '1.82 mg/m³',
-        person: '张明',
-        phone: '138-0012-3456'
-      })
-    }, 15000)
-  },
-  beforeDestroy() {
-    clearTimeout(this.alertTimer)
+    ScreenHeader, BasicStats, SafetyLevel, PersonStats,
+    EnvSensing, RiskWarning, DeviceStats, EquipmentStats
   }
 }
 </script>

+ 18 - 6
src/views/VideoMonitor.vue

@@ -3,11 +3,11 @@
     <ScreenHeader />
     <div class="page-body">
       <div class="video-left">
-        <BuildingNav @node-click="handleNodeClick" />
+        <BuildingNav/>
       </div>
       <div class="video-right">
         <div class="video-breadcrumb">{{ breadcrumb }}</div>
-        <VideoGrid />
+        <VideoGrid ref="videoComponent"/>
       </div>
     </div>
   </div>
@@ -23,13 +23,25 @@ export default {
   components: { ScreenHeader, BuildingNav, VideoGrid },
   data() {
     return {
-      breadcrumb: '安科院院区 → 科研楼A → 3层'
+      breadcrumb: ''
     }
   },
   methods: {
-    handleNodeClick(data) {
-      console.log('选中节点:', data.label)
-    }
+    setVideoData(list){
+      console.log('setVideoData',list)
+        this.$refs['videoComponent'].getVideoData(list)
+    },
+    setAddress(list){
+      let text = '';
+      for(let i=0;i<list.length;i++){
+        if(i==0){
+          text = text + list[i];
+        }else{
+          text = text + ' → ' + list[i];
+        }
+      }
+      this.$set(this,'breadcrumb',text);
+    },
   }
 }
 </script>

+ 14 - 0
vue.config.js

@@ -1,4 +1,5 @@
 const { defineConfig } = require('@vue/cli-service')
+const webpack = require('webpack')
 
 module.exports = defineConfig({
   transpileDependencies: true,
@@ -39,6 +40,19 @@ module.exports = defineConfig({
   },
 
   configureWebpack: {
+    plugins: [
+      new webpack.ProvidePlugin({
+        Buffer: ['buffer', 'Buffer'],
+        process: 'process/browser'
+      })
+    ],
+    resolve: {
+      fallback: {
+        url: require.resolve('url/'),
+        buffer: require.resolve('buffer/'),
+        process: require.resolve('process/browser')
+      }
+    },
     optimization: {
       splitChunks: {
         chunks: 'all',