Przeglądaj źródła

视频图像综合解析系统-提交

lxh 2 tygodni temu
rodzic
commit
c388ed7c8c
84 zmienionych plików z 3127 dodań i 0 usunięć
  1. BIN
      src/assets/images/wind-video/20260315_192656.jpg
  2. BIN
      src/assets/images/wind-video/20260315_193111.jpg
  3. BIN
      src/assets/images/wind-video/20260315_194758.jpg
  4. BIN
      src/assets/images/wind-video/20260315_195803.jpg
  5. BIN
      src/assets/images/wind-video/20260315_202803.jpg
  6. BIN
      src/assets/images/wind-video/20260315_210425.jpg
  7. BIN
      src/assets/images/wind-video/20260315_211358.jpg
  8. BIN
      src/assets/images/wind-video/20260315_212805.jpg
  9. BIN
      src/assets/images/wind-video/20260315_213411.jpg
  10. BIN
      src/assets/images/wind-video/20260315_220400.jpg
  11. BIN
      src/assets/images/wind-video/car1.jpg
  12. BIN
      src/assets/images/wind-video/car10.jpg
  13. BIN
      src/assets/images/wind-video/car11.jpg
  14. BIN
      src/assets/images/wind-video/car12.jpg
  15. BIN
      src/assets/images/wind-video/car13.jpg
  16. BIN
      src/assets/images/wind-video/car14.jpg
  17. BIN
      src/assets/images/wind-video/car15.jpg
  18. BIN
      src/assets/images/wind-video/car16.jpg
  19. BIN
      src/assets/images/wind-video/car17.jpg
  20. BIN
      src/assets/images/wind-video/car18.jpg
  21. BIN
      src/assets/images/wind-video/car19.jpg
  22. BIN
      src/assets/images/wind-video/car2.jpg
  23. BIN
      src/assets/images/wind-video/car20.jpg
  24. BIN
      src/assets/images/wind-video/car21.jpg
  25. BIN
      src/assets/images/wind-video/car22.jpg
  26. BIN
      src/assets/images/wind-video/car23.jpg
  27. BIN
      src/assets/images/wind-video/car24.jpg
  28. BIN
      src/assets/images/wind-video/car25.jpg
  29. BIN
      src/assets/images/wind-video/car26.jpg
  30. BIN
      src/assets/images/wind-video/car3.jpg
  31. BIN
      src/assets/images/wind-video/car4.jpg
  32. BIN
      src/assets/images/wind-video/car5.jpg
  33. BIN
      src/assets/images/wind-video/car6.jpg
  34. BIN
      src/assets/images/wind-video/car7.jpg
  35. BIN
      src/assets/images/wind-video/car8.jpg
  36. BIN
      src/assets/images/wind-video/car9.jpg
  37. BIN
      src/assets/images/wind-video/f1.png
  38. BIN
      src/assets/images/wind-video/f2.png
  39. BIN
      src/assets/images/wind-video/f3.png
  40. BIN
      src/assets/images/wind-video/fan1.png
  41. BIN
      src/assets/images/wind-video/fan2.png
  42. BIN
      src/assets/images/wind-video/fan3.png
  43. BIN
      src/assets/images/wind-video/fan4.png
  44. BIN
      src/assets/images/wind-video/fan5.png
  45. BIN
      src/assets/images/wind-video/fan6.png
  46. BIN
      src/assets/images/wind-video/fan7.png
  47. BIN
      src/assets/images/wind-video/gap1.jpg
  48. BIN
      src/assets/images/wind-video/gap10.jpg
  49. BIN
      src/assets/images/wind-video/gap12.jpg
  50. BIN
      src/assets/images/wind-video/gap17.jpg
  51. BIN
      src/assets/images/wind-video/gap2.jpg
  52. BIN
      src/assets/images/wind-video/gap3.jpg
  53. BIN
      src/assets/images/wind-video/gap4.jpg
  54. BIN
      src/assets/images/wind-video/gap5.jpg
  55. BIN
      src/assets/images/wind-video/gap6.jpg
  56. BIN
      src/assets/images/wind-video/gap7.jpg
  57. BIN
      src/assets/images/wind-video/gap8.jpg
  58. BIN
      src/assets/images/wind-video/open.jpg
  59. BIN
      src/assets/images/wind-video/open1.jpg
  60. BIN
      src/assets/images/wind-video/open11.jpg
  61. BIN
      src/assets/images/wind-video/open12.jpg
  62. BIN
      src/assets/images/wind-video/open13.jpg
  63. BIN
      src/assets/images/wind-video/open14.jpg
  64. BIN
      src/assets/images/wind-video/open15.jpg
  65. BIN
      src/assets/images/wind-video/open2.jpg
  66. BIN
      src/assets/images/wind-video/person1.jpg
  67. BIN
      src/assets/images/wind-video/person2.jpg
  68. BIN
      src/assets/images/wind-video/person3.jpg
  69. BIN
      src/assets/images/wind-video/v1.mp4
  70. BIN
      src/assets/images/wind-video/v2.mp4
  71. BIN
      src/assets/images/wind-video/video/4.mp4
  72. BIN
      src/assets/images/wind-video/video/b1.mp4
  73. BIN
      src/assets/images/wind-video/video/b2.mp4
  74. BIN
      src/assets/images/wind-video/video/f.mp4
  75. BIN
      src/assets/images/wind-video/video/fan-main.mp4
  76. BIN
      src/assets/images/wind-video/video/p.mp4
  77. BIN
      src/assets/images/wind-video/video/v1.mp4
  78. BIN
      src/assets/images/wind-video/video/v2.mp4
  79. 54 0
      src/views/vent/monitorManager/fanLocalVideo/fanLocalVideo.data.ts
  80. 990 0
      src/views/vent/monitorManager/fanLocalVideo/index.vue
  81. 912 0
      src/views/vent/monitorManager/mainLocalWind/index.vue
  82. 43 0
      src/views/vent/monitorManager/mainLocalWind/mainLocalWind.data.ts
  83. 1038 0
      src/views/vent/monitorManager/windDoorVideo/index.vue
  84. 90 0
      src/views/vent/monitorManager/windDoorVideo/windDoorVideo.data.ts

BIN
src/assets/images/wind-video/20260315_192656.jpg


BIN
src/assets/images/wind-video/20260315_193111.jpg


BIN
src/assets/images/wind-video/20260315_194758.jpg


BIN
src/assets/images/wind-video/20260315_195803.jpg


BIN
src/assets/images/wind-video/20260315_202803.jpg


BIN
src/assets/images/wind-video/20260315_210425.jpg


BIN
src/assets/images/wind-video/20260315_211358.jpg


BIN
src/assets/images/wind-video/20260315_212805.jpg


BIN
src/assets/images/wind-video/20260315_213411.jpg


BIN
src/assets/images/wind-video/20260315_220400.jpg


BIN
src/assets/images/wind-video/car1.jpg


BIN
src/assets/images/wind-video/car10.jpg


BIN
src/assets/images/wind-video/car11.jpg


BIN
src/assets/images/wind-video/car12.jpg


BIN
src/assets/images/wind-video/car13.jpg


BIN
src/assets/images/wind-video/car14.jpg


BIN
src/assets/images/wind-video/car15.jpg


BIN
src/assets/images/wind-video/car16.jpg


BIN
src/assets/images/wind-video/car17.jpg


BIN
src/assets/images/wind-video/car18.jpg


BIN
src/assets/images/wind-video/car19.jpg


BIN
src/assets/images/wind-video/car2.jpg


BIN
src/assets/images/wind-video/car20.jpg


BIN
src/assets/images/wind-video/car21.jpg


BIN
src/assets/images/wind-video/car22.jpg


BIN
src/assets/images/wind-video/car23.jpg


BIN
src/assets/images/wind-video/car24.jpg


BIN
src/assets/images/wind-video/car25.jpg


BIN
src/assets/images/wind-video/car26.jpg


BIN
src/assets/images/wind-video/car3.jpg


BIN
src/assets/images/wind-video/car4.jpg


BIN
src/assets/images/wind-video/car5.jpg


BIN
src/assets/images/wind-video/car6.jpg


BIN
src/assets/images/wind-video/car7.jpg


BIN
src/assets/images/wind-video/car8.jpg


BIN
src/assets/images/wind-video/car9.jpg


BIN
src/assets/images/wind-video/f1.png


BIN
src/assets/images/wind-video/f2.png


BIN
src/assets/images/wind-video/f3.png


BIN
src/assets/images/wind-video/fan1.png


BIN
src/assets/images/wind-video/fan2.png


BIN
src/assets/images/wind-video/fan3.png


BIN
src/assets/images/wind-video/fan4.png


BIN
src/assets/images/wind-video/fan5.png


BIN
src/assets/images/wind-video/fan6.png


BIN
src/assets/images/wind-video/fan7.png


BIN
src/assets/images/wind-video/gap1.jpg


BIN
src/assets/images/wind-video/gap10.jpg


BIN
src/assets/images/wind-video/gap12.jpg


BIN
src/assets/images/wind-video/gap17.jpg


BIN
src/assets/images/wind-video/gap2.jpg


BIN
src/assets/images/wind-video/gap3.jpg


BIN
src/assets/images/wind-video/gap4.jpg


BIN
src/assets/images/wind-video/gap5.jpg


BIN
src/assets/images/wind-video/gap6.jpg


BIN
src/assets/images/wind-video/gap7.jpg


BIN
src/assets/images/wind-video/gap8.jpg


BIN
src/assets/images/wind-video/open.jpg


BIN
src/assets/images/wind-video/open1.jpg


BIN
src/assets/images/wind-video/open11.jpg


BIN
src/assets/images/wind-video/open12.jpg


BIN
src/assets/images/wind-video/open13.jpg


BIN
src/assets/images/wind-video/open14.jpg


BIN
src/assets/images/wind-video/open15.jpg


BIN
src/assets/images/wind-video/open2.jpg


BIN
src/assets/images/wind-video/person1.jpg


BIN
src/assets/images/wind-video/person2.jpg


BIN
src/assets/images/wind-video/person3.jpg


BIN
src/assets/images/wind-video/v1.mp4


BIN
src/assets/images/wind-video/v2.mp4


BIN
src/assets/images/wind-video/video/4.mp4


BIN
src/assets/images/wind-video/video/b1.mp4


BIN
src/assets/images/wind-video/video/b2.mp4


BIN
src/assets/images/wind-video/video/f.mp4


BIN
src/assets/images/wind-video/video/fan-main.mp4


BIN
src/assets/images/wind-video/video/p.mp4


BIN
src/assets/images/wind-video/video/v1.mp4


BIN
src/assets/images/wind-video/video/v2.mp4


+ 54 - 0
src/views/vent/monitorManager/fanLocalVideo/fanLocalVideo.data.ts

@@ -0,0 +1,54 @@
+import { getAssetURL } from "/@/utils/ui"
+
+export const fanList = [
+  { id: 1, name: '1号掘进工作面局部风机' },
+  { id: 2, name: '2号回采工作面局部风机' },
+  { id: 3, name: '3号运输巷局部风机' },
+  { id: 4, name: '4号回风巷局部风机' },
+  { id: 5, name: '5号备用局部风机' }
+]
+
+export const areaList = [
+  { id: 1, name: '变频器区' }, { id: 2, name: '配电柜区' }, { id: 3, name: '风机本体区' },
+  { id: 4, name: '接线盒区' }, { id: 5, name: '散热区' }
+]
+
+// 在岗授权人员(用于比对)
+export const authPersons = ['张三', '李四', '王五', '赵六']
+
+export const statusData = {
+  normalFan: 4, offlineFan: 0, faultFan: 1, realTimePerson: 2, totalPerson: 15, warn: 3
+}
+
+export const areaStatsList = [
+  { areaName: '变频器区', personCount: 1, avgStayTime: '8分20秒' },
+  { areaName: '配电柜区', personCount: 1, avgStayTime: '5分10秒' },
+  { areaName: '风机本体区', personCount: 0, avgStayTime: '0秒' },
+]
+
+// 图片与轨迹数据
+export const currentImgList =  [
+  { id: 1, fanId: 1, areaId: 1, personName: '张三', teamName: '机电队', stayTime: '8分20秒', enterTime: '2026-03-28 16:22:00', leaveTime: '2026-03-28 16:30:20', imgUrl:getAssetURL('wind-video/fan1.png')  },
+  { id: 2, fanId: 1, areaId: 2, personName: '李四', teamName: '维修队', stayTime: '5分10秒', enterTime: '2026-03-28 16:25:00', leaveTime: '2026-03-28 16:30:10', imgUrl:getAssetURL('wind-video/fan1.png')  },
+  { id: 1, fanId: 1, areaId: 1, personName: '张三', teamName: '机电队', stayTime: '8分20秒', enterTime: '2026-03-28 16:22:00', leaveTime: '2026-03-28 16:30:20', imgUrl:getAssetURL('wind-video/fan1.png')  },
+  { id: 2, fanId: 1, areaId: 2, personName: '李四', teamName: '维修队', stayTime: '5分10秒', enterTime: '2026-03-28 16:25:00', leaveTime: '2026-03-28 16:30:10', imgUrl:getAssetURL('wind-video/fan1.png') },
+  { id: 1, fanId: 1, areaId: 1, personName: '张三', teamName: '机电队', stayTime: '8分20秒', enterTime: '2026-03-28 16:22:00', leaveTime: '2026-03-28 16:30:20', imgUrl:getAssetURL('wind-video/fan1.png') },
+  { id: 2, fanId: 1, areaId: 2, personName: '李四', teamName: '维修队', stayTime: '5分10秒', enterTime: '2026-03-28 16:25:00', leaveTime: '2026-03-28 16:30:10', imgUrl:getAssetURL('wind-video/fan1.png') },
+  { id: 1, fanId: 1, areaId: 1, personName: '张三', teamName: '机电队', stayTime: '8分20秒', enterTime: '2026-03-28 16:22:00', leaveTime: '2026-03-28 16:30:20',imgUrl:getAssetURL('wind-video/fan1.png')},
+  { id: 2, fanId: 1, areaId: 2, personName: '李四', teamName: '维修队', stayTime: '5分10秒', enterTime: '2026-03-28 16:25:00', leaveTime: '2026-03-28 16:30:10', imgUrl:getAssetURL('wind-video/fan1.png')  },
+]
+
+export const personTrackList = [
+  { fanId: 1, areaId: 1, personName: '张三', teamName: '机电队', enterTime: '2026-03-28 16:22:00', leaveTime: '2026-03-28 16:30:20', stayTime: '8分20秒' },
+  { fanId: 1, areaId: 2, personName: '李四', teamName: '维修队', enterTime: '2026-03-28 16:25:00', leaveTime: '2026-03-28 16:30:10', stayTime: '5分10秒' }
+]
+
+export const areaStayList = [
+  { areaName: '变频器区', personCount: 2, maxStay: '8分20秒', minStay: '3分45秒', avgStay: '6分02秒', totalStay: '12分05秒' },
+  { areaName: '配电柜区', personCount: 1, maxStay: '5分10秒', minStay: '5分10秒', avgStay: '5分10秒', totalStay: '5分10秒' }
+]
+
+export const warnRecordList = [
+  { fanId: 1, areaId: 1, personName: '张三', warnType: '人员逗留', warnTime: '2026-03-28 16:30:22', handleStatus: '处理中' },
+  { fanId: 1, areaId: 2, personName: '李四', warnType: '非法闯入', warnTime: '2026-03-28 16:28:15', handleStatus: '未处理' }
+]

+ 990 - 0
src/views/vent/monitorManager/fanLocalVideo/index.vue

@@ -0,0 +1,990 @@
+<template>
+  <div class="screen-container">
+    <div class="top-section">
+      <div class="system-title">局部风机无人值守演示系统</div>
+      <div class="top-inner">
+        <div class="top-left">
+          <div class="fan-switch">
+            <label></label>
+            <select v-model="selectFan" @change="switchFan">
+              <option value="all">全部风机</option>
+              <option v-for="fan in fanList" :key="fan.id" :value="fan.id">
+                {{ fan.name }}
+              </option>
+            </select>
+            <button class="btn btn-primary" @click="refreshVideo">刷新视频</button>
+            <div class="time-box">当前时间:{{ currentTime }}</div>
+          </div>
+
+          <div class="video-single">
+            <div class="video-item">
+              <div class="video-title">风机实时监控 [{{ currentFanName }}]</div>
+              <div class="video-box">
+                <video class="video-bg" autoplay loop muted playsinline webkit-playsinline controls>
+                  <source src="@/assets/images/wind-video/video/fan-main.mp4" type="video/mp4">
+                </video>
+              </div>
+            </div>
+
+            <!-- 改造后:井下设备操作日志区域 -->
+            <div class="video-item alarm-video-replace">
+              <div class="alarm-title">
+                <span>井下设备操作日志</span>
+                <div>
+                  <span class="alarm-badge">{{ operationLog.length }}</span>
+                  <button class="btn btn-sm" @click="clearLog">清空</button>
+                </div>
+              </div>
+              <div class="alarm-scroll" ref="logScroll">
+                <div class="alarm-item" v-for="(item, idx) in operationLog" :key="idx"
+                  :class="[item.status, item.new ? 'new' : '']">
+                  <div class="alarm-full-line">
+                    <span class="alarm-time">{{ item.time }}</span>
+                    <span class="alarm-fan">{{ item.fanName }}</span>
+                    <span class="alarm-type">{{ item.operateType }}</span>
+                    <span class="alarm-desc">{{ item.operator }} | {{ item.compareResult }}</span>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <div class="top-right">
+          <div class="status-panel">
+            <div class="status-title">风机状态总览</div>
+            <div class="status-grid">
+              <div class="status-item normal">
+                <div class="status-num">{{ statusData.normalFan }}</div>
+                <div class="status-name">风机正常</div>
+              </div>
+              <div class="status-item offline">
+                <div class="status-num">{{ statusData.offlineFan }}</div>
+                <div class="status-name">风机离线</div>
+              </div>
+              <div class="status-item fault">
+                <div class="status-num">{{ statusData.faultFan }}</div>
+                <div class="status-name">风机故障</div>
+              </div>
+              <div class="status-item warn">
+                <div class="status-num">{{ statusData.warn }}</div>
+                <div class="status-name">实时预警</div>
+              </div>
+              <div class="status-item person-online">
+                <div class="status-num">{{ statusData.realTimePerson }}</div>
+                <div class="status-name">实时人数</div>
+              </div>
+              <div class="status-item person-total">
+                <div class="status-num">{{ statusData.totalPerson }}</div>
+                <div class="status-name">今日总进入</div>
+              </div>
+            </div>
+          </div>
+
+          <div class="area-stats-panel">
+            <div class="status-title">区域停留统计</div>
+            <div class="area-stats-list">
+              <div class="area-stats-item" v-for="(item, idx) in areaStatsList" :key="idx">
+                <div class="area-name">{{ item.areaName }}</div>
+                <div class="area-data">
+                  <span class="person-count">{{ item.personCount }}人</span>
+                  <span class="stay-time">平均{{ item.avgStayTime }}</span>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="middle-section">
+      <div class="search-panel">
+        <div class="search-item">
+          <label>时间范围:</label>
+          <input type="datetime-local" v-model="searchForm.startTime" class="search-input">
+          <span class="split">-</span>
+          <input type="datetime-local" v-model="searchForm.endTime" class="search-input">
+          <button class="btn btn-sm btn-default" @click="selectToday">今日</button>
+          <button class="btn btn-sm btn-default" @click="select7d">近7天</button>
+        </div>
+        <div class="search-item">
+          <label>操作类型:</label>
+          <select v-model="searchForm.operateType" class="search-select">
+            <option value="all">全部操作</option>
+            <option value="风机调频">风机调频</option>
+            <option value="风机切换">风机切换</option>
+            <option value="参数调整">参数调整</option>
+            <option value="复位操作">复位操作</option>
+          </select>
+        </div>
+        <div class="search-item">
+          <label>风机选择:</label>
+          <select v-model="searchForm.fanId" class="search-select">
+            <option value="all">全部风机</option>
+            <option v-for="fan in fanList" :key="fan.id" :value="fan.id">{{ fan.name }}</option>
+          </select>
+        </div>
+        <div class="search-btn-group">
+          <button class="btn btn-primary" @click="handleSearch">一键检索</button>
+          <button class="btn btn-default" @click="resetSearch">重置</button>
+        </div>
+      </div>
+    </div>
+
+    <div class="bottom-section">
+      <div class="content-wrap">
+        <div class="img-list">
+          <div class="img-item" v-for="(item, idx) in currentImgList" :key="idx" @click="openImgModal(item)">
+            <img :src="item.imgUrl" class="img-thumbnail">
+            <div class="img-desc">
+              <p class="desc-line1">{{ getFanName(item.fanId) }}-{{ getAreaName(item.areaId) }}</p>
+              <p class="desc-line2">{{ item.personName }} 停留{{ item.stayTime }}</p>
+            </div>
+          </div>
+        </div>
+
+        <div class="right-data-panel">
+          <div class="tab-nav">
+            <div class="tab-item" :class="{ active: activeTab === 'personTrack' }" @click="activeTab = 'personTrack'">
+              人员轨迹</div>
+            <div class="tab-item" :class="{ active: activeTab === 'areaStay' }" @click="activeTab = 'areaStay'">区域统计
+            </div>
+            <div class="tab-item" :class="{ active: activeTab === 'warnRecord' }" @click="activeTab = 'warnRecord'">预警记录
+            </div>
+          </div>
+
+          <div class="data-list">
+            <div class="data-header" v-if="activeTab === 'personTrack'">
+              <div class="data-col">序号</div>
+              <div class="data-col">风机</div>
+              <div class="data-col">区域</div>
+              <div class="data-col">姓名</div>
+              <div class="data-col">区队</div>
+              <div class="data-col">进入</div>
+              <div class="data-col">离开</div>
+              <div class="data-col">时长</div>
+            </div>
+            <div class="data-header" v-else-if="activeTab === 'areaStay'">
+              <div class="data-col">序号</div>
+              <div class="data-col">区域</div>
+              <div class="data-col">人数</div>
+              <div class="data-col">最长</div>
+              <div class="data-col">最短</div>
+              <div class="data-col">平均</div>
+              <div class="data-col">总计</div>
+            </div>
+            <div class="data-header" v-else>
+              <div class="data-col">序号</div>
+              <div class="data-col">风机</div>
+              <div class="data-col">区域</div>
+              <div class="data-col">姓名</div>
+              <div class="data-col">类型</div>
+              <div class="data-col">时间</div>
+              <div class="data-col">状态</div>
+            </div>
+
+            <div class="data-row" v-for="(item, idx) in personTrackList" :key="idx" v-if="activeTab === 'personTrack'">
+              <div class="data-col">{{ idx + 1 }}</div>
+              <div class="data-col">{{ getFanName(item.fanId) }}</div>
+              <div class="data-col">{{ getAreaName(item.areaId) }}</div>
+              <div class="data-col">{{ item.personName }}</div>
+              <div class="data-col">{{ item.teamName }}</div>
+              <div class="data-col">{{ item.enterTime }}</div>
+              <div class="data-col">{{ item.leaveTime }}</div>
+              <div class="data-col">{{ item.stayTime }}</div>
+            </div>
+            <div class="data-row" v-for="(item, idx) in areaStayList" :key="idx" v-if="activeTab === 'areaStay'">
+              <div class="data-col">{{ idx + 1 }}</div>
+              <div class="data-col">{{ item.areaName }}</div>
+              <div class="data-col">{{ item.personCount }}</div>
+              <div class="data-col">{{ item.maxStay }}</div>
+              <div class="data-col">{{ item.minStay }}</div>
+              <div class="data-col">{{ item.avgStay }}</div>
+              <div class="data-col">{{ item.totalStay }}</div>
+            </div>
+            <div class="data-row" v-for="(item, idx) in warnRecordList" :key="idx" v-if="activeTab === 'warnRecord'">
+              <div class="data-col">{{ idx + 1 }}</div>
+              <div class="data-col">{{ getFanName(item.fanId) }}</div>
+              <div class="data-col">{{ getAreaName(item.areaId) }}</div>
+              <div class="data-col">{{ item.personName }}</div>
+              <div class="data-col">{{ item.warnType }}</div>
+              <div class="data-col">{{ item.warnTime }}</div>
+              <div class="data-col" :class="item.handleStatus === '已处理' ? 'handled' : 'unhandled'">{{ item.handleStatus
+                }}</div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="img-modal" v-show="showImgModal" @click="closeImgModal">
+      <div class="modal-content" @click.stop>
+        <div class="modal-header">
+          <span>现场抓拍详情</span>
+          <button class="close-btn" @click="closeImgModal">×</button>
+        </div>
+        <img :src="currentModalImg" class="modal-img">
+        <div class="modal-info">
+          <p>{{ modalInfo.fanName }} - {{ modalInfo.areaName }}</p>
+          <p>人员:{{ modalInfo.personName }} | 所属区队:{{ modalInfo.teamName }}</p>
+          <p>进入时间:{{ modalInfo.enterTime }} | 离开时间:{{ modalInfo.leaveTime }} | 停留时长:{{ modalInfo.stayTime }}</p>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, onUnmounted, nextTick } from 'vue'
+import { fanList, areaList, authPersons,statusData,areaStatsList,currentImgList ,personTrackList,areaStayList,warnRecordList} from './fanLocalVideo.data'
+
+// 工具方法
+const getFanName = (id) => fanList.find(i => i.id === id)?.name || '未知风机'
+const getAreaName = (id) => areaList.find(i => i.id === id)?.name || '未知区域'
+const selectFan = ref('all')
+const currentFanName = ref('1号掘进工作面局部风机')
+const currentTime = ref('')
+let timeTimer: null | NodeJS.Timeout = null;
+// ==============================================
+// 核心:井下设备操作日志(PLC + 配电柜 + 人员比对)
+// ==============================================
+const logScroll = ref<any>(null)
+let logTimer : null | NodeJS.Timeout = null;
+let addLogTimer: null | NodeJS.Timeout = null;
+// 操作日志列表(最多7条)
+const operationLog = ref([
+  { time: '10:12:20', fanName: '1号风机', operateType: '风机调频', operator: '张三', compareResult: '人员准入匹配', status: 'green' },
+  { time: '11:05:10', fanName: '2号风机', operateType: '参数调整', operator: '李四', compareResult: '人员准入匹配', status: 'green' },
+  { time: '14:30:00', fanName: '3号风机', operateType: '切换主备', operator: '未知人员', compareResult: '未检测到人员', status: 'red' },
+])
+// 搜索
+const searchForm = ref({ startTime: '', endTime: '', operateType: 'all', fanId: 'all' })
+const activeTab = ref('personTrack')
+const showImgModal = ref(false)
+const currentModalImg = ref('')
+const modalInfo = ref({}) as any
+
+// 生成PLC/配电柜操作记录 + 人员比对
+const createOperationLog = () => {
+  const now = new Date()
+  const time = `${String(now.getHours()).padStart(2, 0)}:${String(now.getMinutes()).padStart(2, 0)}:${String(now.getSeconds()).padStart(2, 0)}`
+
+  const fan = fanList[Math.floor(Math.random() * fanList.length)]
+  const operateTypes = ['风机调频', '切换主备', '参数调整', '复位操作']
+  const operateType = operateTypes[Math.floor(Math.random() * operateTypes.length)]
+
+  // 随机操作人员
+  const allOps = [...authPersons, '未知人员', '外来人员']
+  const operator = allOps[Math.floor(Math.random() * allOps.length)]
+
+  // 人员准入比对逻辑
+  const compareResult = authPersons.includes(operator) ? '人员准入匹配' : '未检测到人员'
+  const status = compareResult === '人员准入匹配' ? 'green' : 'red'
+
+  return {
+    time,
+    fanName: fan.name,
+    operateType,
+    operator,
+    compareResult,
+    status
+  }
+}
+
+// 自动生成日志,最多保留7条
+const startAddOperationLog = () => {
+  addLogTimer = setInterval(() => {
+    const log = createOperationLog()
+    operationLog.value.unshift(log)
+    log.new = true
+    setTimeout(() => (log.new = false), 1800)
+
+    // 最多7条
+    if (operationLog.value.length > 7) {
+      operationLog.value.pop()
+    }
+  }, 6000)
+}
+
+// 日志滚动
+const startLogScroll = () => {
+  nextTick(() => {
+    if (!logScroll.value) return
+    clearInterval(logTimer)
+    logTimer = setInterval(() => {
+      logScroll.value.scrollTop += 32
+      if (logScroll.value.scrollTop >= logScroll.value.scrollHeight - logScroll.value.clientHeight) {
+        logScroll.value.scrollTop = 0
+      }
+    }, 1500)
+  })
+}
+
+// 清空日志
+const clearLog = () => {
+  operationLog.value = []
+}
+
+const switchFan = () => {
+  if (selectFan.value !== 'all') currentFanName.value = getFanName(+selectFan.value)
+}
+const refreshVideo = () => {
+  const v = document.querySelector('.video-bg')
+  if (v) v.src = `/videos/fan-main.mp4?${Math.random()}`
+}
+
+const formatDate = (d) => {
+  const y = d.getFullYear()
+  const m = String(d.getMonth() + 1).padStart(2, 0)
+  const dt = String(d.getDate()).padStart(2, 0)
+  const h = String(d.getHours()).padStart(2, 0)
+  const mi = String(d.getMinutes()).padStart(2, 0)
+  return `${y}-${m}-${dt}T${h}:${mi}`
+}
+const selectToday = () => {
+  const now = new Date()
+  searchForm.value.startTime = formatDate(new Date(now.getFullYear(), now.getMonth(), now.getDate()))
+  searchForm.value.endTime = formatDate(now)
+}
+const select7d = () => {
+  const now = new Date()
+  searchForm.value.startTime = formatDate(new Date(now - 7 * 86400000))
+  searchForm.value.endTime = formatDate(now)
+}
+const handleSearch = () => alert('检索成功')
+const resetSearch = () => {
+  searchForm.value = { startTime: '', endTime: '', operateType: 'all', fanId: 'all' }
+  selectToday()
+}
+
+const openImgModal = (item) => {
+  showImgModal.value = true
+  currentModalImg.value = item.imgUrl
+  modalInfo.value = {
+    fanName: getFanName(item.fanId), areaName: getAreaName(item.areaId),
+    personName: item.personName, teamName: item.teamName,
+    enterTime: item.enterTime, leaveTime: item.leaveTime, stayTime: item.stayTime
+  }
+}
+const closeImgModal = () => showImgModal.value = false
+
+const updateTime = () => {
+  const d = new Date()
+  currentTime.value = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, 0)}-${String(d.getDate()).padStart(2, 0)} ${String(d.getHours()).padStart(2, 0)}:${String(d.getMinutes()).padStart(2, 0)}:${String(d.getSeconds()).padStart(2, 0)}`
+}
+
+onMounted(() => {
+  selectToday()
+  updateTime()
+  timeTimer = setInterval(updateTime, 1000)
+  startLogScroll()
+  startAddOperationLog()
+})
+onUnmounted(() => {
+  clearInterval(timeTimer)
+  clearInterval(logTimer)
+  clearInterval(addLogTimer)
+})
+</script>
+
+<style lang="less" scoped>
+.screen-container {
+  width: 100%;
+  height: 100%;
+  background: #0a102b;
+  color: #e0efff;
+  font-family: "Microsoft YaHei", sans-serif;
+  overflow: hidden !important;
+  padding: 8px;
+  box-sizing: border-box;
+}
+
+.system-title {
+  text-align: center;
+  font-size: 28px;
+  font-weight: bold;
+  color: #49bbff;
+  text-shadow: 0 0 15px #2a82ff;
+  padding: 6px 0;
+  letter-spacing: 2px;
+  margin: 0 0 4px 0;
+}
+
+.top-section {
+  height: 48%;
+  border: 1px solid #1f4499;
+  border-radius: 8px;
+  background: rgba(8, 18, 48, 0.75);
+  margin-bottom: 6px;
+  overflow: hidden;
+  box-sizing: border-box;
+}
+
+.top-inner {
+  display: flex;
+  height: calc(100% - 40px);
+  padding: 6px 8px;
+  gap: 10px;
+  box-sizing: border-box;
+}
+
+.top-left {
+  flex: 7;
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.top-right {
+  flex: 3;
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.fan-switch {
+  display: flex;
+  align-items: center;
+  height: 36px;
+  gap: 10px;
+}
+
+.fan-switch select {
+  height: 32px;
+  padding: 0 8px;
+  background: rgba(8, 18, 48, 0.8);
+  border: 1px solid #2a5bda;
+  color: #fff;
+  border-radius: 4px;
+  width: 280px;
+}
+
+.time-box {
+  font-size: 15px;
+  color: #49bbff;
+  font-weight: 500;
+  margin: auto;
+  letter-spacing: 1px;
+}
+
+.video-single {
+  display: flex;
+  flex: 1;
+  gap: 8px;
+}
+
+.video-item {
+  flex: 1;
+  border: 1px solid #2255cc;
+  border-radius: 4px;
+  background: #00000080;
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+}
+
+.video-title {
+  height: 30px;
+  line-height: 30px;
+  padding-left: 8px;
+  font-size: 13px;
+  color: #9cc9ff;
+  background: rgba(25, 55, 128, 0.3);
+}
+
+.video-box {
+  position: relative;
+  width: 100%;
+  flex: 1;
+  overflow: hidden;
+}
+
+.video-bg {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+  opacity: 0.9;
+}
+
+.alarm-video-replace {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+}
+
+.alarm-title {
+  height: 30px;
+  line-height: 30px;
+  padding: 0 8px;
+  background: rgba(25, 55, 128, 0.3);
+  color: #9cc9ff;
+  font-size: 13px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.alarm-scroll {
+  flex: 1;
+  padding: 4px;
+  overflow: hidden;
+  height: 100%;
+  max-height: 100%;
+}
+
+.alarm-item {
+  padding: 6px 8px;
+  margin-bottom: 4px;
+  border-radius: 4px;
+  font-size: 12px;
+  border-left: 3px solid transparent;
+  display: block;
+}
+
+.alarm-full-line {
+  width: 100%;
+  display: block;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.alarm-time,
+.alarm-fan,
+.alarm-type,
+.alarm-desc {
+  margin-right: 12px;
+  line-height: 24px;
+}
+
+.alarm-desc {
+  margin-right: 0;
+}
+
+/* 操作日志样式:匹配=绿色,不匹配=红色 */
+.alarm-item.green {
+  background: rgba(0, 255, 100, 0.15);
+  color: #9cffcc;
+  border-left-color: #00ff66;
+}
+
+.alarm-item.red {
+  background: rgba(255, 60, 60, 0.2);
+  color: #ff9999;
+  border-left-color: #ff4757;
+}
+
+.status-panel,
+.area-stats-panel {
+  flex: 1;
+  border: 1px solid #2255cc;
+  border-radius: 4px;
+  background: #00000060;
+  display: flex;
+  flex-direction: column;
+}
+
+.status-title {
+  height: 30px;
+  line-height: 30px;
+  padding: 0 8px;
+  background: rgba(25, 55, 128, 0.3);
+  color: #9cc9ff;
+  font-size: 13px;
+}
+
+.status-grid {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  grid-template-rows: 1fr 1fr 1fr;
+  gap: 4px;
+  padding: 4px;
+  flex: 1;
+}
+
+.status-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  background: rgba(30, 65, 140, 0.2);
+  border-radius: 4px;
+}
+
+.status-num {
+  font-size: 20px;
+  font-weight: bold;
+  color: #fff;
+}
+
+.status-name {
+  font-size: 11px;
+  color: #9cc9ff;
+  margin-top: 2px;
+}
+
+.area-stats-list {
+  flex: 1;
+  padding: 8px;
+  display: flex;
+  flex-direction: column;
+  gap: 5px;
+  overflow-y: auto;
+}
+
+.area-stats-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 6px 10px;
+  background: rgba(30, 65, 140, 0.25);
+  border-radius: 6px;
+  font-size: 13px;
+}
+
+.area-name {
+  color: #9cc9ff;
+  width: 70px;
+}
+
+.area-data {
+  display: flex;
+  gap: 15px;
+}
+
+.person-count {
+  color: #32ff80;
+  font-weight: bold;
+}
+
+.stay-time {
+  color: #ffa502;
+}
+
+.alarm-badge {
+  display: inline-block;
+  width: 18px;
+  height: 18px;
+  line-height: 18px;
+  text-align: center;
+  background: #49bbff;
+  border-radius: 50%;
+  font-size: 11px;
+  color: #fff;
+  margin-right: 6px;
+}
+
+.btn {
+  padding: 5px 10px;
+  background: rgba(42, 91, 218, 0.3);
+  border: 1px solid #2a5bda;
+  color: #fff;
+  border-radius: 4px;
+  cursor: pointer;
+}
+
+.btn-primary {
+  background: #49bbff60;
+  border-color: #49bbff;
+}
+
+.btn-sm {
+  padding: 3px 6px;
+  font-size: 11px;
+}
+
+.btn-default {
+  background: rgba(30, 65, 140, 0.3);
+  border-color: #1f4499;
+}
+
+.middle-section {
+  height: 6%;
+  border: 1px solid #1f4499;
+  border-radius: 8px;
+  background: rgba(8, 18, 48, 0.75);
+  margin-bottom: 6px;
+  display: flex;
+  align-items: center;
+  padding: 0 15px;
+  box-sizing: border-box;
+}
+
+.search-panel {
+  display: flex;
+  align-items: center;
+  gap: 15px;
+  width: 100%;
+}
+
+.search-item {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+
+.search-item label {
+  font-size: 13px;
+  color: #9cc9ff;
+}
+
+.search-input,
+.search-select {
+  height: 30px;
+  padding: 0 6px;
+  background: rgba(8, 18, 48, 0.8);
+  border: 1px solid #2a5bda;
+  color: #fff;
+  border-radius: 4px;
+  width: 160px;
+}
+
+.split {
+  color: #9cc9ff;
+  font-size: 12px;
+}
+
+.search-btn-group {
+  margin-left: auto;
+  display: flex;
+  gap: 8px;
+}
+
+.bottom-section {
+  height: 44%;
+  border: 1px solid #1f4499;
+  border-radius: 8px;
+  background: rgba(8, 18, 48, 0.75);
+  overflow: hidden;
+  box-sizing: border-box;
+}
+
+.content-wrap {
+  display: flex;
+  height: 100%;
+  padding: 8px;
+  gap: 10px;
+  box-sizing: border-box;
+}
+
+.img-list {
+  flex: 2;
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  overflow-y: auto;
+  padding: 4px;
+  align-content: flex-start;
+}
+
+.right-data-panel {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.img-item {
+  width: calc(25% - 6px);
+  border: 1px solid #2a5bda;
+  border-radius: 4px;
+  background: #00000060;
+  padding: 4px;
+  cursor: pointer;
+  display: flex;
+  flex-direction: column;
+  height: 160px;
+}
+
+.img-thumbnail {
+  width: 100%;
+  height: 120px;
+  object-fit: contain;
+  border-radius: 4px;
+  background: #000;
+}
+
+.img-desc {
+  height: 32px;
+  margin-top: 4px;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  gap: 2px;
+}
+
+.desc-line1 {
+  font-size: 10px;
+  color: #9cc9ff;
+  text-align: center;
+  margin: 0;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.desc-line2 {
+  font-size: 11px;
+  color: #e0efff;
+  text-align: center;
+  margin: 0;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.tab-nav {
+  display: flex;
+  height: 32px;
+  background: rgba(25, 55, 128, 0.2);
+  border-radius: 4px;
+  overflow: hidden;
+}
+
+.tab-item {
+  flex: 1;
+  line-height: 32px;
+  text-align: center;
+  font-size: 13px;
+  color: #9cc9ff;
+  cursor: pointer;
+}
+
+.tab-item.active {
+  background: #49bbff40;
+  color: #49bbff;
+  border-bottom: 2px solid #49bbff;
+}
+
+.data-list {
+  flex: 1;
+  border: 1px solid #2a5bda;
+  border-radius: 4px;
+  background: #00000060;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+}
+
+.data-header {
+  display: flex;
+  height: 30px;
+  line-height: 30px;
+  background: rgba(25, 55, 128, 0.3);
+  font-size: 12px;
+}
+
+.data-row {
+  display: flex;
+  height: 30px;
+  line-height: 30px;
+  border-bottom: 1px dashed #1f3e8a;
+  font-size: 11px;
+  color: #a0d3ff;
+}
+
+.data-col {
+  flex: 1;
+  text-align: center;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.data-col:first-child {
+  flex: 0 1 50px;
+}
+
+.handled {
+  color: #32ff80;
+  font-weight: bold;
+}
+
+.unhandled {
+  color: #ff4757;
+  font-weight: bold;
+}
+
+.img-modal {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100vw;
+  height: 100vh;
+  background: rgba(0, 0, 0, 0.85);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 9999;
+}
+
+.modal-content {
+  background: #0a102b;
+  border: 2px solid #49bbff;
+  border-radius: 8px;
+  width: 700px;
+  padding: 12px;
+}
+
+.modal-header {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 8px;
+  font-size: 15px;
+  color: #49bbff;
+}
+
+.close-btn {
+  background: none;
+  border: none;
+  color: #fff;
+  font-size: 20px;
+  cursor: pointer;
+}
+
+.modal-img {
+  width: 100%;
+  height: 400px;
+  object-fit: contain;
+  background: #000;
+  border-radius: 4px;
+}
+
+.modal-info {
+  text-align: center;
+  margin-top: 8px;
+  font-size: 13px;
+  color: #e0efff;
+}
+
+::-webkit-scrollbar {
+  width: 6px;
+  height: 6px;
+}
+
+::-webkit-scrollbar-track {
+  background: rgba(25, 55, 128, 0.2);
+  border-radius: 3px;
+}
+
+::-webkit-scrollbar-thumb {
+  background: #49bbff;
+  border-radius: 3px;
+}
+
+@keyframes newAlarmFlash {
+  0% {
+    background: rgba(73, 187, 255, 0.4);
+    transform: scale(1.02);
+  }
+
+  100% {
+    background: initial;
+    transform: scale(1);
+  }
+}
+
+.alarm-item.new {
+  animation: newAlarmFlash 1.8s forwards;
+}
+</style>

+ 912 - 0
src/views/vent/monitorManager/mainLocalWind/index.vue

@@ -0,0 +1,912 @@
+<template>
+  <div class="screen-container">
+    <div class="top-section">
+      <div class="system-title">主通风机无人巡检演示系统</div>
+      <div class="top-inner">
+        <div class="top-left">
+          <div class="fan-switch">
+            <label></label>
+            <select v-model="selectDevice" @change="switchDevice">
+              <option value="all">全部设备</option>
+              <option v-for="item in deviceList" :key="item.id" :value="item.id">
+                {{ item.name }}
+              </option>
+            </select>
+            <button class="btn btn-primary" @click="refreshVideo">刷新视频</button>
+            <div class="time-box">当前时间:{{ currentTime }}</div>
+          </div>
+
+          <!-- 双视频画面:主风机主体 + 配电室 -->
+          <div class="video-single">
+            <div class="video-item">
+              <div class="video-title">主风机主体监控 [{{ currentDeviceName }}]</div>
+              <div class="video-box">
+                <video class="video-bg" autoplay loop muted playsinline webkit-playsinline controls>
+                  <source src="@/assets/images/wind-video/video/f.mp4" type="video/mp4">
+                </video>
+              </div>
+            </div>
+
+            <div class="video-item">
+              <div class="video-title">配电室监控</div>
+              <div class="video-box">
+                <video class="video-bg" autoplay loop muted playsinline webkit-playsinline controls>
+                  <source src="@/assets/images/wind-video/video/p.mp4" type="video/mp4">
+                </video>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <div class="top-right">
+          <!-- 设备操作日志(最多显示3条) -->
+          <div class="status-panel log-panel">
+            <div class="status-title">设备操作日志</div>
+            <div class="alarm-scroll" ref="logScroll" style="height: 100%;padding:4px;">
+              <!-- 核心修改:slice(0, 3) -->
+              <div class="alarm-item" v-for="(item, idx) in operationLog.slice(0, 3)" :key="idx"
+                :class="[item.status, item.new ? 'new' : '']">
+                <div class="alarm-full-line">
+                  <span class="alarm-time">{{ item.time }}</span>
+                  <span class="alarm-fan">{{ item.deviceName }}</span>
+                  <span class="alarm-type">{{ item.operateType }}</span>
+                  <span class="alarm-desc">{{ item.compareResult }}</span>
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <!-- 主风机状态 -->
+          <div class="status-panel status-panel-large">
+            <div class="status-title">主风机运行状态</div>
+            <div class="status-grid">
+              <div class="status-item normal">
+                <div class="status-num">{{ statusData.runningFan }}</div>
+                <div class="status-name">运行风机</div>
+              </div>
+              <div class="status-item offline">
+                <div class="status-num">{{ statusData.stopFan }}</div>
+                <div class="status-name">停止风机</div>
+              </div>
+              <div class="status-item fault">
+                <div class="status-num">{{ statusData.fault }}</div>
+                <div class="status-name">设备故障</div>
+              </div>
+              <div class="status-item warn">
+                <div class="status-num">{{ statusData.warn }}</div>
+                <div class="status-name">预警总数</div>
+              </div>
+              <div class="status-item person-online">
+                <div class="status-num">{{ statusData.personDetect }}</div>
+                <div class="status-name">人员检测</div>
+              </div>
+              <div class="status-item person-total">
+                <div class="status-num">{{ statusData.todayRecord }}</div>
+                <div class="status-name">今日记录</div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="middle-section">
+      <div class="search-panel">
+        <div class="search-item">
+          <label>时间范围:</label>
+          <input type="datetime-local" v-model="searchForm.startTime" class="search-input">
+          <span class="split">-</span>
+          <input type="datetime-local" v-model="searchForm.endTime" class="search-input">
+          <button class="btn btn-sm btn-default" @click="selectToday">今日</button>
+          <button class="btn btn-sm btn-default" @click="select7d">近7天</button>
+        </div>
+        <div class="search-item">
+          <label>操作类型:</label>
+          <select v-model="searchForm.operateType" class="search-select">
+            <option value="all">全部操作</option>
+            <option value="启动风机">启动风机</option>
+            <option value="停止风机">停止风机</option>
+            <option value="参数调整">参数调整</option>
+            <option value="复位操作">复位操作</option>
+          </select>
+        </div>
+        <div class="search-item">
+          <label>设备选择:</label>
+          <select v-model="searchForm.deviceId" class="search-select">
+            <option value="all">全部设备</option>
+            <option v-for="item in deviceList" :key="item.id" :value="item.id">{{ item.name }}</option>
+          </select>
+        </div>
+        <div class="search-btn-group">
+          <button class="btn btn-primary" @click="handleSearch">一键检索</button>
+          <button class="btn btn-default" @click="resetSearch">重置</button>
+        </div>
+      </div>
+    </div>
+
+    <div class="bottom-section">
+      <div class="content-wrap">
+        <!-- 抓拍图片区域 -->
+        <div class="img-list">
+          <div class="img-item" v-for="(item, idx) in currentImgList" :key="idx" @click="openImgModal(item)">
+            <img :src="item.imgUrl" class="img-thumbnail">
+            <div class="img-desc">
+              <p class="desc-line1">{{ item.deviceName }} - {{ item.location }}</p>
+              <p class="desc-line2">{{ item.time }} | {{ item.event }}</p>
+            </div>
+          </div>
+        </div>
+
+        <!-- 右侧表格:仅保留人员检测记录 -->
+        <div class="right-data-panel">
+          <div class="tab-nav">
+            <div class="tab-item" :class="{ active: activeTab === 'personDetect' }" @click="activeTab = 'personDetect'">
+              人员检测记录</div>
+            <div class="tab-item" :class="{ active: activeTab === 'operateLog' }" @click="activeTab = 'operateLog'">
+              设备操作日志</div>
+            <div class="tab-item" :class="{ active: activeTab === 'warnRecord' }" @click="activeTab = 'warnRecord'">
+              设备预警记录</div>
+          </div>
+
+          <div class="data-list">
+            <!-- 人员检测(无姓名、无区队) -->
+            <div class="data-header" v-if="activeTab === 'personDetect'">
+              <div class="data-col">序号</div>
+              <div class="data-col">设备</div>
+              <div class="data-col">位置</div>
+              <div class="data-col">检测时间</div>
+              <div class="data-col">检测结果</div>
+              <div class="data-col">状态</div>
+            </div>
+            <!-- 操作日志 -->
+            <div class="data-header" v-else-if="activeTab === 'operateLog'">
+              <div class="data-col">序号</div>
+              <div class="data-col">设备</div>
+              <div class="data-col">操作</div>
+              <div class="data-col">时间</div>
+              <div class="data-col">比对结果</div>
+              <div class="data-col">状态</div>
+            </div>
+            <!-- 预警 -->
+            <div class="data-header" v-else>
+              <div class="data-col">序号</div>
+              <div class="data-col">设备</div>
+              <div class="data-col">位置</div>
+              <div class="data-col">预警类型</div>
+              <div class="data-col">时间</div>
+              <div class="data-col">状态</div>
+            </div>
+
+            <div class="data-row" v-for="(item, idx) in personDetectList" :key="idx"
+              v-if="activeTab === 'personDetect'">
+              <div class="data-col">{{ idx + 1 }}</div>
+              <div class="data-col">{{ item.deviceName }}</div>
+              <div class="data-col">{{ item.location }}</div>
+              <div class="data-col">{{ item.detectTime }}</div>
+              <div class="data-col">{{ item.result }}</div>
+              <div class="data-col" :class="item.status === '正常' ? 'handled' : 'unhandled'">{{ item.status
+                }}</div>
+            </div>
+
+            <!-- 表格里的操作日志也限制3条 -->
+            <div class="data-row" v-for="(item, idx) in operationLog.slice(0, 3)" :key="idx"
+              v-if="activeTab === 'operateLog'">
+              <div class="data-col">{{ idx + 1 }}</div>
+              <div class="data-col">{{ item.deviceName }}</div>
+              <div class="data-col">{{ item.operateType }}</div>
+              <div class="data-col">{{ item.time }}</div>
+              <div class="data-col">{{ item.compareResult }}</div>
+              <div class="data-col" :class="item.status === 'green' ? 'handled' : 'unhandled'">{{
+                item.status === 'green' ? '正常' : '异常' }}</div>
+            </div>
+
+            <div class="data-row" v-for="(item, idx) in warnRecordList" :key="idx" v-if="activeTab === 'warnRecord'">
+              <div class="data-col">{{ idx + 1 }}</div>
+              <div class="data-col">{{ item.deviceName }}</div>
+              <div class="data-col">{{ item.location }}</div>
+              <div class="data-col">{{ item.warnType }}</div>
+              <div class="data-col">{{ item.warnTime }}</div>
+              <div class="data-col" :class="item.handleStatus === '已处理' ? 'handled' : 'unhandled'">{{
+                item.handleStatus }}</div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="img-modal" v-show="showImgModal" @click="closeImgModal">
+      <div class="modal-content" @click.stop>
+        <div class="modal-header">
+          <span>现场抓拍详情</span>
+          <button class="close-btn" @click="closeImgModal">×</button>
+        </div>
+        <img :src="currentModalImg" class="modal-img">
+        <div class="modal-info">
+          <p>{{ modalInfo.deviceName }} - {{ modalInfo.location }}</p>
+          <p>事件:{{ modalInfo.event }}</p>
+          <p>时间:{{ modalInfo.time }}</p>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, onUnmounted, nextTick } from 'vue'
+import { deviceList, personDetectList, currentImgList, warnRecordList } from './mainLocalWind.data'
+
+
+const selectDevice = ref('all')
+const currentDeviceName = ref('一号主通风机')
+const currentTime = ref('')
+let timeTimer: null | NodeJS.Timeout = null;
+// 运行状态
+const statusData = ref({
+  runningFan: 1,
+  stopFan: 1,
+  fault: 0,
+  warn: 2,
+  personDetect: 1,
+  todayRecord: 26
+})
+// ======================
+// 设备操作日志(限制最多3条)
+// ======================
+const logScroll = ref<any>(null)
+let logTimer: null | NodeJS.Timeout = null;
+let addLogTimer: null | NodeJS.Timeout = null;
+const operationLog = ref([
+  { time: '08:50:10', deviceName: '一号主风机', operateType: '启动风机', compareResult: 'PLC操作匹配', status: 'green' },
+  { time: '10:20:30', deviceName: '配电室', operateType: '参数调整', compareResult: 'PLC操作匹配', status: 'green' },
+  { time: '14:10:00', deviceName: '二号主风机', operateType: '停止风机', compareResult: '未检测到人员', status: 'red' },
+])
+// ======================
+// 通用方法
+// ======================
+const searchForm = ref({ startTime: '', endTime: '', operateType: 'all', deviceId: 'all' })
+const activeTab = ref('personDetect')
+const showImgModal = ref(false)
+const currentModalImg = ref('')
+const modalInfo = ref({}) as any
+
+const createOperationLog = () => {
+  const now = new Date()
+  const time = `${String(now.getHours()).padStart(2, 0)}:${String(now.getMinutes()).padStart(2, 0)}:${String(now.getSeconds()).padStart(2, 0)}`
+  const device = deviceList[Math.floor(Math.random() * deviceList.length)]
+  const operateTypes = ['启动风机', '停止风机', '参数调整', '复位操作']
+  const operateType = operateTypes[Math.floor(Math.random() * operateTypes.length)]
+  const ok = Math.random() > 0.3
+  return {
+    time,
+    deviceName: device.name,
+    operateType,
+    compareResult: ok ? 'PLC操作匹配' : '未检测到人员',
+    status: ok ? 'green' : 'red'
+  }
+}
+
+const startAddOperationLog = () => {
+  addLogTimer = setInterval(() => {
+    const log = createOperationLog()
+    operationLog.value.unshift(log)
+    log.new = true
+    setTimeout(() => (log.new = false), 1800)
+    // 核心修改:超过3条就删掉最后一条
+    if (operationLog.value.length > 3) {
+      operationLog.value.pop()
+    }
+  }, 6000)
+}
+
+const startLogScroll = () => {
+  nextTick(() => {
+    if (!logScroll.value) return
+    clearInterval(logTimer)
+    logTimer = setInterval(() => {
+      logScroll.value.scrollTop += 32
+      if (logScroll.value.scrollTop >= logScroll.value.scrollHeight - logScroll.value.clientHeight) {
+        logScroll.value.scrollTop = 0
+      }
+    }, 1500)
+  })
+}
+
+const switchDevice = () => {
+  if (selectDevice.value !== 'all') {
+    const item = deviceList.find(i => i.id === +selectDevice.value)
+    if (item) currentDeviceName.value = item.name
+  }
+}
+
+const refreshVideo = () => {
+  const videos = document.querySelectorAll('.video-bg')
+  videos.forEach(v => {
+    v.src = v.src.split('?')[0] + `?${Math.random()}`
+  })
+}
+
+const formatDate = (d) => {
+  const y = d.getFullYear()
+  const m = String(d.getMonth() + 1).padStart(2, 0)
+  const dt = String(d.getDate()).padStart(2, 0)
+  const h = String(d.getHours()).padStart(2, 0)
+  const mi = String(d.getMinutes()).padStart(2, 0)
+  return `${y}-${m}-${dt}T${h}:${mi}`
+}
+
+const selectToday = () => {
+  const now = new Date()
+  searchForm.value.startTime = formatDate(new Date(now.getFullYear(), now.getMonth(), now.getDate()))
+  searchForm.value.endTime = formatDate(now)
+}
+
+const select7d = () => {
+  const now = new Date()
+  searchForm.value.startTime = formatDate(new Date(now - 7 * 86400000))
+  searchForm.value.endTime = formatDate(now)
+}
+
+const handleSearch = () => alert('检索成功')
+const resetSearch = () => {
+  searchForm.value = { startTime: '', endTime: '', operateType: 'all', deviceId: 'all' }
+  selectToday()
+}
+
+const openImgModal = (item) => {
+  showImgModal.value = true
+  currentModalImg.value = item.imgUrl
+  modalInfo.value = item
+}
+
+const closeImgModal = () => showImgModal.value = false
+
+const updateTime = () => {
+  const d = new Date()
+  currentTime.value = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, 0)}-${String(d.getDate()).padStart(2, 0)} ${String(d.getHours()).padStart(2, 0)}:${String(d.getMinutes()).padStart(2, 0)}:${String(d.getSeconds()).padStart(2, 0)}`
+}
+
+onMounted(() => {
+  selectToday()
+  updateTime()
+  timeTimer = setInterval(updateTime, 1000)
+  startLogScroll()
+  startAddOperationLog()
+})
+
+onUnmounted(() => {
+  clearInterval(timeTimer)
+  clearInterval(logTimer)
+  clearInterval(addLogTimer)
+})
+</script>
+
+<style lang="less" scoped>
+.screen-container {
+  width: 100%;
+  height: 100%;
+  background: #0a102b;
+  color: #e0efff;
+  font-family: "Microsoft YaHei", sans-serif;
+  overflow: hidden !important;
+  padding: 8px;
+  box-sizing: border-box;
+}
+
+.system-title {
+  text-align: center;
+  font-size: 28px;
+  font-weight: bold;
+  color: #49bbff;
+  text-shadow: 0 0 15px #2a82ff;
+  padding: 6px 0;
+  letter-spacing: 2px;
+  margin: 0 0 4px 0;
+}
+
+.top-section {
+  height: 48%;
+  border: 1px solid #1f4499;
+  border-radius: 8px;
+  background: rgba(8, 18, 48, 0.75);
+  margin-bottom: 6px;
+  overflow: hidden;
+  box-sizing: border-box;
+}
+
+.top-inner {
+  display: flex;
+  height: calc(100% - 40px);
+  padding: 6px 8px;
+  gap: 10px;
+  box-sizing: border-box;
+}
+
+.top-left {
+  flex: 7;
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.top-right {
+  flex: 3;
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+/* 压缩操作日志面板(适配3条高度) */
+.log-panel {
+  flex: 0 0 35% !important;
+  min-height: auto !important;
+}
+
+/* 扩大状态面板 */
+.status-panel-large {
+  flex: 1 !important;
+  min-height: 220px !important;
+}
+
+.fan-switch {
+  display: flex;
+  align-items: center;
+  height: 36px;
+  gap: 10px;
+}
+
+.fan-switch select {
+  height: 32px;
+  padding: 0 8px;
+  background: rgba(8, 18, 48, 0.8);
+  border: 1px solid #2a5bda;
+  color: #fff;
+  border-radius: 4px;
+  width: 280px;
+}
+
+.time-box {
+  font-size: 15px;
+  color: #49bbff;
+  font-weight: 500;
+  margin: auto;
+  letter-spacing: 1px;
+}
+
+.video-single {
+  display: flex;
+  flex: 1;
+  gap: 8px;
+}
+
+.video-item {
+  flex: 1;
+  border: 1px solid #2255cc;
+  border-radius: 4px;
+  background: #00000080;
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+}
+
+.video-title {
+  height: 30px;
+  line-height: 30px;
+  padding-left: 8px;
+  font-size: 13px;
+  color: #9cc9ff;
+  background: rgba(25, 55, 128, 0.3);
+}
+
+.video-box {
+  position: relative;
+  width: 100%;
+  flex: 1;
+  overflow: hidden;
+}
+
+.video-bg {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+  opacity: 0.9;
+}
+
+.status-panel {
+  flex: 1;
+  border: 1px solid #2255cc;
+  border-radius: 4px;
+  background: #00000060;
+  display: flex;
+  flex-direction: column;
+}
+
+.status-title {
+  height: 30px;
+  line-height: 30px;
+  padding: 0 8px;
+  background: rgba(25, 55, 128, 0.3);
+  color: #9cc9ff;
+  font-size: 13px;
+}
+
+.status-grid {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  grid-template-rows: 1fr 1fr 1fr;
+  gap: 4px;
+  padding: 4px;
+  flex: 1;
+}
+
+.status-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  background: rgba(30, 65, 140, 0.2);
+  border-radius: 4px;
+}
+
+.status-num {
+  font-size: 22px;
+  font-weight: bold;
+  color: #fff;
+}
+
+.status-name {
+  font-size: 12px;
+  color: #9cc9ff;
+  margin-top: 2px;
+}
+
+.alarm-item {
+  padding: 4px 6px;
+  margin-bottom: 3px;
+  border-radius: 4px;
+  font-size: 12px;
+  border-left: 3px solid transparent;
+  display: block;
+}
+
+.alarm-full-line {
+  width: 100%;
+  display: block;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.alarm-time,
+.alarm-fan,
+.alarm-type,
+.alarm-desc {
+  margin-right: 10px;
+  line-height: 22px;
+}
+
+.alarm-item.green {
+  background: rgba(0, 255, 100, 0.15);
+  color: #9cffcc;
+  border-left-color: #00ff66;
+}
+
+.alarm-item.red {
+  background: rgba(255, 60, 60, 0.2);
+  color: #ff9999;
+  border-left-color: #ff4757;
+}
+
+.btn {
+  padding: 5px 10px;
+  background: rgba(42, 91, 218, 0.3);
+  border: 1px solid #2a5bda;
+  color: #fff;
+  border-radius: 4px;
+  cursor: pointer;
+}
+
+.btn-primary {
+  background: #49bbff60;
+  border-color: #49bbff;
+}
+
+.btn-sm {
+  padding: 3px 6px;
+  font-size: 11px;
+}
+
+.btn-default {
+  background: rgba(30, 65, 140, 0.3);
+  border-color: #1f4499;
+}
+
+.middle-section {
+  height: 6%;
+  border: 1px solid #1f4499;
+  border-radius: 8px;
+  background: rgba(8, 18, 48, 0.75);
+  margin-bottom: 6px;
+  display: flex;
+  align-items: center;
+  padding: 0 15px;
+  box-sizing: border-box;
+}
+
+.search-panel {
+  display: flex;
+  align-items: center;
+  gap: 15px;
+  width: 100%;
+}
+
+.search-item {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+
+.search-item label {
+  font-size: 13px;
+  color: #9cc9ff;
+}
+
+.search-input,
+.search-select {
+  height: 30px;
+  padding: 0 6px;
+  background: rgba(8, 18, 48, 0.8);
+  border: 1px solid #2a5bda;
+  color: #fff;
+  border-radius: 4px;
+  width: 160px;
+}
+
+.split {
+  color: #9cc9ff;
+  font-size: 12px;
+}
+
+.search-btn-group {
+  margin-left: auto;
+  display: flex;
+  gap: 8px;
+}
+
+.bottom-section {
+  height: 44%;
+  border: 1px solid #1f4499;
+  border-radius: 8px;
+  background: rgba(8, 18, 48, 0.75);
+  overflow: hidden;
+  box-sizing: border-box;
+}
+
+.content-wrap {
+  display: flex;
+  height: 100%;
+  padding: 8px;
+  gap: 10px;
+  box-sizing: border-box;
+}
+
+.img-list {
+  flex: 2;
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  overflow-y: auto;
+  padding: 4px;
+  align-content: flex-start;
+}
+
+.right-data-panel {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.img-item {
+  width: calc(25% - 6px);
+  border: 1px solid #2a5bda;
+  border-radius: 4px;
+  background: #00000060;
+  padding: 4px;
+  cursor: pointer;
+  display: flex;
+  flex-direction: column;
+  height: 160px;
+}
+
+.img-thumbnail {
+  width: 100%;
+  height: 120px;
+  object-fit: contain;
+  border-radius: 4px;
+  background: #000;
+}
+
+.img-desc {
+  height: 32px;
+  margin-top: 4px;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  gap: 2px;
+}
+
+.desc-line1 {
+  font-size: 10px;
+  color: #9cc9ff;
+  text-align: center;
+  margin: 0;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.desc-line2 {
+  font-size: 11px;
+  color: #e0efff;
+  text-align: center;
+  margin: 0;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.tab-nav {
+  display: flex;
+  height: 32px;
+  background: rgba(25, 55, 128, 0.2);
+  border-radius: 4px;
+  overflow: hidden;
+}
+
+.tab-item {
+  flex: 1;
+  line-height: 32px;
+  text-align: center;
+  font-size: 13px;
+  color: #9cc9ff;
+  cursor: pointer;
+}
+
+.tab-item.active {
+  background: #49bbff40;
+  color: #49bbff;
+  border-bottom: 2px solid #49bbff;
+}
+
+.data-list {
+  flex: 1;
+  border: 1px solid #2a5bda;
+  border-radius: 4px;
+  background: #00000060;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+}
+
+.data-header {
+  display: flex;
+  height: 30px;
+  line-height: 30px;
+  background: rgba(25, 55, 128, 0.3);
+  font-size: 12px;
+}
+
+.data-row {
+  display: flex;
+  height: 30px;
+  line-height: 30px;
+  border-bottom: 1px dashed #1f3e8a;
+  font-size: 11px;
+  color: #a0d3ff;
+}
+
+.data-col {
+  flex: 1;
+  text-align: center;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.data-col:first-child {
+  flex: 0 1 50px;
+}
+
+.handled {
+  color: #32ff80;
+  font-weight: bold;
+}
+
+.unhandled {
+  color: #ff4757;
+  font-weight: bold;
+}
+
+.img-modal {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100vw;
+  height: 100vh;
+  background: rgba(0, 0, 0, 0.85);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 9999;
+}
+
+.modal-content {
+  background: #0a102b;
+  border: 2px solid #49bbff;
+  border-radius: 8px;
+  width: 700px;
+  padding: 12px;
+}
+
+.modal-header {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 8px;
+  font-size: 15px;
+  color: #49bbff;
+}
+
+.close-btn {
+  background: none;
+  border: none;
+  color: #fff;
+  font-size: 20px;
+  cursor: pointer;
+}
+
+.modal-img {
+  width: 100%;
+  height: 400px;
+  object-fit: contain;
+  background: #000;
+  border-radius: 4px;
+}
+
+.modal-info {
+  text-align: center;
+  margin-top: 8px;
+  font-size: 13px;
+  color: #e0efff;
+}
+
+::-webkit-scrollbar {
+  width: 6px;
+  height: 6px;
+}
+
+::-webkit-scrollbar-track {
+  background: rgba(25, 55, 128, 0.2);
+  border-radius: 3px;
+}
+
+::-webkit-scrollbar-thumb {
+  background: #49bbff;
+  border-radius: 3px;
+}
+
+@keyframes newAlarmFlash {
+  0% {
+    background: rgba(73, 187, 255, 0.4);
+    transform: scale(1.02);
+  }
+
+  100% {
+    background: initial;
+    transform: scale(1);
+  }
+}
+
+.alarm-item.new {
+  animation: newAlarmFlash 1.8s forwards;
+}
+</style>

+ 43 - 0
src/views/vent/monitorManager/mainLocalWind/mainLocalWind.data.ts

@@ -0,0 +1,43 @@
+import { getAssetURL } from "/@/utils/ui"
+
+// 主风机设备
+export const deviceList = [
+  { id: 1, name: '一号主通风机' },
+  { id: 2, name: '二号主通风机' },
+  { id: 3, name: '配电室总柜' }
+]
+
+// ======================
+// 人员检测记录(无姓名、无区队)
+// ======================
+export const personDetectList = [
+  { deviceName: '一号主风机', location: '风机主体', detectTime: '2026-04-02 08:45:20', result: '检测到人员', status: '正常' },
+  { deviceName: '配电室', location: '设备操作区', detectTime: '2026-04-02 10:18:10', result: '检测到人员', status: '正常' },
+  { deviceName: '二号主风机', location: '风机主体', detectTime: '2026-04-02 14:05:00', result: '检测到人员', status: '异常' },
+]
+
+// ======================
+// 抓拍图片
+// ======================
+export const currentImgList = [
+  { id: 1, deviceName: '一号主风机', location: '风机主体', event: '人员检测', time: '08:45', imgUrl: getAssetURL('wind-video/f1.png') },
+  { id: 1, deviceName: '一号主风机', location: '风机主体', event: '人员检测', time: '09:11', imgUrl: getAssetURL('wind-video/f2.png') },
+  { id: 1, deviceName: '一号主风机', location: '风机主体', event: '人员检测', time: '11:45', imgUrl: getAssetURL('wind-video/f3.png') },
+  { id: 1, deviceName: '一号主风机', location: '风机主体', event: '人员检测', time: '08:45', imgUrl: getAssetURL('wind-video/f1.png') },
+  { id: 1, deviceName: '一号主风机', location: '风机主体', event: '人员检测', time: '09:11', imgUrl: getAssetURL('wind-video/f2.png') },
+  { id: 1, deviceName: '一号主风机', location: '风机主体', event: '人员检测', time: '11:45', imgUrl: getAssetURL('wind-video/f3.png') },
+  { id: 1, deviceName: '一号主风机', location: '风机主体', event: '人员检测', time: '08:45', imgUrl: getAssetURL('wind-video/f1.png') },
+  { id: 1, deviceName: '一号主风机', location: '风机主体', event: '人员检测', time: '09:11', imgUrl: getAssetURL('wind-video/f2.png') },
+  { id: 1, deviceName: '一号主风机', location: '风机主体', event: '人员检测', time: '11:45', imgUrl: getAssetURL('wind-video/f3.png') },
+  { id: 1, deviceName: '一号主风机', location: '风机主体', event: '人员检测', time: '08:45', imgUrl: getAssetURL('wind-video/f1.png') },
+  { id: 1, deviceName: '一号主风机', location: '风机主体', event: '人员检测', time: '09:11', imgUrl: getAssetURL('wind-video/f2.png') },
+  { id: 1, deviceName: '一号主风机', location: '风机主体', event: '人员检测', time: '11:45', imgUrl: getAssetURL('wind-video/f3.png') },
+]
+
+// ======================
+// 预警
+// ======================
+export const warnRecordList = [
+    { deviceName: '一号主风机', location: '风机主体', warnType: '设备断网', warnTime: '2026-04-02 09:30:00', handleStatus: '已处理' },
+    { deviceName: '配电室', location: '配电柜', warnType: '设备断网', warnTime: '2026-04-02 11:20:00', handleStatus: '处理中' },
+]

+ 1038 - 0
src/views/vent/monitorManager/windDoorVideo/index.vue

@@ -0,0 +1,1038 @@
+<template>
+  <div class="screen-container">
+    <div class="top-section">
+      <div class="system-title">风门视频图像综合解析系统</div>
+      <div class="top-inner">
+        <div class="top-left">
+          <div class="door-switch">
+            <label></label>
+            <select v-model="selectDoor" @change="switchDoor">
+              <option value="all">全部风门</option>
+              <option v-for="door in doorList" :key="door.id" :value="door.id">
+                {{ door.name }}
+              </option>
+            </select>
+            <button class="btn btn-primary" @click="refreshVideo">刷新视频</button>
+
+            <!-- 当前时间 -->
+            <div class="time-box">当前时间:{{ currentTime }}</div>
+          </div>
+
+          <div class="video-double">
+            <div class="video-item">
+              <div class="video-title">前门 [{{ currentDoorName }}]</div>
+              <div class="video-box">
+                <!-- 视频标签:所有属性齐全,兼容所有浏览器 -->
+                <video class="video-bg" autoplay loop muted playsinline webkit-playsinline controls>
+                  <source src="@/assets/images/wind-video/video/v1.mp4" type="video/mp4">
+                  <!-- 兜底提示:视频不支持时显示 -->
+                  您的浏览器不支持视频播放
+                </video>
+              </div>
+            </div>
+            <div class="video-item">
+              <div class="video-title">后门 [{{ currentDoorName }}]</div>
+              <div class="video-box">
+                <video class="video-bg" autoplay loop muted playsinline webkit-playsinline controls>
+                  <source src="@/assets/images/wind-video/video/v2.mp4" type="video/mp4">
+                  <!-- 兜底提示:视频不支持时显示 -->
+                  您的浏览器不支持视频播放
+                </video>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <div class="top-right">
+          <div class="status-panel">
+            <div class="status-title">风门状态总览</div>
+            <div class="status-grid">
+              <div class="status-item normal">
+                <div class="status-num">{{ statusData.normal }}</div>
+                <div class="status-name">正常运行</div>
+              </div>
+              <div class="status-item offline">
+                <div class="status-num">{{ statusData.offline }}</div>
+                <div class="status-name">设备离线</div>
+              </div>
+              <div class="status-item fault">
+                <div class="status-num">{{ statusData.fault }}</div>
+                <div class="status-name">设备故障</div>
+              </div>
+              <div class="status-item warn">
+                <div class="status-num">{{ statusData.warn }}</div>
+                <div class="status-name">实时预警</div>
+              </div>
+              <div class="status-item car-pass">
+                <div class="status-num">{{ statusData.carPass }}</div>
+                <div class="status-name">今日车通</div>
+              </div>
+              <div class="status-item person-pass">
+                <div class="status-num">{{ statusData.personPass }}</div>
+                <div class="status-name">今日人通</div>
+              </div>
+            </div>
+          </div>
+
+          <div class="alarm-panel">
+            <div class="alarm-title">
+              <span>实时告警</span>
+              <div>
+                <span class="alarm-badge">{{ alarmList.length }}</span>
+                <button class="btn btn-sm" @click="clearAlarm">清空</button>
+              </div>
+            </div>
+            <div class="alarm-scroll" ref="alarmScroll">
+              <div class="alarm-item" v-for="(item, idx) in alarmList" :key="idx"
+                :class="[item.level, item.new ? 'new' : '']">
+                <span class="alarm-time">{{ item.time }}</span>
+                <span class="alarm-door">{{ getDoorName(item.doorId) }}</span>
+                <span class="alarm-type">{{ item.type }}</span>
+                <span class="alarm-desc">{{ item.desc }}</span>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="middle-section">
+      <div class="search-panel">
+        <div class="search-item">
+          <label>时间范围:</label>
+          <input type="datetime-local" v-model="searchForm.startTime" class="search-input">
+          <span class="split">-</span>
+          <input type="datetime-local" v-model="searchForm.endTime" class="search-input">
+          <button class="btn btn-sm btn-default" @click="selectToday">今日</button>
+          <button class="btn btn-sm btn-default" @click="select7d">近7天</button>
+        </div>
+        <div class="search-item">
+          <label>告警类型:</label>
+          <select v-model="searchForm.alarmType" class="search-select">
+            <option value="all">全部类型</option>
+            <option v-for="type in alarmTypeList" :key="type.value" :value="type.value">{{ type.label }}</option>
+          </select>
+        </div>
+        <div class="search-item">
+          <label>风门选择:</label>
+          <select v-model="searchForm.doorId" class="search-select">
+            <option value="all">全部风门</option>
+            <option v-for="door in doorList" :key="door.id" :value="door.id">{{ door.name }}</option>
+          </select>
+        </div>
+        <div class="search-btn-group">
+          <button class="btn btn-primary" @click="handleSearch">一键检索</button>
+          <button class="btn btn-default" @click="resetSearch">重置</button>
+        </div>
+      </div>
+    </div>
+
+    <div class="bottom-section">
+      <div class="content-wrap">
+        <div class="img-list">
+          <div class="img-item" v-for="(item, idx) in currentImgList" :key="idx" @click="openImgModal(item)">
+            <img :src="item.imgUrl" class="img-thumbnail">
+            <div class="img-desc">
+              <p class="desc-line1">{{ getDoorName(item.doorId) }}</p>
+              <p class="desc-line2">
+                <span v-if="item.type === '车辆通行'">车辆通行/车牌号{{ item.license }}</span>
+                <span v-else-if="item.type === '人员通行'">人员通行/{{ item.personName }}</span>
+                <span v-else-if="item.type === '风门未关严'">风门未关严 缝隙{{ item.gap }}mm</span>
+              </p>
+            </div>
+          </div>
+        </div>
+
+        <div class="right-data-panel">
+          <div class="tab-nav">
+            <div class="tab-item" :class="{ active: activeTab === 'car' }" @click="activeTab = 'car'">车辆通行记录</div>
+            <div class="tab-item" :class="{ active: activeTab === 'person' }" @click="activeTab = 'person'">人员通行记录</div>
+            <div class="tab-item" :class="{ active: activeTab === 'unclose' }" @click="activeTab = 'unclose'">风门未关严记录
+            </div>
+          </div>
+
+          <div class="data-list">
+            <div class="data-header" v-if="activeTab === 'car'">
+              <div class="data-col">序号</div>
+              <div class="data-col">风门名称</div>
+              <div class="data-col">车牌号</div>
+              <div class="data-col">通行时间</div>
+              <div class="data-col">结果</div>
+            </div>
+            <div class="data-header" v-else-if="activeTab === 'person'">
+              <div class="data-col">序号</div>
+              <div class="data-col">风门名称</div>
+              <div class="data-col">人员信息</div>
+              <div class="data-col">通行时间</div>
+              <div class="data-col">结果</div>
+            </div>
+            <div class="data-header" v-else>
+              <div class="data-col">序号</div>
+              <div class="data-col">风门名称</div>
+              <div class="data-col">检测时间</div>
+              <div class="data-col">缝隙(mm)</div>
+              <div class="data-col">等级</div>
+            </div>
+
+            <div class="data-row" v-for="(item, idx) in carList" :key="idx" v-if="activeTab === 'car'">
+              <div class="data-col">{{ idx + 1 }}</div>
+              <div class="data-col">{{ getDoorName(item.doorId) }}</div>
+              <div class="data-col">{{ item.license }}</div>
+              <div class="data-col">{{ item.time }}</div>
+              <div class="data-col">{{ item.result }}</div>
+            </div>
+            <div class="data-row" v-for="(item, idx) in personList" :key="idx" v-if="activeTab === 'person'">
+              <div class="data-col">{{ idx + 1 }}</div>
+              <div class="data-col">{{ getDoorName(item.doorId) }}</div>
+              <div class="data-col">{{ item.personInfo }}</div>
+              <div class="data-col">{{ item.time }}</div>
+              <div class="data-col">{{ item.result }}</div>
+            </div>
+            <div class="data-row" v-for="(item, idx) in uncloseList" :key="idx" v-if="activeTab === 'unclose'">
+              <div class="data-col">{{ idx + 1 }}</div>
+              <div class="data-col">{{ getDoorName(item.doorId) }}</div>
+              <div class="data-col">{{ item.time }}</div>
+              <div class="data-col">{{ item.gap }}</div>
+              <div class="data-col" :class="item.level">{{ item.level }}</div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <div class="img-modal" v-show="showImgModal" @click="closeImgModal">
+      <div class="modal-content" @click.stop>
+        <div class="modal-header">
+          <span>图像详情</span>
+          <button class="close-btn" @click="closeImgModal">×</button>
+        </div>
+        <img :src="currentModalImg" class="modal-img">
+        <div class="modal-info">
+          <p>{{ modalInfo }}</p>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, onUnmounted } from 'vue'
+import { doorList,statusData,alarmTypeList,createRandomAlarm,currentImgList,carList,personList,uncloseList } from './windDoorVideo.data'
+
+const selectDoor = ref('all')
+const currentDoorId = ref(1)
+const currentDoorName = ref('五盘区402辅运大巷风门')
+const videoUrl1 = ref('https://picsum.photos/800/600?random=1')
+const videoUrl2 = ref('https://picsum.photos/800/600?random=2')
+const showImgModal = ref(false)
+const currentModalImg = ref('')
+const modalInfo = ref('')
+const activeTab = ref('car')
+const currentTime = ref('')
+// https获取监测数据
+let timeTimer: null | NodeJS.Timeout = null;
+let addAlarmTimer: null | NodeJS.Timeout = null;// 新增:定时添加告警定时器
+let alarmTimer: null | NodeJS.Timeout = null; // 原有滚动定时器
+const alarmScroll = ref<any>(null)
+const searchForm = ref({ startTime: '', endTime: '', alarmType: 'all', doorId: 'all' })
+const alarmList = ref<any[]>([
+  { time: '16:30:22', doorId: 2, type: '防夹人预警', desc: '危险区域有人闯入,已触发PLC停止', level: 'red' },
+  { time: '16:28:15', doorId: 4, type: '风门未关严', desc: '门板闭合缝隙15mm,超标10mm', level: 'yellow' },
+  { time: '16:25:36', doorId: 4, type: '异物卡堵', desc: '门体通道检测到石块异物,影响闭合', level: 'orange' },
+  { time: '16:20:18', doorId: 1, type: '违规停留', desc: '禁停区有车辆滞留3分钟,未及时驶离', level: 'gray' },
+  { time: '16:15:45', doorId: 3, type: '密封失效', desc: '疑似风门密封胶条脱落,存在漏风风险', level: 'yellow' },
+  { time: '16:10:30', doorId: 5, type: '车辆撞击', desc: '风门门体疑似受到轻微撞击,需检查变形', level: 'orange' },
+  { time: '16:05:12', doorId: 2, type: '防夹人预警', desc: '风门闭合区检测到人员肢体,紧急止停', level: 'red' },
+  { time: '16:00:58', doorId: 1, type: '风门未关严', desc: '门板闭合缝隙8mm,接近阈值', level: 'gray' },
+  { time: '15:55:33', doorId: 5, type: '异物卡堵', desc: '风门轨道有杂物', level: 'yellow' },
+  { time: '15:50:20', doorId: 3, type: '违规停留', desc: '禁停区有人员逗留,已语音提醒', level: 'gray' }
+])
+
+
+
+// 新增:定时添加新告警到顶部
+const startAddAlarm = () => {
+  // 每5-10秒随机生成1条(可自行调整时间)
+  const randomTime = 5000 + Math.floor(Math.random() * 5000)
+  addAlarmTimer = setInterval(() => {
+    const newAlarm = createRandomAlarm()
+    alarmList.value.unshift(newAlarm) // 插入到列表顶部,实现“新告警优先展示”
+    newAlarm.new = true;
+    // 1.8 秒后清除动画
+    setTimeout(() => {
+      newAlarm.new = false;
+    }, 1800);
+    // 限制列表长度,避免数据过多(保留20条,可调整)
+    if (alarmList.value.length > 20) {
+      alarmList.value.pop()
+    }
+  }, randomTime)
+}
+const getDoorName = (id) => {
+  const d = doorList.find(i => i.id === id)
+  return d ? d.name : '未知风门'
+}
+const switchDoor = () => {
+  if (selectDoor.value !== 'all') {
+    currentDoorId.value = +selectDoor.value
+    currentDoorName.value = getDoorName(currentDoorId.value)
+    videoUrl1.value = `https://picsum.photos/800/600?random=${Math.random()}`
+    videoUrl2.value = `https://picsum.photos/800/600?random=${Math.random()}`
+  }
+}
+const refreshVideo = () => {
+  videoUrl1.value = `https://picsum.photos/800/600?random=${Math.random()}`
+  videoUrl2.value = `https://picsum.photos/800/600?random=${Math.random()}`
+}
+const clearAlarm = () => { alarmList.value = [] }
+const formatDate = (d) => {
+  const y = d.getFullYear()
+  const m = String(d.getMonth() + 1).padStart(2, '0')
+  const dt = String(d.getDate()).padStart(2, '0')
+  const h = String(d.getHours()).padStart(2, '0')
+  const mi = String(d.getMinutes()).padStart(2, '0')
+  return `${y}-${m}-${dt}T${h}:${mi}`
+}
+
+const selectToday = () => {
+  const now = new Date()
+  const st = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0)
+  searchForm.value.startTime = formatDate(st)
+  searchForm.value.endTime = formatDate(now)
+}
+
+const select7d = () => {
+  const now = new Date()
+  const st = new Date(now.getTime() - 7 * 86400000)
+  searchForm.value.startTime = formatDate(st)
+  searchForm.value.endTime = formatDate(now)
+}
+
+const handleSearch = () => { alert('检索成功') }
+const resetSearch = () => { searchForm.value = { startTime: '', endTime: '', alarmType: 'all', doorId: 'all' } }
+
+const openImgModal = (item) => {
+  showImgModal.value = true
+  currentModalImg.value = item.imgUrl
+  let info = ''
+  if (item.type === '车辆通行') info = `${getDoorName(item.doorId)} | 车辆通行/${item.license}`
+  if (item.type === '人员通行') info = `${getDoorName(item.doorId)} | 人员通行/${item.personName}`
+  if (item.type === '风门未关严') info = `${getDoorName(item.doorId)} | 缝隙${item.gap}mm`
+  modalInfo.value = info
+}
+const closeImgModal = () => { showImgModal.value = false }
+
+const updateTime = () => {
+  const d = new Date()
+  const y = d.getFullYear()
+  const m = String(d.getMonth() + 1).padStart(2, '0')
+  const dt = String(d.getDate()).padStart(2, '0')
+  const h = String(d.getHours()).padStart(2, '0')
+  const mi = String(d.getMinutes()).padStart(2, '0')
+  const se = String(d.getSeconds()).padStart(2, '0')
+  currentTime.value = `${y}-${m}-${dt} ${h}:${mi}:${se}`
+}
+// 恢复告警滚动(优化流畅度)
+const startAlarmScroll = () => {
+  if (!alarmScroll.value) return
+  alarmTimer = setInterval(() => {
+    const scrollDom = alarmScroll.value
+    const firstItem = scrollDom.firstElementChild
+    if (firstItem) {
+      // 平滑滚动到下一条,贴合工业界面体验
+      scrollDom.scrollTop += 28
+      // 滚动到底部时重置
+      if (scrollDom.scrollTop >= scrollDom.scrollHeight - scrollDom.clientHeight) {
+        scrollDom.scrollTop = 0
+      }
+    }
+  }, 1500) // 滚动速度(数值越小越快,可调整)
+}
+
+onMounted(() => {
+  selectToday()
+  updateTime()
+  timeTimer = setInterval(updateTime, 1000)
+  startAlarmScroll() // 恢复原有滚动
+  startAddAlarm() // 新增:启动定时添加新告警
+})
+
+onUnmounted(() => {
+  clearInterval(timeTimer)
+  if (alarmTimer) clearInterval(alarmTimer) // 销毁滚动定时器
+  if (addAlarmTimer) clearInterval(addAlarmTimer) // 销毁新增告警定时器
+})
+</script>
+
+<style lang="less" scoped>
+.screen-container {
+  position: relative;
+  width: 100vw;
+  height: 100%;
+  background: #0a102b;
+  color: #e0efff;
+  font-family: "Microsoft YaHei", sans-serif;
+  /* 关键修复:禁止页面整体滚动,内部区域自己滚动 */
+  overflow: hidden !important;
+  padding: 8px;
+  box-sizing: border-box;
+}
+
+.system-title {
+  text-align: center;
+  font-size: 28px;
+  font-weight: bold;
+  color: #49bbff;
+  text-shadow: 0 0 15px #2a82ff;
+  padding: 6px 0;
+  letter-spacing: 2px;
+  margin: 0 0 4px 0;
+}
+
+.top-section {
+  height: 48%;
+  border: 1px solid #1f4499;
+  border-radius: 8px;
+  background: rgba(8, 18, 48, 0.75);
+  margin-bottom: 6px;
+  overflow: hidden;
+  box-sizing: border-box;
+}
+
+.top-inner {
+  display: flex;
+  height: calc(100% - 40px);
+  padding: 6px 8px;
+  gap: 10px;
+  box-sizing: border-box;
+}
+
+.top-left {
+  flex: 7;
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.top-right {
+  flex: 3;
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.door-switch {
+  display: flex;
+  align-items: center;
+  height: 36px;
+  gap: 10px;
+}
+
+.door-switch label {
+  font-size: 14px;
+  color: #9cc9ff;
+}
+
+.door-switch select {
+  height: 32px;
+  padding: 0 8px;
+  background: rgba(8, 18, 48, 0.8);
+  border: 1px solid #2a5bda;
+  color: #fff;
+  border-radius: 4px;
+  width: 280px;
+}
+
+.time-box {
+  font-size: 15px;
+  color: #49bbff;
+  font-weight: 500;
+  margin: auto;
+  letter-spacing: 1px;
+}
+
+.video-double {
+  display: flex;
+  flex: 1;
+  gap: 8px;
+}
+
+.video-item {
+  flex: 1;
+  border: 1px solid #2255cc;
+  border-radius: 4px;
+  background: #00000080;
+  display: flex;
+  flex-direction: column;
+}
+
+.video-title {
+  height: 30px;
+  line-height: 30px;
+  padding-left: 8px;
+  font-size: 13px;
+  color: #9cc9ff;
+  background: rgba(25, 55, 128, 0.3);
+}
+
+.video-box {
+  position: relative;
+  width: 100%;
+  flex: 1;
+  overflow: hidden;
+}
+
+.video-bg {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+  opacity: 0.9;
+}
+
+.ai-mark {
+  position: absolute;
+  border: 2px solid;
+  background: rgba(255, 255, 255, 0.05);
+}
+
+.danger-area {
+  width: 35%;
+  height: 45%;
+  top: 22%;
+  left: 12%;
+  border-color: #ff4757;
+}
+
+.stop-area {
+  width: 25%;
+  height: 30%;
+  bottom: 12%;
+  right: 12%;
+  border-color: #ffa502;
+}
+
+.detect-box {
+  position: absolute;
+  border: 2px solid;
+}
+
+.person {
+  width: 50px;
+  height: 90px;
+  top: 35%;
+  left: 18%;
+  border-color: #32ff80;
+}
+
+.car {
+  width: 100px;
+  height: 50px;
+  top: 65%;
+  right: 18%;
+  border-color: #32b5ff;
+}
+
+.foreign {
+  width: 40px;
+  height: 40px;
+  top: 50%;
+  left: 45%;
+  border-color: #ff9500;
+}
+
+.status-panel,
+.alarm-panel {
+  flex: 1;
+  border: 1px solid #2255cc;
+  border-radius: 4px;
+  background: #00000060;
+  display: flex;
+  flex-direction: column;
+}
+
+.status-title,
+.alarm-title {
+  height: 30px;
+  line-height: 30px;
+  padding: 0 8px;
+  background: rgba(25, 55, 128, 0.3);
+  color: #9cc9ff;
+  font-size: 13px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.status-grid {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  grid-template-rows: 1fr 1fr 1fr;
+  gap: 4px;
+  padding: 4px;
+  flex: 1;
+}
+
+.status-item {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  background: rgba(30, 65, 140, 0.2);
+  border-radius: 4px;
+}
+
+.status-num {
+  font-size: 20px;
+  font-weight: bold;
+  color: #fff;
+}
+
+.status-name {
+  font-size: 11px;
+  color: #9cc9ff;
+  margin-top: 2px;
+}
+
+.alarm-badge {
+  display: inline-block;
+  width: 18px;
+  height: 18px;
+  line-height: 18px;
+  text-align: center;
+  background: #ff4757;
+  border-radius: 50%;
+  font-size: 11px;
+  color: #fff;
+  margin-right: 6px;
+}
+
+.alarm-scroll {
+  padding: 4px;
+  flex: 1;
+  overflow: hidden;
+  position: relative;
+}
+
+.alarm-item {
+  padding: 4px 6px;
+  margin-bottom: 3px;
+  border-radius: 4px;
+  font-size: 11px;
+  display: flex;
+  gap: 6px;
+  align-items: center;
+}
+
+.alarm-item.red {
+  background: rgba(255, 60, 60, 0.2);
+  color: #ff9999;
+  border-left: 3px solid #ff4757;
+}
+
+.alarm-item.orange {
+  background: rgba(255, 153, 0, 0.2);
+  color: #ffc37f;
+  border-left: 3px solid #ff9500;
+}
+
+.alarm-item.yellow {
+  background: rgba(255, 230, 0, 0.2);
+  color: #fff380;
+  border-left: 3px solid #ffcc32;
+}
+
+.btn {
+  padding: 5px 10px;
+  background: rgba(42, 91, 218, 0.3);
+  border: 1px solid #2a5bda;
+  color: #fff;
+  border-radius: 4px;
+  cursor: pointer;
+}
+
+.btn-primary {
+  background: #49bbff60;
+  border-color: #49bbff;
+}
+
+.btn-sm {
+  padding: 3px 6px;
+  font-size: 11px;
+}
+
+.btn-default {
+  background: rgba(30, 65, 140, 0.3);
+  border-color: #1f4499;
+}
+
+.middle-section {
+  height: 6%;
+  border: 1px solid #1f4499;
+  border-radius: 8px;
+  background: rgba(8, 18, 48, 0.75);
+  margin-bottom: 6px;
+  display: flex;
+  align-items: center;
+  padding: 0 15px;
+  box-sizing: border-box;
+}
+
+.search-panel {
+  display: flex;
+  align-items: center;
+  gap: 15px;
+  width: 100%;
+}
+
+.search-item {
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+
+.search-item label {
+  font-size: 13px;
+  color: #9cc9ff;
+}
+
+.search-input,
+.search-select {
+  height: 30px;
+  padding: 0 6px;
+  background: rgba(8, 18, 48, 0.8);
+  border: 1px solid #2a5bda;
+  color: #fff;
+  border-radius: 4px;
+  width: 160px;
+}
+
+.split {
+  color: #9cc9ff;
+  font-size: 12px;
+}
+
+.search-btn-group {
+  margin-left: auto;
+  display: flex;
+  gap: 8px;
+}
+
+.bottom-section {
+  height: 44%;
+  border: 1px solid #1f4499;
+  border-radius: 8px;
+  background: rgba(8, 18, 48, 0.75);
+  overflow: hidden;
+  box-sizing: border-box;
+}
+
+.content-wrap {
+  display: flex;
+  height: 100%;
+  padding: 8px;
+  gap: 10px;
+  box-sizing: border-box;
+}
+
+.img-list {
+  flex: 2;
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  overflow-y: auto;
+  padding: 4px;
+}
+
+.img-item {
+  width: calc(25% - 6px);
+  border: 1px solid #2a5bda;
+  border-radius: 4px;
+  background: #00000060;
+  padding: 4px;
+  cursor: pointer;
+  display: flex;
+  flex-direction: column;
+}
+
+.img-thumbnail {
+  width: 100%;
+  flex: 1;
+  object-fit: cover;
+  border-radius: 4px;
+}
+
+.img-desc {
+  height: 32px;
+  margin-top: 4px;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  gap: 2px;
+}
+
+.desc-line1 {
+  font-size: 10px;
+  color: #9cc9ff;
+  text-align: center;
+  margin: 0;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.desc-line2 {
+  font-size: 11px;
+  color: #e0efff;
+  text-align: center;
+  margin: 0;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.right-data-panel {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  gap: 6px;
+}
+
+.tab-nav {
+  display: flex;
+  height: 32px;
+  background: rgba(25, 55, 128, 0.2);
+  border-radius: 4px;
+  overflow: hidden;
+}
+
+.tab-item {
+  flex: 1;
+  line-height: 32px;
+  text-align: center;
+  font-size: 13px;
+  color: #9cc9ff;
+  cursor: pointer;
+}
+
+.tab-item.active {
+  background: #49bbff40;
+  color: #49bbff;
+  border-bottom: 2px solid #49bbff;
+}
+
+.data-list {
+  flex: 1;
+  border: 1px solid #2a5bda;
+  border-radius: 4px;
+  background: #00000060;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+}
+
+.data-header {
+  display: flex;
+  height: 30px;
+  line-height: 30px;
+  background: rgba(25, 55, 128, 0.3);
+  font-size: 12px;
+}
+
+.data-row {
+  display: flex;
+  height: 30px;
+  line-height: 30px;
+  border-bottom: 1px dashed #1f3e8a;
+  font-size: 11px;
+  color: #a0d3ff;
+}
+
+.data-col {
+  flex: 1;
+  text-align: center;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.data-col:first-child {
+  flex: 0 1 50px;
+}
+
+.严重 {
+  color: #ff4757;
+  font-weight: 500;
+}
+
+.一般 {
+  color: #ff9500;
+  font-weight: 500;
+}
+
+.img-modal {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100vw;
+  height: 100vh;
+  background: rgba(0, 0, 0, 0.85);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 9999;
+}
+
+.modal-content {
+  background: #0a102b;
+  border: 2px solid #49bbff;
+  border-radius: 8px;
+  width: 700px;
+  padding: 12px;
+}
+
+.modal-header {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 8px;
+  font-size: 15px;
+  color: #49bbff;
+}
+
+.close-btn {
+  background: none;
+  border: none;
+  color: #fff;
+  font-size: 20px;
+  cursor: pointer;
+}
+
+.modal-img {
+  width: 100%;
+  height: 400px;
+  object-fit: contain;
+  background: #000;
+  border-radius: 4px;
+}
+
+.modal-info {
+  text-align: center;
+  margin-top: 8px;
+  font-size: 13px;
+  color: #e0efff;
+}
+
+/* 统一美化滚动条(深色科技蓝风格) */
+::-webkit-scrollbar {
+  width: 6px;
+  height: 6px;
+}
+
+::-webkit-scrollbar-track {
+  background: rgba(25, 55, 128, 0.2);
+  border-radius: 3px;
+}
+
+::-webkit-scrollbar-thumb {
+  background: #49bbff;
+  border-radius: 3px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+  background: #6cccff;
+}
+
+/* 图片列表区域专属滚动条 */
+.img-list::-webkit-scrollbar {
+  width: 6px;
+}
+
+.img-list::-webkit-scrollbar-track {
+  background: rgba(8, 18, 48, 0.5);
+}
+
+.img-list::-webkit-scrollbar-thumb {
+  background: #49bbff;
+}
+
+/* 新告警插入高亮动画(醒目版) */
+@keyframes alarmFlash {
+  0% {
+    background: rgba(73, 187, 255, 0.4) !important;
+    transform: scale(1.02);
+  }
+
+  30% {
+    background: rgba(255, 70, 70, 0.5) !important;
+  }
+
+  60% {
+    background: rgba(73, 187, 255, 0.4) !important;
+  }
+
+  100% {
+    background: initial !important;
+    transform: scale(1);
+  }
+}
+
+/* 新告警动画类名 */
+.alarm-item.new {
+  animation: alarmFlash 1.8s ease-out forwards;
+  position: relative;
+  z-index: 10;
+}
+
+/* 1. 红色高等级告警 持续闪烁(监控级重点提醒) */
+@keyframes redAlarmBlink {
+  0% {
+    background: rgba(255, 60, 60, 0.2);
+  }
+
+  50% {
+    background: rgba(255, 60, 60, 0.5);
+  }
+
+  100% {
+    background: rgba(255, 60, 60, 0.2);
+  }
+}
+
+/* 2. 新告警 高亮缩放闪烁(插入时醒目提醒) */
+@keyframes newAlarmFlash {
+  0% {
+    background: rgba(73, 187, 255, 0.4);
+    transform: scale(1.02);
+    box-shadow: 0 0 8px rgba(73, 187, 255, 0.8);
+  }
+
+  30% {
+    background: rgba(255, 140, 40, 0.5);
+    box-shadow: 0 0 12px rgba(255, 140, 40, 0.8);
+  }
+
+  60% {
+    background: rgba(73, 187, 255, 0.4);
+    box-shadow: 0 0 8px rgba(73, 187, 255, 0.8);
+  }
+
+  100% {
+    background: initial;
+    transform: scale(1);
+    box-shadow: none;
+  }
+}
+
+/* 绑定动画类名 */
+.alarm-item.red {
+  animation: redAlarmBlink 1.2s infinite ease-in-out;
+  /* 红色告警持续闪 */
+}
+
+.alarm-item.new {
+  animation: newAlarmFlash 1.8s ease-out forwards;
+  /* 新告警单次高亮闪 */
+  position: relative;
+  z-index: 10;
+  /* 新告警浮在最上层 */
+}
+</style>

+ 90 - 0
src/views/vent/monitorManager/windDoorVideo/windDoorVideo.data.ts

@@ -0,0 +1,90 @@
+import { getAssetURL } from "/@/utils/ui"
+
+export const doorList = [
+  { id: 1, name: '五盘区402辅运大巷风门' },
+  { id: 2, name: '六盘区501胶带运输巷风门' },
+  { id: 3, name: '七盘区602回风联络巷风门' },
+  { id: 4, name: '中央变电所进风通道风门' },
+  { id: 5, name: '采区水泵房专用通道风门' }
+]
+
+export const statusData = {
+  normal: 3, offline: 0, fault: 1, warn: 1, carPass: 28, personPass: 15
+}
+
+export const alarmTypeList = [
+  { label: 'AI防夹人预警', value: 'antiClip' },
+  { label: '异物卡堵预警', value: 'foreign' },
+  { label: '密封失效故障', value: 'seal' },
+  { label: '违规停留预警', value: 'stop' },
+  { label: '风门未关严', value: 'unclose' },
+  { label: '车辆撞击故障', value: 'crash' }
+]
+
+// 新增:随机生成告警(模拟后端实时数据)
+export const createRandomAlarm = () => {
+  const doorIds = [1, 2, 3, 4, 5]
+  const alarmTypes = [
+    { type: '防夹人预警', desc: ['危险区域有人闯入,已触发PLC停止', '风门闭合区检测到人员肢体,紧急止停'], level: 'red' },
+    { type: '风门未关严', desc: ['门板闭合缝隙15mm,超标10mm', '门板闭合缝隙12mm,接近阈值'], level: 'yellow' },
+    { type: '异物卡堵', desc: ['门体通道检测到石块异物', '风门轨道有杂物卡滞'], level: 'orange' },
+    { type: '违规停留', desc: ['禁停区有车辆滞留3分钟', '禁停区有人员逗留,已语音提醒'], level: 'gray' },
+    { type: '密封失效', desc: ['风门密封胶条脱落', '风门密封处漏风,压力值异常'], level: 'yellow' },
+    { type: '车辆撞击', desc: ['风门门体疑似受到撞击', '风门门体疑似受到撞击,需检查'], level: 'orange' }
+  ]
+  // 随机取值
+  const randomDoor = doorIds[Math.floor(Math.random() * doorIds.length)]
+  const randomType = alarmTypes[Math.floor(Math.random() * alarmTypes.length)]
+  const randomDesc = randomType.desc[Math.floor(Math.random() * randomType.desc.length)]
+  // 生成当前时间(时分秒)
+  const now = new Date()
+  const h = String(now.getHours()).padStart(2, '0')
+  const m = String(now.getMinutes()).padStart(2, '0')
+  const s = String(now.getSeconds()).padStart(2, '0')
+  const currentTime = `${h}:${m}:${s}`
+  // 返回新告警
+  return {
+    time: currentTime,
+    doorId: randomDoor,
+    type: randomType.type,
+    desc: randomDesc,
+    level: randomType.level
+  }
+}
+
+export const currentImgList = [
+  { id: 1, doorId: 2, type: '车辆通行', license: '3571', imgUrl: getAssetURL('wind-video/car1.jpg') },
+  { id: 2, doorId: 4, type: '风门未关严', gap: 15, imgUrl: getAssetURL('wind-video/gap1.jpg') },
+  { id: 3, doorId: 1, type: '人员通行', personName: '张三、李四', imgUrl: getAssetURL('wind-video/person1.jpg') },
+  { id: 4, doorId: 3, type: '车辆通行', license: '6829', imgUrl: getAssetURL('wind-video/car2.jpg') },
+  { id: 5, doorId: 2, type: '人员通行', personName: '王五', imgUrl: getAssetURL('wind-video/person2.jpg') },
+  { id: 6, doorId: 1, type: '车辆通行', license: '1122', imgUrl: getAssetURL('wind-video/car3.jpg') },
+  { id: 7, doorId: 3, type: '风门未关严', gap: 10, imgUrl: getAssetURL('wind-video/gap2.jpg') },
+  { id: 8, doorId: 5, type: '人员通行', personName: '赵六、陈七', imgUrl: getAssetURL('wind-video/person3.jpg') },
+  { id: 9, doorId: 4, type: '车辆通行', license: '3344', imgUrl: getAssetURL('wind-video/car1.jpg') },
+  { id: 10, doorId: 1, type: '风门未关严', gap: 8, imgUrl: getAssetURL('wind-video/gap3.jpg') },
+]
+
+export const carList =[
+  { doorId: 1, license: '3571', time: '2026-03-27 16:20:18', result: '通行成功' },
+  { doorId: 2, license: '6829', time: '2026-03-27 16:10:25', result: '通行成功' },
+  { doorId: 3, license: '1122', time: '2026-03-27 15:50:33', result: '通行成功' },
+  { doorId: 4, license: '3344', time: '2026-03-27 15:30:41', result: '通行成功' },
+  { doorId: 5, license: '5566', time: '2026-03-27 15:10:50', result: '通行成功' },
+]
+
+export const personList = [
+  { doorId: 2, personInfo: '张三、李四(2人)', time: '2026-03-27 16:15:40', result: '正常通行' },
+  { doorId: 1, personInfo: '王五(1人)', time: '2026-03-27 16:05:22', result: '正常通行' },
+  { doorId: 3, personInfo: '赵六、陈七(2人)', time: '2026-03-27 15:45:11', result: '正常通行' },
+  { doorId: 5, personInfo: '周八、吴九(2人)', time: '2026-03-27 15:25:39', result: '正常通行' },
+  { doorId: 4, personInfo: '郑十、刘一(2人)', time: '2026-03-27 15:05:55', result: '正常通行' },
+]
+
+export const uncloseList = [
+  { doorId: 4, time: '2026-03-27 16:28:15', gap: 15, level: '严重' },
+  { doorId: 3, time: '2026-03-27 16:18:33', gap: 10, level: '一般' },
+  { doorId: 2, time: '2026-03-27 16:08:49', gap: 12, level: '一般' },
+  { doorId: 1, time: '2026-03-27 15:58:12', gap: 8, level: '一般' },
+  { doorId: 5, time: '2026-03-27 15:38:27', gap: 9, level: '一般' },
+]