Просмотр исходного кода

Merge branch 'master' of http://39.97.59.228:8013/hrx/mky-vent-base

bobo04052021@163.com 2 недель назад
Родитель
Сommit
14e28ddecb
89 измененных файлов с 3452 добавлено и 902 удалено
  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. 2 1
      src/views/vent/deviceManager/configurationTable/types.ts
  80. 175 60
      src/views/vent/home/configurable/belt/belt-new.vue
  81. 1 1
      src/views/vent/home/configurable/belt/configurable.data.ts
  82. 4 697
      src/views/vent/home/configurable/belt/threejs/belt.threejs.ts
  83. 143 143
      src/views/vent/home/configurable/components/belt/FireSensorAnalysis.vue
  84. 54 0
      src/views/vent/monitorManager/fanLocalVideo/fanLocalVideo.data.ts
  85. 990 0
      src/views/vent/monitorManager/fanLocalVideo/index.vue
  86. 912 0
      src/views/vent/monitorManager/mainLocalWind/index.vue
  87. 43 0
      src/views/vent/monitorManager/mainLocalWind/mainLocalWind.data.ts
  88. 1038 0
      src/views/vent/monitorManager/windDoorVideo/index.vue
  89. 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


+ 2 - 1
src/views/vent/deviceManager/configurationTable/types.ts

@@ -108,7 +108,8 @@ export interface ModuleData {
         | 'warning_result'
         | 'vehicle_co_analysis'
         | 'sprayCtrl'
-        | 'cameraList';
+        | 'cameraList'
+        | 'CameraListTest';
       /** 分区大小 */
       basis: string;
       overflow?: boolean;

+ 175 - 60
src/views/vent/home/configurable/belt/belt-new.vue

@@ -2,7 +2,7 @@
 <template>
   <div class="company-home">
     <customHeader> 皮带巷智能管控 </customHeader>
-    <div class="modal-box" id="modalBox" v-if="pageType !== 'history'">
+    <div class="modal-box" id="modalBox" v-if="pageType !== 'history' && isInitModal">
       <Three3D ref="three3D" :modalName="'pidai'" class="modal-3d" @success="initModalAnimate" />
       <div class="modal-css3d" id="css3dContainer"> </div>
     </div>
@@ -32,41 +32,45 @@
 </template>
 
 <script setup lang="ts">
-import { onMounted, ref, watch, nextTick, onUnmounted } from 'vue';
+import { onMounted, ref, watch, nextTick } from 'vue';
 import customHeader from './components/customHeader-belt.vue';
 import { useInitConfigs, useInitPage } from '../hooks/useInit';
 import { testBeltNew, testYjkf, testSpary } from './configurable.data';
 import ModuleCommon from './components/ModuleCommon.vue';
-import Three3D from '/@/views/vent/home/configurable/components/three3d.vue';
+import Three3D from '/@/views/vent/home/configurable/components/three3D.vue';
 import BeltNav from './components/BeltNav.vue';
-import { useRoute } from 'vue-router';
+import { useRouter, useRoute } from 'vue-router';
 import { getSystem, getMonitorAndAlertBelt, getDevice } from './configurable.api';
 import { modalAnimate } from './threejs/belt.threejs';
 import History from './components/detail/history.vue';
-
+// 初始化配置
 const { configs, fetchConfigs } = useInitConfigs();
-const { updateData, data } = useInitPage('皮带巷三级防灭火系统');
-const route = useRoute();
-
-// 锁:只初始化一次
-const initialized = ref(false);
-const pageType = ref('fire_risk_warn');
-
-// 缓存所有页面数据(核心:切换不闪烁)
+const { updateEnhancedConfigs, updateData, data } = useInitPage('皮带巷三级防灭火系统');
+const isInitModal = ref(false);
 const pageCache = ref({
   fire_risk_warn: { configs: testBeltNew },
   emergencyControl: { configs: testYjkf },
   sprayControl: { configs: testSpary },
 });
 
-let intervalTimer = null;
+const pageType = ref('fire_risk_warn');
+const route = useRoute();
+const modalMonitorData = ref({});
+// 下拉框选项
 const beltOptions = [
   { id: '1', beltName: '主运巷皮带 1' },
   { id: '2', beltName: '主运巷皮带 2' },
 ];
+
 const selectedBeltId = ref('1');
 
-// 风险等级样式
+// 下拉框切换处理
+function handleBeltChange(id: string) {
+  selectedBeltId.value = id;
+  refresh();
+}
+
+// 风险等级样式映射
 function getLevelClass(level: string) {
   switch (level) {
     case '重大风险':
@@ -79,27 +83,74 @@ function getLevelClass(level: string) {
       return '';
   }
 }
+// 刷新数据
+async function refresh() {
+  // await fetchConfigs('sys_Leather');
 
-// 刷新:只更新数据,不抖动
-async function refresh(isFirst = false) {
-  await fetchConfigs('sys_Leather');
+  //   if (pageType.value === 'fire_risk_warn') {
+  //     configs.value = testBeltNew;
+  //     const res = await getMonitorAndAlertBelt({
+  //       sysId: '1637983899775242242',
+  //       dataList: 'fire_risk_warn,warn_result,vehicle_co_correlate',
+  //     });
+  //     updateData(res);
+  //   } else if (pageType.value === 'emergencyControl') {
+  //     configs.value = testYjkf;
+  //     const res = await getSystem({
+  //       devicetype: 'sys',
+  //       systemID: '1637983899775242242',
+  //       type: 'ventS',
+  //     });
+  //     updateData(res);
+  //   } else if (pageType.value === 'sprayControl') {
+  //     const params = {
+  //       devicetype: 'sys',
+  //       systemID: '2028657172566073346',
+  //     };
+  //     Promise.resolve(getDevice(params)).then((originalData) => {
+  //       updateData(originalData);
+  //       const sprayData: any[] = [];
+  //       if (data.value?.msgTxt) {
+  //         data.value.msgTxt.forEach((item) => {
+  //           const hasSprayAuto = item.type && item.type.toLowerCase().includes('spray_auto');
+  //           if (hasSprayAuto) {
+  //             sprayData.push({
+  //               ...item,
+  //               ...item.readData,
+  //             });
+  //           }
+  //         });
+  //       }
+  //       data.value.sprayData = sprayData;
+  //       updateData(data.value);
+  //     });
+  //     configs.value = testSpary;
+  //   }
+  //   if (isFirst) initialized.value = true;
 
-  if (pageType.value === 'fire_risk_warn') {
-    configs.value = testBeltNew;
-    const res = await getMonitorAndAlertBelt({
+  // 由于模型中需要用到风门的监测数据,这里进行公共调用(后期精确调用风门)
+  const modalRes = {};
+  const systemParams = {
+    devicetype: 'sys',
+    systemID: '1637983899775242242',
+    type: 'ventS',
+  };
+  const resSys = await getSystem(systemParams);
+  Object.assign(modalRes, resSys);
+  if (pageType.value == 'fire_risk_warn') {
+    configs.value = [...testBeltNew];
+    const params = {
       sysId: '1637983899775242242',
       dataList: 'fire_risk_warn,warn_result,vehicle_co_correlate',
-    });
-    updateData(res);
-  } else if (pageType.value === 'emergencyControl') {
-    configs.value = testYjkf;
-    const res = await getSystem({
-      devicetype: 'sys',
-      systemID: '1637983899775242242',
-      type: 'ventS',
-    });
-    updateData(res);
-  } else if (pageType.value === 'sprayControl') {
+    };
+    const resWarn = await getMonitorAndAlertBelt(params);
+    updateData(resWarn);
+    Object.assign(modalRes, resWarn);
+  } else if (pageType.value == 'emergencyControl') {
+    //
+    updateData(resSys);
+    configs.value = [...testYjkf];
+  } else if (pageType.value == 'sprayControl') {
     const params = {
       devicetype: 'sys',
       systemID: '2028657172566073346',
@@ -121,50 +172,70 @@ async function refresh(isFirst = false) {
       data.value.sprayData = sprayData;
       updateData(data.value);
     });
-    configs.value = testSpary;
+    configs.value = [...testSpary];
+    console.log(configs.value);
+  } else {
+    configs.value = testBeltNew;
   }
-  // 第一次加载完再渲染,绝不抖动
-  if (isFirst) initialized.value = true;
+  modalMonitorData.value = modalRes;
+}
+
+// // 定时刷新
+function initInterval() {
+  setInterval(() => {
+    refresh();
+  }, 60000);
 }
 
-// 导航切换:从缓存取,瞬间渲染
 async function changePage(pageTypeStr) {
   const target = pageTypeStr || route.query.pageType || 'fire_risk_warn';
   if (pageType.value === target) return;
-
-  // 先切页面
   pageType.value = target;
-
-  // 从缓存拿配置 → 瞬间显示,绝不空白
   configs.value = pageCache.value[target]?.configs || testBeltNew;
-
   await nextTick();
-  await refresh(); // 后台刷新数据,不影响视图
+  await refresh();
 }
 
-// 3D 动画
+// watch(
+//   // 监听动态路由参数 :type
+//   () => route.params.type,
+//   (newVal) => {
+//     if (newVal) {
+//       console.log('切换页面类型:', newVal);
+//       refresh(); // 切换路由自动刷新
+//     }
+//   }
+// );
+
 function initModalAnimate(modal) {
+  console.log('初始化模型', modal);
   modal.isRender = true;
-  modalAnimate(modal);
+  modalAnimate(modal, modalMonitorData);
 }
 
-// 路由监听
 watch(
   () => route.query.pageType,
-  (val) => {
-    if (val && !initialized.value) changePage(val);
+  (newQueryType) => {
+    if (newQueryType) {
+      changePage(newQueryType as string);
+    }
   },
-  { immediate: true }
+  { immediate: true } // 初始化立刻执行
+);
+
+watch(
+  () => modalMonitorData.value,
+  (newData, oldData) => {
+    if (newData && !Object.keys(oldData).length) {
+      isInitModal.value = true;
+    }
+  }
 );
 
-// 挂载:只执行一次,不重复渲染
 onMounted(async () => {
-  configs.value = testBeltNew; // 初始直接给默认配置
-  await refresh(true);
-  intervalTimer = setInterval(() => refresh(), 60000);
+  await refresh();
+  initInterval();
 });
-
-onUnmounted(() => clearInterval(intervalTimer));
 </script>
 <style lang="less" scoped>
 .company-home {
@@ -194,14 +265,57 @@ onUnmounted(() => clearInterval(intervalTimer));
     background: url('/@/assets/images/beltFire/mainbj.png') no-repeat;
     background-size: 100% 100%;
     position: relative;
-    overflow: hidden;
-    .box-container {
-      position: relative;
-      width: 100%;
-      height: 100%;
+    width: 100%;
+    height: 100%;
+  }
+}
+
+.center-warning-container {
+  position: absolute;
+  left: 50%;
+  transform: translateX(-50%);
+  top: 50%;
+  width: 600px;
+  height: 200px;
+  background-color: rgba(0, 0, 0, 0.7);
+  border-radius: 10px;
+  padding: 15px;
+  box-shadow: 0 0 20px rgba(0, 255, 255, 0.3);
+  z-index: 5;
+  color: #fff;
+
+  .warning-header {
+    font-size: 18px;
+    font-weight: bold;
+    margin-bottom: 10px;
+    color: #ff6b6b;
+  }
+
+  .warning-list {
+    width: 100%;
+    height: 100%;
+    overflow-y: auto;
+    display: flex;
+    flex-direction: column;
+    gap: 8px;
+  }
+
+  .warning-item {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 8px;
+    background-color: rgba(0, 0, 0, 0.5);
+    border-radius: 5px;
+    border-left: 4px solid #ff6b6b;
+
+    .warning-time {
+      font-size: 14px;
+      color: #ccc;
     }
   }
 
+  // 中间预警结果区
   .center-warning-container {
     position: absolute;
     left: 50%;
@@ -283,6 +397,7 @@ onUnmounted(() => clearInterval(intervalTimer));
     }
   }
 
+  // 巷道示意图
   .belt-diagram {
     position: absolute;
     left: 50%;
@@ -307,4 +422,4 @@ onUnmounted(() => clearInterval(intervalTimer));
   position: absolute;
   z-index: 1;
 }
-</style>
+</style>

+ 1 - 1
src/views/vent/home/configurable/belt/configurable.data.ts

@@ -1133,7 +1133,7 @@ export const testSpary: Config[] = [
   {
     deviceType: '',
     moduleName: '摄像头视频信号',
-    pageType: '',
+    pageType: 'beltYjkf',
     moduleData: {
       header: {
         show: false,

Разница между файлами не показана из-за своего большого размера
+ 4 - 697
src/views/vent/home/configurable/belt/threejs/belt.threejs.ts


+ 143 - 143
src/views/vent/home/configurable/components/belt/FireSensorAnalysis.vue

@@ -36,167 +36,167 @@
 </template>
 
 <script setup lang="ts">
-import { computed } from 'vue';
-import { getFormattedText } from '../../hooks/helper';
+  import { computed } from 'vue';
+  import { getFormattedText } from '../../hooks/helper';
 
-const props = defineProps<{
-  config: Array<{
-    title: string;
-    readFrom: string;
-    items: Array<{
-      label: string;
-      code: string;
-      trans?: String;
-      status?: string;
-      info?: string;
+  const props = defineProps<{
+    config: Array<{
+      title: string;
+      readFrom: string;
+      items: Array<{
+        label: string;
+        code: string;
+        trans?: String;
+        status?: string;
+        info?: string;
+      }>;
     }>;
-  }>;
-  data: {
-    [key: string]: any;
-  };
-}>();
+    data: {
+      [key: string]: any;
+    };
+  }>();
 
-/**
- * 解析状态并返回对应的 CSS 类名
- */
-const parseStatus = (statusStr: string) => {
-  return statusStr === 'true' ? 'status-danger' : 'status-normal';
-};
+  /**
+   * 解析状态并返回对应的 CSS 类名
+   */
+  const parseStatus = (statusStr: string) => {
+    return statusStr === 'true' ? 'status-danger' : 'status-normal';
+  };
 </script>
 
 <style scoped lang="less">
-/* 全局面板样式 */
-.fire-safety-panel {
-  border-radius: 8px;
-  padding: 15px;
-  color: #fff;
-  font-family: 'Microsoft YaHei', sans-serif;
-  padding-top: 10px;
-}
-/* 列表容器 */
-.sensor-list {
-  display: flex;
-  flex-direction: column;
-  gap: 15px; /* 组间距 */
-}
-
-/* 分组卡片 */
-.sensor-group {
-  background: url('@/assets/images/beltFire/fireMonitor/2-1.png') no-repeat;
-  background-size: 100% 100%;
-}
-
-/* 组标题 */
-.group-title {
-  background: url('@/assets/images/beltFire/fireMonitor/2-2.png') no-repeat;
-  background-size: 35% 100%;
-  color: #fff;
-  font-size: 12px;
-  font-weight: bold;
-  font-style: italic;
-  margin-bottom: 8px;
-  padding-bottom: 5px;
-  padding-left: 10px;
-}
-
-/* 列表项 */
-.sensor-item {
-  background: url('@/assets/images/beltFire/fireMonitor/2-4.png') no-repeat;
-  background-size: 100% 100%;
-  display: flex;
-  align-items: center;
-  margin-bottom: 5px;
-  font-size: 13px;
-  color: #c0d0e0;
-  overflow-y: auto;
-}
-.item-status {
-  width: 90%;
-  display: flex;
-  flex-direction: row;
-  justify-content: space-between;
-}
-.item-icon {
-  background: url('@/assets/images/beltFire/fireMonitor/2-5.svg') no-repeat;
-  background-size: 100% 100%;
-  width: 20px;
-  height: 20px;
-  margin-left: -7px;
-}
-.item-label {
-  height: 30px;
-  line-height: 30px;
-  text-align: right;
-  color: #fff;
-  font-size: 11px;
-}
-
-.item-content {
-  flex: 1;
-  display: flex;
-  align-items: center;
-  .label {
-    color: #fafafa;
+  /* 全局面板样式 */
+  .fire-safety-panel {
+    border-radius: 8px;
+    padding: 15px;
+    color: #fff;
+    font-family: 'Microsoft YaHei', sans-serif;
+    padding-top: 10px;
+  }
+  /* 列表容器 */
+  .sensor-list {
+    display: flex;
+    flex-direction: column;
+    gap: 15px; /* 组间距 */
   }
-}
-
-.item-value {
-  margin-right: 10px;
-  font-size: 12px;
-  color: #36dae7;
-}
-.item-info {
-  color: #889db0;
-  font-size: 11px;
-}
 
-/* 状态指示灯 */
-.status-dot {
-  display: inline-block;
-  width: 10px;
-  height: 10px;
-  border-radius: 50%;
-  margin-left: 5px;
-  margin-top: 10px;
-  animation: pulse 2s infinite;
+  /* 分组卡片 */
+  .sensor-group {
+    background: url('@/assets/images/beltFire/fireMonitor/2-1.png') no-repeat;
+    background-size: 100% 100%;
+  }
 
-  &.status-normal {
-    background-color: #00ff00;
-    box-shadow: 0 0 6px 2px rgba(104, 255, 45, 0.6);
+  /* 组标题 */
+  .group-title {
+    background: url('@/assets/images/beltFire/fireMonitor/2-2.png') no-repeat;
+    background-size: 35% 100%;
+    color: #fff;
+    font-size: 12px;
+    font-weight: bold;
+    font-style: italic;
+    margin-bottom: 8px;
+    padding-bottom: 5px;
+    padding-left: 10px;
   }
 
-  &.status-danger {
-    background-color: #ff4d4d;
-    box-shadow: 0 0 6px 2px rgb(255, 0, 0);
-    animation: flash 1s infinite;
+  /* 列表项 */
+  .sensor-item {
+    background: url('@/assets/images/beltFire/fireMonitor/2-4.png') no-repeat;
+    background-size: 100% 100%;
+    display: flex;
+    align-items: center;
+    margin-bottom: 5px;
+    font-size: 13px;
+    color: #c0d0e0;
+    overflow-y: auto;
+  }
+  .item-status {
+    width: 90%;
+    display: flex;
+    flex-direction: row;
+    justify-content: space-between;
+  }
+  .item-icon {
+    background: url('@/assets/images/beltFire/fireMonitor/2-5.svg') no-repeat;
+    background-size: 100% 100%;
+    width: 20px;
+    height: 20px;
+    margin-left: -7px;
+  }
+  .item-label {
+    height: 30px;
+    line-height: 30px;
+    text-align: right;
+    color: #fff;
+    font-size: 11px;
   }
-}
 
-/* 动画效果 */
-@keyframes pulse {
-  0% {
-    transform: scale(1);
-    opacity: 1;
+  .item-content {
+    flex: 1;
+    display: flex;
+    align-items: center;
+    .label {
+      color: #fafafa;
+    }
   }
-  50% {
-    transform: scale(1.2);
-    opacity: 0.8;
+
+  .item-value {
+    margin-right: 10px;
+    font-size: 12px;
+    color: #36dae7;
   }
-  100% {
-    transform: scale(1);
-    opacity: 1;
+  .item-info {
+    color: #889db0;
+    font-size: 11px;
   }
-}
 
-@keyframes flash {
-  0% {
-    opacity: 1;
+  /* 状态指示灯 */
+  .status-dot {
+    display: inline-block;
+    width: 10px;
+    height: 10px;
+    border-radius: 50%;
+    margin-left: 5px;
+    margin-top: 10px;
+    animation: pulse 2s infinite;
+
+    &.status-normal {
+      background-color: #00ff00;
+      box-shadow: 0 0 6px 2px rgba(104, 255, 45, 0.6);
+    }
+
+    &.status-danger {
+      background-color: #ff4d4d;
+      box-shadow: 0 0 6px 2px rgb(255, 0, 0);
+      animation: flash 1s infinite;
+    }
   }
-  50% {
-    opacity: 0.5;
+
+  /* 动画效果 */
+  @keyframes pulse {
+    0% {
+      transform: scale(1);
+      opacity: 1;
+    }
+    50% {
+      transform: scale(1.2);
+      opacity: 0.8;
+    }
+    100% {
+      transform: scale(1);
+      opacity: 1;
+    }
   }
-  100% {
-    opacity: 1;
+
+  @keyframes flash {
+    0% {
+      opacity: 1;
+    }
+    50% {
+      opacity: 0.5;
+    }
+    100% {
+      opacity: 1;
+    }
   }
-}
 </style>

+ 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: '一般' },
+]

Некоторые файлы не были показаны из-за большого количества измененных файлов