瀏覽代碼

通风预警-绿色版本优化-提交

lxh 2 天之前
父節點
當前提交
d952dfee45
共有 21 個文件被更改,包括 1310 次插入 和 1160 次删除
  1. 3 0
      pnpm-workspace.yaml
  2. 38 7
      src/views/vent/home/configurable/components/detail/CustomChart.vue
  3. 82 90
      src/views/vent/home/configurable/components/green-right-tag.vue
  4. 1 0
      src/views/vent/home/configurable/configurable.data.ts
  5. 11 0
      src/views/vent/home/configurable/green/components/CustomTable-green.vue
  6. 28 11
      src/views/vent/home/configurable/green/components/MiniBoard-green.vue
  7. 6 4
      src/views/vent/home/configurable/green/components/ModuleOriginal-green.vue
  8. 64 92
      src/views/vent/home/configurable/green/components/center-area-green.vue
  9. 10 22
      src/views/vent/home/configurable/green/components/content-green.vue
  10. 231 217
      src/views/vent/home/configurable/green/components/dz-chart.vue
  11. 40 16
      src/views/vent/home/configurable/green/components/dz-list.vue
  12. 162 155
      src/views/vent/home/configurable/green/components/dz-risk.vue
  13. 146 138
      src/views/vent/home/configurable/green/components/green-nav.vue
  14. 5 3
      src/views/vent/home/configurable/green/components/module-green.vue
  15. 6 2
      src/views/vent/home/configurable/green/components/moduleBottom-green.vue
  16. 5 3
      src/views/vent/home/configurable/green/components/moduleDual-green.vue
  17. 6 2
      src/views/vent/home/configurable/green/components/moduleLeft-green.vue
  18. 113 133
      src/views/vent/home/configurable/green/dust-green.vue
  19. 155 120
      src/views/vent/home/configurable/green/fire-green.vue
  20. 117 106
      src/views/vent/home/configurable/green/fusion-warn-green.vue
  21. 81 39
      src/views/vent/home/configurable/green/vent-green.vue

+ 3 - 0
pnpm-workspace.yaml

@@ -1,3 +1,6 @@
+packages:
+  - '.'
+
 allowBuilds:
   '@swc/core': true
   '@vue-office/docx': true

+ 38 - 7
src/views/vent/home/configurable/components/detail/CustomChart.vue

@@ -82,6 +82,8 @@
         end: e.end,
         height: 15,
         bottom: 15,
+        // 配置 showDetail: false 时隐藏缩放组件内的明细文字
+        showDetail: e.showDetail !== false,
       };
     });
 
@@ -531,8 +533,7 @@
             dataZoom: baseDataZoom,
             type: 'category',
             axisLabel: {
-              // color: textStyle.color,
-              color: '#ccfbff',
+              color: textStyle.color,
               interval: 0,
               width: baseDataZoom.length ? 100 : 800 / get(baseSeries, '[0].data.length', 1),
               overflow: 'break',
@@ -548,9 +549,11 @@
                 opacity: i === 0 ? 0.1 : 0,
               },
             },
+            nameTextStyle: {
+              color: textStyle.color,
+            },
             axisLabel: {
-              // color: textStyle.color,
-              color: '#ffffffcc',
+              color: textStyle.color,
             },
           };
         }),
@@ -621,8 +624,7 @@
             ...e,
             type: 'category',
             axisLabel: {
-              // color: textStyle.color,
-              color: '#ccfbff',
+              color: textStyle.color,
               width: 100,
               overflow: 'break',
             },
@@ -636,8 +638,11 @@
                 opacity: i === 0 ? 0.1 : 0,
               },
             },
+            nameTextStyle: {
+              color: textStyle.color,
+            },
             axisLabel: {
-              color: '#ffffffcc',
+              color: textStyle.color,
             },
           };
         }),
@@ -679,6 +684,7 @@
             ...e,
             type: 'category',
             axisLabel: {
+              color: textStyle.color,
               width: 100,
               overflow: 'break',
             },
@@ -698,6 +704,10 @@
                 opacity: i === 0 ? 0.1 : 0,
               },
             },
+            axisLabel: {
+              ...(e as any).axisLabel,
+              color: textStyle.color,
+            },
           };
         }),
         series: baseSeries.map((serie) => {
@@ -782,6 +792,7 @@
             type: 'category',
             boundaryGap: false,
             axisLabel: {
+              color: textStyle.color,
               width: 100,
               overflow: 'break',
             },
@@ -790,6 +801,12 @@
         yAxis: yAxis.map((e, i) => {
           return {
             ...e,
+            nameTextStyle: {
+              color: textStyle.color,
+            },
+            axisLabel: {
+              color: textStyle.color,
+            },
             splitLine: {
               lineStyle: {
                 color: '#fff',
@@ -946,6 +963,7 @@
             ...e,
             type: 'category',
             axisLabel: {
+              color: textStyle.color,
               interval: 0,
               width: (domWidth - 100) / get(baseSeries, '[0].data.length', 1),
               overflow: 'break',
@@ -955,6 +973,12 @@
         yAxis: yAxis.map((e, i) => {
           return {
             ...e,
+            nameTextStyle: {
+              color: textStyle.color,
+            },
+            axisLabel: {
+              color: textStyle.color,
+            },
             splitLine: {
               lineStyle: {
                 opacity: i === 0 ? 0.1 : 0,
@@ -1049,6 +1073,7 @@
             ...e,
             type: 'category',
             axisLabel: {
+              color: textStyle.color,
               interval: 0,
               width: (domWidth - 100) / get(baseSeries, '[0].data.length', 1),
               overflow: 'break',
@@ -1058,6 +1083,12 @@
         yAxis: yAxis.map((e, i) => {
           return {
             ...e,
+            nameTextStyle: {
+              color: textStyle.color,
+            },
+            axisLabel: {
+              color: textStyle.color,
+            },
             splitLine: {
               lineStyle: {
                 opacity: i === 0 ? 0.1 : 0,

+ 82 - 90
src/views/vent/home/configurable/components/green-right-tag.vue

@@ -1,121 +1,113 @@
 <!-- eslint-disable vue/multi-word-component-names -->
 <template>
   <div class="greenRightTag">
-    <div class="wind-tag" v-for="(item, index) in tagList">
-      <div class=wind-tag-icon></div>
-      <div class="wind-tag-label">{{ item.label }}</div>
+    <div class="wind-tag" v-for="(item, index) in tagList" :key="index">
+      <div class="wind-tag-icon"></div>
+      <div class="wind-tag-label" :title="item.label">{{ item.label }}</div>
       <div class="wind-tag-val">{{ item.value }}</div>
     </div>
   </div>
 </template>
 <script lang="ts" setup>
-import { onMounted, onUnmounted, ref, watch, computed } from 'vue';
-
-let tagList = ref<any[]>([
-  { label: '总回风量(m³/min)', value: '25870' },
-  { label: '总进风量(m³/min)', value: '24989' },
-  { label: '计划风量(m³/min)', value: '348' },
-  { label: '有效风量率', value: '87.3%' },
-
-])
-
+  import { ref } from 'vue';
+
+  const tagList = ref<any[]>([
+    { label: '总回风量(m³/min)', value: '25870' },
+    { label: '总进风量(m³/min)', value: '24989' },
+    { label: '计划风量(m³/min)', value: '348' },
+    { label: '有效风量率', value: '87.3%' },
+  ]);
 </script>
 <style lang="less" scoped>
-@import '/@/design/theme.less';
-
-@font-face {
-  font-family: 'douyuFont';
-  src: url('/@/assets/font/douyuFont.otf');
-}
+  @import '/@/design/theme.less';
 
-.greenRightTag {
-  position: relative;
-  width: 100%;
-  height: 100%;
-  display: flex;
-  flex-direction: column;
-  justify-content: space-between;
-  align-items: center;
+  @font-face {
+    font-family: 'douyuFont';
+    src: url('/@/assets/font/douyuFont.otf');
+  }
 
-  .wind-tag {
+  .greenRightTag {
     position: relative;
-    width: 110px;
-    height: 124px;
-    background: url('../../../../../assets//images/home-green/green-wind-tag.png') no-repeat;
-    background-size: 100% 100%;
-
-    &:nth-child(1) {
-      .wind-tag-icon {
-        position: absolute;
-        width: 37px;
-        height: 37px;
-        background: url(/src/assets/images/home-green/green-zhfl.png) no-repeat;
-        left: 50%;
-        top: 16px;
-        transform: translate(-50%, 0);
+    width: 100%;
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    justify-content: space-between;
+    align-items: center;
+    box-sizing: border-box;
+    overflow: hidden;
+
+    .wind-tag {
+      position: relative;
+      width: 92px;
+      height: 104px;
+      flex-shrink: 0;
+      background: url('/@/assets/images/home-green/green-wind-tag.png') no-repeat;
+      background-size: 100% 100%;
+      pointer-events: auto;
+
+      &:nth-child(1) {
+        .wind-tag-icon {
+          background: url('/@/assets/images/home-green/green-zhfl.png') no-repeat center;
+          background-size: contain;
+        }
+      }
 
+      &:nth-child(2) {
+        .wind-tag-icon {
+          background: url('/@/assets/images/home-green/green-zjfl.png') no-repeat center;
+          background-size: contain;
+        }
       }
-    }
 
-    &:nth-child(2) {
-      .wind-tag-icon {
-        position: absolute;
-        width: 37px;
-        height: 37px;
-        background: url(/src/assets/images/home-green/green-zjfl.png) no-repeat;
-        left: 50%;
-        top: 16px;
-        transform: translate(-50%, 0);
+      &:nth-child(3) {
+        .wind-tag-icon {
+          background: url('/@/assets/images/home-green/green-jhfl.png') no-repeat center;
+          background-size: contain;
+        }
+      }
 
+      &:nth-child(4) {
+        .wind-tag-icon {
+          background: url('/@/assets/images/home-green/green-yxfl.png') no-repeat center;
+          background-size: contain;
+        }
       }
-    }
 
-    &:nth-child(3) {
       .wind-tag-icon {
         position: absolute;
-        width: 37px;
-        height: 37px;
-        background: url(/src/assets/images/home-green/green-jhfl.png) no-repeat;
+        width: 30px;
+        height: 30px;
         left: 50%;
-        top: 16px;
+        top: 12px;
         transform: translate(-50%, 0);
-
       }
-    }
 
-    &:nth-child(4) {
-      .wind-tag-icon {
+      .wind-tag-label {
         position: absolute;
-        width: 37px;
-        height: 37px;
-        background: url(/src/assets/images/home-green/green-yxfl.png) no-repeat;
-        left: 50%;
-        top: 16px;
-        transform: translate(-50%, 0);
-
+        left: 0;
+        bottom: 28px;
+        width: 100%;
+        padding: 0 2px;
+        box-sizing: border-box;
+        text-align: center;
+        font-size: 11px;
+        color: #fff;
+        overflow: hidden;
+        white-space: nowrap;
+        text-overflow: ellipsis;
       }
-    }
-
-    .wind-tag-label {
-      position: absolute;
-      left: 0;
-      bottom: 34px;
-      width: 100%;
-      text-align: center;
-      font-size: 12px;
-    }
 
-    .wind-tag-val {
-      position: absolute;
-      left: 0;
-      bottom: 5px;
-      width: 100%;
-      text-align: center;
-      font-size: 14px;
-      font-family: 'douyuFont';
-      color: #39f4e0;
+      .wind-tag-val {
+        position: absolute;
+        left: 0;
+        bottom: 4px;
+        width: 100%;
+        text-align: center;
+        font-size: 13px;
+        font-family: 'douyuFont';
+        color: #39f4e0;
+      }
     }
-
   }
-}
 </style>

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

@@ -780,6 +780,7 @@ export const testConfigVent: Config[] = [
             {
               show: true,
               end: 20,
+              showDetail: false,
             },
           ],
           series: [

+ 11 - 0
src/views/vent/home/configurable/green/components/CustomTable-green.vue

@@ -118,6 +118,17 @@
       padding: 5px 0;
       box-sizing: border-box;
       overflow-y: auto;
+      overflow-x: hidden;
+      // 隐藏滚动条,仍可滚动(开采煤层自燃发火特性等)
+      scrollbar-width: none;
+      -ms-overflow-style: none;
+
+      &::-webkit-scrollbar {
+        width: 0 !important;
+        height: 0 !important;
+        display: none;
+      }
+
       .table__content_list_row {
         width: 100%;
         display: flex;

+ 28 - 11
src/views/vent/home/configurable/green/components/MiniBoard-green.vue

@@ -127,10 +127,17 @@ defineEmits(['click']);
 }
 
 .mini-board_B {
-  width: 115px;
+  width: 122px;
+  min-width: 118px;
   height: 50px;
+  padding: 2px 8px 4px;
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  line-height: 1;
   background-image: var(--image-value-bg);
-  background-size: auto 40px;
   background-position: center bottom;
   background-repeat: no-repeat;
   background-size: 100% 100%;
@@ -143,8 +150,16 @@ defineEmits(['click']);
 }
 
 .mini-board_D {
-  // width: 105px;
+  width: 120px;
+  min-width: 118px;
   height: 50px;
+  padding: 2px 8px 4px;
+  box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  line-height: 1;
   background-image: var(--image-mini-board-1);
   background-position: center bottom;
   background-size: 100% 100%;
@@ -268,13 +283,14 @@ defineEmits(['click']);
   color: #3ecca7;
   font-size: 12px;
   font-family: 'douyuFont';
-  height: 28px;
-  line-height: 28px;
+  height: 18px;
+  line-height: 18px;
+  margin-bottom: 0;
 }
 
 .mini-board__label_B {
-  line-height: 16px;
-  height: 16px;
+  line-height: 14px;
+  height: 14px;
   font-size: 12px;
 }
 
@@ -288,15 +304,16 @@ defineEmits(['click']);
 
 .mini-board__value_D {
   font-size: 12px;
-  height: 28px;
-  line-height: 32px;
+  height: 18px;
+  line-height: 18px;
   color: #3ecca7;
   font-family: 'douyuFont';
+  margin-bottom: 0;
 }
 
 .mini-board__label_D {
-  line-height: 18px;
-  height: 18px;
+  line-height: 14px;
+  height: 14px;
   font-size: 12px;
 }
 

+ 6 - 4
src/views/vent/home/configurable/green/components/ModuleOriginal-green.vue

@@ -5,7 +5,7 @@
     :style="style"
     :title="moduleName"
     :visible="visible"
-    :class="{ 'cursor-pointer': !!moduleData.to }"
+    :clickable="!!moduleData.to"
     @close="$emit('close')"
     @click="redirectTo"
   >
@@ -43,9 +43,11 @@
   const selectedData = ref();
 
   const style = computed(() => {
-    const size = props.showStyle.size;
-    const position = props.showStyle.position;
-    return size + position;
+    const size = (props.showStyle.size || '').trim();
+    const position = (props.showStyle.position || '').trim();
+    const sizePart = size && !size.endsWith(';') ? `${size};` : size;
+    const positionPart = position && !position.endsWith(';') ? `${position};` : position;
+    return sizePart + positionPart;
   });
 
   // 根据配置里的定位判断应该使用哪个module组件

+ 64 - 92
src/views/vent/home/configurable/green/components/center-area-green.vue

@@ -3,70 +3,30 @@
     <div class="container">
       <div class="risk-score">
         <div class="item-label">风险得分</div>
-        <div class="item-val">{{ paramDatas.riskLevel !== undefined && paramDatas.riskLevel !== null ? paramDatas.riskLevel : '-' }}</div>
+        <div class="item-val">{{
+          paramDatas.riskLevel !== undefined && paramDatas.riskLevel !== null ? paramDatas.riskLevel : '-'
+        }}</div>
       </div>
       <div class="dust-warn">
         <div class="dust-icon"></div>
-        <div class="item-content" @click="showModal('dust')">
+        <div class="item-content is-clickable" @click="showModal('dust')">
           <div class="ite-label">粉尘监测预警</div>
-          <div class="ite-val">{{
-            paramDatas.fc == 0
-              ? '低风险'
-              : paramDatas.fc == 101
-                ? '低风险'
-                : paramDatas.fc == 102
-                  ? '一般风险'
-                  : paramDatas.fc == 103
-                    ? '较大风险'
-                    : paramDatas.fc == 104
-                      ? '重大风险'
-                      : paramDatas.fc == 201
-                        ? '报警'
-                        : paramDatas.fc
-          }}</div>
+          <div class="ite-val">{{ formatWarnLevel(paramDatas.fc) }}</div>
         </div>
       </div>
       <div class="vent-warn">
         <div class="vent-icon"></div>
-        <div class="item-content" @click="showModal('vent')">
+        <div class="item-content is-clickable" @click="showModal('vent')">
           <div class="ite-label">通风监测预警</div>
-          <div class="ite-val">{{
-            paramDatas.tf == 0
-              ? '低风险'
-              : paramDatas.tf == 101
-                ? '低风险'
-                : paramDatas.tf == 102
-                  ? '一般风险'
-                  : paramDatas.tf == 103
-                    ? '较大风险'
-                    : paramDatas.tf == 104
-                      ? '重大风险'
-                      : paramDatas.tf == 201
-                        ? '报警'
-                        : paramDatas.tf
-          }}</div>
+          <div class="ite-val">{{ formatWarnLevel(paramDatas.tf) }}</div>
         </div>
       </div>
       <div class="gas-warn">
         <div class="gas-icon"></div>
         <div class="item-content1">
-          <div class="ite-content" @click="showModal('gas')">
+          <div class="ite-content is-clickable" @click="showModal('gas')">
             <div class="ite-label">瓦斯监测预警</div>
-            <div class="ite-val">{{
-              paramDatas.ws == 0
-                ? '低风险'
-                : paramDatas.ws == 101
-                  ? '低风险'
-                  : paramDatas.ws == 102
-                    ? '一般风险'
-                    : paramDatas.ws == 103
-                      ? '较大风险'
-                      : paramDatas.ws == 104
-                        ? '重大风险'
-                        : paramDatas.ws == 201
-                          ? '报警'
-                          : paramDatas.ws
-            }}</div>
+            <div class="ite-val">{{ formatWarnLevel(paramDatas.ws) }}</div>
           </div>
           <div class="ite-content">
             <div class="ite-label">矿井瓦斯等级鉴定</div>
@@ -76,56 +36,28 @@
       </div>
       <div class="device-warn">
         <div class="device-icon"></div>
-        <div class="item-content" @click="showModal('sbyj')">
+        <div class="item-content is-clickable" @click="showModal('sbyj')">
           <div class="ite-label">设备监测预警</div>
-          <div class="ite-val">{{
-            paramDatas.sb == 0
-              ? '低风险'
-              : paramDatas.sb == 101
-                ? '低风险'
-                : paramDatas.sb == 102
-                  ? '一般风险'
-                  : paramDatas.sb == 103
-                    ? '较大风险'
-                    : paramDatas.sb == 104
-                      ? '重大风险'
-                      : paramDatas.sb == 201
-                        ? '报警'
-                        : paramDatas.sb
-          }}</div>
+          <div class="ite-val">{{ formatWarnLevel(paramDatas.sb) }}</div>
         </div>
       </div>
       <div class="fire-warn">
         <div class="fire-icon"></div>
-        <div class="item-content" @click="showModal('fire')">
+        <div class="item-content is-clickable" @click="showModal('fire')">
           <div class="ite-label">火灾监测预警</div>
-          <div class="ite-val">{{
-            paramDatas.fire == 0
-              ? '低风险'
-              : paramDatas.fire == 101
-                ? '低风险'
-                : paramDatas.fire == 102
-                  ? '一般风险'
-                  : paramDatas.fire == 103
-                    ? '较大风险'
-                    : paramDatas.fire == 104
-                      ? '重大风险'
-                      : paramDatas.fire == 201
-                        ? '报警'
-                        : paramDatas.fire
-          }}</div>
+          <div class="ite-val">{{ formatWarnLevel(paramDatas.fire) }}</div>
         </div>
       </div>
       <div class="db-warn">
         <div class="db-icon"></div>
-        <div class="item-content" @click="showModal('Minepressure')">
+        <div class="item-content is-clickable" @click="showModal('Minepressure')">
           <div class="ite-label">顶板</div>
           <div class="ite-val">低风险</div>
         </div>
       </div>
       <div class="sz-warn">
         <div class="sz-icon"></div>
-        <div class="item-content" @click="showModal('waterLevel')">
+        <div class="item-content is-clickable" @click="showModal('waterLevel')">
           <div class="ite-label">水灾</div>
           <div class="ite-val">低风险</div>
         </div>
@@ -142,7 +74,8 @@
   import { useRouter } from 'vue-router';
   import { useGlobSetting } from '/@/hooks/setting';
   import RiskWarnLevel from './dz-risk.vue';
-  let props = defineProps({
+
+  const props = defineProps({
     paramData: {
       type: Object,
       default: () => {
@@ -151,11 +84,24 @@
     },
   });
 
-  let router = useRouter();
-  let paramDatas = reactive<any>({});
-  const { sysOrgCode, sysDataType } = useGlobSetting();
+  const router = useRouter();
+  const paramDatas = reactive<any>({});
+  const { sysOrgCode } = useGlobSetting();
+
+  /** 风险等级码转展示文案,未知/空值显示 - */
+  function formatWarnLevel(level: unknown) {
+    if (level === undefined || level === null || level === '') return '-';
+    const map: Record<number | string, string> = {
+      0: '低风险',
+      101: '低风险',
+      102: '一般风险',
+      103: '较大风险',
+      104: '重大风险',
+      201: '报警',
+    };
+    return map[level as number | string] ?? '-';
+  }
 
-  //跳转详情
   function showModal(data) {
     switch (data) {
       case 'vent':
@@ -168,7 +114,6 @@
         router.push('/dust/warn/home');
         break;
       case 'waterLevel':
-        console.log('waterLevel');
         router.push('/warn/waterLevel/home');
         break;
       case 'gas':
@@ -185,8 +130,9 @@
 
   watch(
     () => props.paramData,
-    (newV, oldV) => {
-      paramDatas = newV;
+    (newV) => {
+      if (!newV) return;
+      Object.assign(paramDatas, newV);
     },
     { immediate: true, deep: true }
   );
@@ -344,6 +290,15 @@
       background: var(--image-model_item_content) no-repeat;
       background-size: 100% 100%;
       color: #fff;
+      transition: filter 0.2s ease, opacity 0.2s ease;
+
+      &.is-clickable {
+        cursor: pointer;
+
+        &:hover {
+          filter: brightness(1.15);
+        }
+      }
     }
 
     .item-content1 {
@@ -360,8 +315,11 @@
     .ite-label {
       text-align: center;
       margin-bottom: 10px;
-      padding-top: 4px;
+      padding: 4px 4px 0;
       font-size: 12px;
+      overflow: hidden;
+      white-space: nowrap;
+      text-overflow: ellipsis;
     }
 
     .ite-val {
@@ -369,12 +327,26 @@
       text-align: center;
       font-family: 'douyuFont';
       font-size: 12px;
+      overflow: hidden;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+      padding: 0 4px;
+      box-sizing: border-box;
     }
 
     .ite-content {
       width: 50%;
       height: 100%;
       color: #fff;
+
+      &.is-clickable {
+        cursor: pointer;
+        transition: filter 0.2s ease, opacity 0.2s ease;
+
+        &:hover {
+          filter: brightness(1.15);
+        }
+      }
     }
   }
 </style>

+ 10 - 22
src/views/vent/home/configurable/green/components/content-green.vue

@@ -64,7 +64,7 @@
         </template>
         <!-- 表格部分,这部分通常是占一整个模块的 -->
         <template v-if="config.name === 'table'">
-          <CustomTable class="content__module text-center overflow-auto" :type="config.type" :columns="config.columns" :data="config.data" />
+          <CustomTable class="content__module text-center" :type="config.type" :columns="config.columns" :data="config.data" />
         </template>
         <template v-if="config.name === 'tabs'">
           <CustomTabs class="content__module" :type="config.type" :tab-config="config.items" :overflow="config.overflow" />
@@ -133,7 +133,6 @@
   import MeasureDetail from '../../components/preset/MeasureDetail.vue';
   import CustomTabs from '../../components/preset/CustomTabs.vue';
   import DeviceAlarm from '../../components/preset/DeviceAlarm.vue';
-  import { config } from 'dotenv';
 
   const props = defineProps<{
     data: any;
@@ -396,20 +395,16 @@
     height: 100%;
     color: #fff;
     overflow-y: auto;
-  }
-
-  // .content__module:first-of-type {
-  //   margin-top: 0;
-  // }
-  // .content__module:last-of-type {
-  //   margin-bottom: 0;
-  // }
-  ::-webkit-scrollbar {
-    width: 5px !important;
-  }
+    overflow-x: hidden;
+    // 隐藏模块内滚动条(预警监测等),仍可滚动
+    scrollbar-width: none;
+    -ms-overflow-style: none;
 
-  ::-webkit-scrollbar-thumb {
-    width: 5px !important;
+    &::-webkit-scrollbar {
+      width: 0 !important;
+      height: 0 !important;
+      display: none;
+    }
   }
 
   :deep(.zxm-select:not(.zxm-select-customize-input) .zxm-select-selector) {
@@ -436,11 +431,4 @@
     box-shadow: unset;
   }
 
-  ::-webkit-scrollbar {
-    width: 5px !important;
-  }
-
-  ::-webkit-scrollbar-thumb {
-    width: 5px !important;
-  }
 </style>

+ 231 - 217
src/views/vent/home/configurable/green/components/dz-chart.vue

@@ -5,7 +5,7 @@
 </template>
 
 <script setup lang="ts">
-  import { ref, reactive, watch, onMounted, nextTick } from 'vue';
+  import { ref, watch, onMounted, onUnmounted, nextTick } from 'vue';
   import * as echarts from 'echarts';
   import warnLevel1 from '@/assets/images/home-green/300.png';
   import warnLevel2 from '@/assets/images/home-green/400.png';
@@ -13,7 +13,7 @@
   import warnLevel4 from '@/assets/images/home-green/600.png';
   import warnLevel5 from '@/assets/images/home-green/700.png';
 
-  let props = defineProps({
+  const props = defineProps({
     echartOption: {
       type: Object,
       default: () => {
@@ -28,264 +28,277 @@
     },
   });
 
-  //获取dom元素节点
-  let chartRef = ref<any>();
-  // 类别
-  let category = ref<any[]>([]);
-  let echartY = ref<any[]>([]);
+  const chartRef = ref<HTMLElement | null>(null);
+  const category = ref<any[]>([]);
+  const echartY = ref<any[]>([]);
+  let chartInstance: echarts.ECharts | null = null;
+
+  function ensureChart() {
+    if (!chartRef.value) return null;
+    if (!chartInstance) {
+      chartInstance = echarts.init(chartRef.value);
+    }
+    return chartInstance;
+  }
+
   function getOption() {
-    let myChart = echarts.init(chartRef.value);
-    let option = {
-      xAxis: {
-        max: 10,
-        splitLine: {
-          show: false,
-        },
-        axisLine: {
-          show: false,
-        },
-        axisLabel: {
-          show: false,
-        },
-        axisTick: {
-          show: false,
-        },
-      },
-      grid: {
-        left: 10,
-        top: 10, // 设置条形图的边距
-        right: 10,
-        bottom: 0,
-      },
-      yAxis: [
-        {
-          type: 'category',
-          inverse: false,
-          data: category.value,
-          axisLine: {
+    nextTick(() => {
+      const myChart = ensureChart();
+      if (!myChart) return;
+      const option = {
+        xAxis: {
+          max: 10,
+          splitLine: {
             show: false,
           },
-          axisTick: {
+          axisLine: {
             show: false,
           },
           axisLabel: {
             show: false,
           },
+          axisTick: {
+            show: false,
+          },
         },
-      ],
-      series: [
-        {
-          // 内
-          type: 'bar',
-          barWidth: 18,
-          silent: true,
-          itemStyle: {
-            normal: {
-              color: function (param) {
-                const colors = ['#ff0000', '#ff7700', '#d8b66f', '#dbbf2e', '#61b4c5'];
-                return colors[param.dataIndex % colors.length];
-              },
+        grid: {
+          left: 10,
+          top: 10,
+          right: 10,
+          bottom: 0,
+        },
+        yAxis: [
+          {
+            type: 'category',
+            inverse: false,
+            data: category.value,
+            axisLine: {
+              show: false,
+            },
+            axisTick: {
+              show: false,
+            },
+            axisLabel: {
+              show: false,
             },
           },
-          label: {
-            normal: {
-              formatter: (params) => {
-                let text;
-                if (params.dataIndex == 0) {
-                  text = `{warnLevel5|\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0${params.data.name}}`;
-                } else if (params.dataIndex == 1) {
-                  text = `{warnLevel4|\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0${params.data.name}}`;
-                } else if (params.dataIndex == 2) {
-                  text = `{warnLevel3|\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0${params.data.name}}`;
-                } else if (params.dataIndex == 3) {
-                  text = `{warnLevel2|\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0${params.data.name}}`;
-                } else if (params.dataIndex == 4) {
-                  text = `{warnLevel1|\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0${params.data.name}}`;
-                }
-                return text;
-              },
-              textStyle: {
-                color: '#fff',
-                fontSize: 12,
+        ],
+        series: [
+          {
+            type: 'bar',
+            barWidth: 18,
+            silent: true,
+            itemStyle: {
+              normal: {
+                color: function (param) {
+                  const colors = ['#ff0000', '#ff7700', '#d8b66f', '#dbbf2e', '#61b4c5'];
+                  return colors[param.dataIndex % colors.length];
+                },
               },
-              position: [0, -22],
-              show: true,
-              rich: {
-                //富文本
-                warnLevel1: {
-                  backgroundColor: {
-                    image: warnLevel1,
-                  },
-                  height: 16,
-                  width: 16,
+            },
+            label: {
+              normal: {
+                formatter: (params) => {
+                  let text;
+                  if (params.dataIndex == 0) {
+                    text = `{warnLevel5|\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0${params.data.name}}`;
+                  } else if (params.dataIndex == 1) {
+                    text = `{warnLevel4|\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0${params.data.name}}`;
+                  } else if (params.dataIndex == 2) {
+                    text = `{warnLevel3|\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0${params.data.name}}`;
+                  } else if (params.dataIndex == 3) {
+                    text = `{warnLevel2|\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0${params.data.name}}`;
+                  } else if (params.dataIndex == 4) {
+                    text = `{warnLevel1|\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0${params.data.name}}`;
+                  }
+                  return text;
                 },
-                warnLevel2: {
-                  backgroundColor: {
-                    //这里可以添加你想自定义的图片
-                    image: warnLevel2,
-                  },
-                  height: 16,
-                  width: 16,
+                textStyle: {
+                  color: '#fff',
+                  fontSize: 12,
                 },
-                warnLevel3: {
-                  backgroundColor: {
-                    image: warnLevel3,
+                position: [0, -22],
+                show: true,
+                rich: {
+                  warnLevel1: {
+                    backgroundColor: {
+                      image: warnLevel1,
+                    },
+                    height: 16,
+                    width: 16,
                   },
-                  height: 16,
-                  width: 16,
-                },
-                warnLevel4: {
-                  backgroundColor: {
-                    //这里可以添加你想自定义的图片
-                    image: warnLevel4,
+                  warnLevel2: {
+                    backgroundColor: {
+                      image: warnLevel2,
+                    },
+                    height: 16,
+                    width: 16,
                   },
-                  height: 16,
-                  width: 16,
-                },
-                warnLevel5: {
-                  backgroundColor: {
-                    //这里可以添加你想自定义的图片
-                    image: warnLevel5,
+                  warnLevel3: {
+                    backgroundColor: {
+                      image: warnLevel3,
+                    },
+                    height: 16,
+                    width: 16,
+                  },
+                  warnLevel4: {
+                    backgroundColor: {
+                      image: warnLevel4,
+                    },
+                    height: 16,
+                    width: 16,
+                  },
+                  warnLevel5: {
+                    backgroundColor: {
+                      image: warnLevel5,
+                    },
+                    height: 16,
+                    width: 16,
                   },
-                  height: 16,
-                  width: 16,
                 },
               },
             },
+            data: category.value,
+            z: 1,
+            animationEasing: 'elasticOut',
           },
-          data: category.value,
-          z: 1,
-          animationEasing: 'elasticOut',
-        },
-
-        {
-          // 分隔
-          type: 'pictorialBar',
-          itemStyle: {
-            normal: {
-              color: 'rgba(28, 48, 52)',
+          {
+            type: 'pictorialBar',
+            itemStyle: {
+              normal: {
+                color: 'rgba(28, 48, 52)',
+              },
             },
+            symbolRepeat: 'fixed',
+            symbolMargin: 3,
+            symbol: 'rect',
+            symbolClip: true,
+            symbolSize: [10, 24],
+            symbolPosition: 'start',
+            symbolOffset: [1, -4],
+            symbolBoundingData: 10,
+            data: [10, 10, 10, 10, 10],
+            z: 2,
+            animationEasing: 'elasticOut',
           },
-          symbolRepeat: 'fixed',
-          symbolMargin: 3,
-          symbol: 'rect',
-          symbolClip: true,
-          symbolSize: [10, 24],
-          symbolPosition: 'start',
-          symbolOffset: [1, -4],
-          symbolBoundingData: 10,
-          data: [10, 10, 10, 10, 10],
-          z: 2,
-          animationEasing: 'elasticOut',
-        },
-        {
-          // label
-          type: 'pictorialBar',
-          symbolBoundingData: 10,
-          itemStyle: {
-            normal: {
-              color: 'none',
+          {
+            type: 'pictorialBar',
+            symbolBoundingData: 10,
+            itemStyle: {
+              normal: {
+                color: 'none',
+              },
             },
-          },
-          label: {
-            normal: {
-              formatter: (params) => {
-                let text;
-                if (params.dataIndex == 0) {
-                  text = '{a| ' + params.data + '个}';
-                } else if (params.dataIndex == 1) {
-                  text = '{b| ' + params.data + '个}';
-                } else if (params.dataIndex == 2) {
-                  text = '{c| ' + params.data + '个}';
-                } else if (params.dataIndex == 3) {
-                  text = '{d| ' + params.data + '个}';
-                } else if (params.dataIndex == 4) {
-                  text = '{e| ' + params.data + '个}';
-                } else {
-                  text = '{f| ' + params.data + '个}';
-                }
+            label: {
+              normal: {
+                formatter: (params) => {
+                  let text;
+                  if (params.dataIndex == 0) {
+                    text = '{a| ' + params.data + '个}';
+                  } else if (params.dataIndex == 1) {
+                    text = '{b| ' + params.data + '个}';
+                  } else if (params.dataIndex == 2) {
+                    text = '{c| ' + params.data + '个}';
+                  } else if (params.dataIndex == 3) {
+                    text = '{d| ' + params.data + '个}';
+                  } else if (params.dataIndex == 4) {
+                    text = '{e| ' + params.data + '个}';
+                  } else {
+                    text = '{f| ' + params.data + '个}';
+                  }
 
-                return text;
-              },
-              rich: {
-                a: {
-                  color: '#ff0000',
-                },
-                b: {
-                  color: '#ff7700',
-                },
-                c: {
-                  color: '#d8b66f',
-                },
-                d: {
-                  color: '#dbbf2e',
+                  return text;
                 },
-                e: {
-                  color: '#61b4c5',
-                },
-                f: {
-                  color: '#fff',
+                rich: {
+                  a: {
+                    color: '#ff0000',
+                  },
+                  b: {
+                    color: '#ff7700',
+                  },
+                  c: {
+                    color: '#d8b66f',
+                  },
+                  d: {
+                    color: '#dbbf2e',
+                  },
+                  e: {
+                    color: '#61b4c5',
+                  },
+                  f: {
+                    color: '#fff',
+                  },
                 },
+                position: [350, -12],
+                show: true,
               },
-              position: [350, -12],
-              show: true,
             },
+            data: echartY.value,
+            z: 0,
           },
-          data: echartY.value,
-          z: 0,
-        },
-
-        {
-          name: '外框',
-          type: 'bar',
-          barGap: '-130%', // 设置外框粗细
-          data: [10, 10, 10, 10, 10],
-
-          barWidth: 29,
-          itemStyle: {
-            normal: {
-              barBorderRadius: [0, 0, 0, 0],
-              color: 'transparent', // 填充色
-              barBorderColor: '#3ecca7', // 边框色
-              barBorderWidth: 1, // 边框宽度
+          {
+            name: '外框',
+            type: 'bar',
+            barGap: '-130%',
+            data: [10, 10, 10, 10, 10],
+            barWidth: 29,
+            itemStyle: {
+              normal: {
+                barBorderRadius: [0, 0, 0, 0],
+                color: 'transparent',
+                barBorderColor: '#3ecca7',
+                barBorderWidth: 1,
+              },
             },
+            z: 2,
           },
-
-          z: 2,
-        },
-        {
-          type: 'scatter',
-          name: '条形',
-          symbol: 'roundRect',
-          symbolSize: [6, 14],
-          symbolOffset: [3, -3],
-          symbolKeepAspect: true,
-          itemStyle: {
-            normal: {
-              color: '#3ecca7',
+          {
+            type: 'scatter',
+            name: '条形',
+            symbol: 'roundRect',
+            symbolSize: [6, 14],
+            symbolOffset: [3, -3],
+            symbolKeepAspect: true,
+            itemStyle: {
+              normal: {
+                color: '#3ecca7',
+              },
             },
+            data: [10, 10, 10, 10, 10],
           },
-          data: [10, 10, 10, 10, 10],
-        },
-      ],
-    };
-    myChart.setOption(option);
-    window.onresize = function () {
-      myChart.resize();
-    };
+        ],
+      };
+      myChart.setOption(option as any, true);
+    });
+  }
+
+  function handleResize() {
+    chartInstance?.resize();
   }
 
   watch(
     () => props.echartData,
-    (newV, oldV) => {
-      if (newV.length) {
-        category.value = newV;
-        echartY.value = newV.map((el: any) => el.value);
+    (newV) => {
+      if (newV && newV.length) {
+        category.value = newV as any[];
+        echartY.value = (newV as any[]).map((el: any) => el.value);
         getOption();
       }
     },
     { immediate: true }
   );
+
+  onMounted(() => {
+    window.addEventListener('resize', handleResize);
+  });
+
+  onUnmounted(() => {
+    window.removeEventListener('resize', handleResize);
+    if (chartInstance) {
+      chartInstance.dispose();
+      chartInstance = null;
+    }
+  });
 </script>
 
 <style lang="less" scoped>
@@ -293,6 +306,7 @@
     position: relative;
     padding-top: 10px;
     height: 100%;
+    box-sizing: border-box;
 
     .chartRef {
       width: 100%;

+ 40 - 16
src/views/vent/home/configurable/green/components/dz-list.vue

@@ -80,39 +80,63 @@ watch(
 }
 .icons-box {
   height: 100%;
-  overflow-y: hidden;
-
-  &:hover {
-    overflow-y: auto;
-    overflow-x: auto;
+  overflow-y: auto;
+  overflow-x: hidden;
+  box-sizing: border-box;
+  // 上下留白,避免左侧图标(top:-18px)被模块边框裁切
+  padding: 18px 0 12px;
+  // 隐藏滚动条,仍可滚动
+  scrollbar-width: none;
+  -ms-overflow-style: none;
+
+  &::-webkit-scrollbar {
+    width: 0;
+    height: 0;
+    display: none;
   }
 
   .icon-item {
     position: relative;
     width: 100%;
     height: 46px;
-    background: var(--image-model_icon-item) no-repeat;
-    background-size: 85% 100%;
+    background: var(--image-model_icon-item) no-repeat left center;
+    // 加长背景条,保证「总数/报警数/断开数」完整落在图内
+    background-size: 100% 100%;
     display: flex;
     align-items: center;
     justify-content: center;
-    margin: 5px auto;
+    margin: 8px auto;
+    box-sizing: border-box;
+    padding-left: 68px;
+    padding-right: 12px;
 
     &:nth-child(even) {
-      padding-right: 0px;
+      padding-right: 12px;
     }
 
     .level-text {
-      width: 76%;
+      width: 100%;
       display: flex;
-      justify-content: space-around;
-      position: absolute;
-      top: 10px;
-      left: 97;
+      justify-content: space-between;
+      align-items: center;
+      position: relative;
+      top: 0;
+      left: 0;
       color: #ffffffe0;
-      font-size: 14px;
+      font-size: 13px;
       text-align: center;
-      letter-spacing: 1px;
+      letter-spacing: 0;
+      box-sizing: border-box;
+      padding: 0 8px 0 4px;
+
+      .all-count,
+      .warn-count,
+      .close-count {
+        flex: 1;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+      }
 
       .num-count,
       .num-count1 {

+ 162 - 155
src/views/vent/home/configurable/green/components/dz-risk.vue

@@ -1,184 +1,191 @@
 <template>
-    <div class="dz-risk">
-        <div class="risk-container">
-            <div class="risk-center"></div>
-            <div ref="riskPie" class="risk-pie"></div>
-        </div>
+  <div class="dz-risk">
+    <div class="risk-container">
+      <div class="risk-center"></div>
+      <div ref="riskPie" class="risk-pie"></div>
     </div>
+  </div>
 </template>
 
 <script setup lang="ts">
-import { ref, nextTick, onMounted, watch } from 'vue'
-import * as echarts from 'echarts';
+  import { ref, nextTick, onMounted, onUnmounted, watch } from 'vue';
+  import * as echarts from 'echarts';
 
-let props = defineProps({
+  const props = defineProps({
     echartData: {
-        type: Array,
-        default: () => {
-            return []
-        }
+      type: Array,
+      default: () => {
+        return [];
+      },
+    },
+  });
+
+  const riskPie = ref<HTMLElement | null>(null);
+  let chartInstance: echarts.ECharts | null = null;
+
+  const colorList = [
+    {
+      type: 'radial',
+      r: 1,
+      colorStops: [
+        { offset: 0, color: 'rgba(182, 242, 255,1)' },
+        { offset: 1, color: 'rgba(95, 228, 255,1)' },
+      ],
+    },
+    {
+      type: 'radial',
+      r: 1,
+      colorStops: [
+        { offset: 0, color: 'rgba(176, 255, 236,1)' },
+        { offset: 1, color: 'rgba(86, 255, 171,1)' },
+      ],
+    },
+    {
+      type: 'radial',
+      r: 1,
+      colorStops: [
+        { offset: 0, color: 'rgba(255, 253, 176,1)' },
+        { offset: 1, color: 'rgba(255, 255, 86,1)' },
+      ],
+    },
+    {
+      type: 'radial',
+      r: 1,
+      colorStops: [
+        { offset: 0, color: 'rgba(215, 179, 255,1)' },
+        { offset: 1, color: 'rgba(134, 88, 255,1)' },
+      ],
+    },
+  ];
+
+  function ensureChart() {
+    if (!riskPie.value) return null;
+    if (!chartInstance) {
+      chartInstance = echarts.init(riskPie.value);
     }
-})
+    return chartInstance;
+  }
 
-let riskPie = ref(null)
-let echartDatas = ref<any[]>([])
-
-function getOption() {
+  function renderChart(data: any[]) {
     nextTick(() => {
-        const colorList = [
-            {
-                type: 'radial',
-                r: 1,
-                colorStops: [{
-                    offset: 0, color: 'rgba(182, 242, 255,1)' // 0% 处的颜色
-                }, {
-                    offset: 1, color: 'rgba(95, 228, 255,1)' // 100% 处的颜色
-                }],
-
-            },
-            {
-                type: 'radial',
-                r: 1,
-                colorStops: [{
-                    offset: 0, color: 'rgba(176, 255, 236,1)' // 0% 处的颜色
-                }, {
-                    offset: 1, color: 'rgba(86, 255, 171,1)' // 100% 处的颜色
-                }],
-
-            },
-            {
-                type: 'radial',
-                r: 1,
-                colorStops: [{
-                    offset: 0, color: 'rgba(255, 253, 176,1)' // 0% 处的颜色
-                }, {
-                    offset: 1, color: 'rgba(255, 255, 86,1)' // 100% 处的颜色
-                }],
-
+      const myChart = ensureChart();
+      if (!myChart) return;
+      const option = {
+        color: colorList,
+        series: [
+          {
+            type: 'pie',
+            center: ['50%', '50%'],
+            radius: ['30%', '50%'],
+            emphasis: {
+              label: {
+                show: true,
+              },
+              opacity: 1,
+              color: function (params) {
+                return colorList[params.dataIndex];
+              },
             },
-            {
-                type: 'radial',
-                r: 1,
-                colorStops: [{
-                    offset: 0, color: 'rgba(215, 179, 255,1)' // 0% 处的颜色
-                }, {
-                    offset: 1, color: 'rgba(134, 88, 255,1)' // 100% 处的颜色
-                }],
-
-            },
-        ]
-        let myChart = echarts.init(riskPie.value);
-        let option = {
-            color: colorList,
-            // tooltip: {
-            //     trigger: 'item'
-            // },
-            series: [{
-                type: 'pie',
-                center: ['50%', '50%'],
-                radius: ['30%', '50%'],
-                emphasis: {
-                    label: {
-                        show: true
-                    },
-                    opacity:1,
-                    color: function (params) {
-                        return colorList[params.dataIndex]
-                    }
-                },
-                itemStyle: {
-                    normal: {
-                        color: function (params) {
-                            return colorList[params.dataIndex]
-                        }
-                    }
+            itemStyle: {
+              normal: {
+                color: function (params) {
+                  return colorList[params.dataIndex];
                 },
-                label: {
-                    show: true,
-                    position: 'outside',
-                    textStyle: {
-                        color: "#fff",
-                        fontSize: 10
-                    },
-                    formatter: '{b}:{c}',
-                    rich: {
-                        // hr: {
-                        //     backgroundColor: 't',
-                        //     borderRadius: 3,
-                        //     width: 3,
-                        //     height: 3,
-                        //     padding: [3, 3, 0, -12]
-                        // },
-                        a: {
-                            padding: [-10, 5, -15, 5]
-                        }
-                    }
+              },
+            },
+            label: {
+              show: true,
+              position: 'outside',
+              textStyle: {
+                color: '#fff',
+                fontSize: 10,
+              },
+              formatter: '{b}:{c}',
+              rich: {
+                a: {
+                  padding: [-10, 5, -15, 5],
                 },
-
-
-                labelLine: {
-                    normal: {
-                        length: 10,
-                        length2: 5,
-                        lineStyle: {
-                            width: 1
-                        }
-                    }
+              },
+            },
+            labelLine: {
+              normal: {
+                length: 10,
+                length2: 5,
+                lineStyle: {
+                  width: 1,
                 },
-                data: echartDatas.value,
-            }]
-        };
-        myChart.setOption(option);
-        window.onresize = function () {
-            myChart.resize();
-        };
+              },
+            },
+            data: data || [],
+          },
+        ],
+      };
+      myChart.setOption(option as any, true);
     });
-}
-
-watch(() => props.echartData, (newV, oldV) => {
-    echartDatas.value = newV
-    getOption()
-}, { immediate: true })
-
+  }
+
+  function handleResize() {
+    chartInstance?.resize();
+  }
+
+  watch(
+    () => props.echartData,
+    (newV) => {
+      renderChart((newV as any[]) || []);
+    },
+    { immediate: true, deep: true }
+  );
+
+  onMounted(() => {
+    window.addEventListener('resize', handleResize);
+  });
+
+  onUnmounted(() => {
+    window.removeEventListener('resize', handleResize);
+    if (chartInstance) {
+      chartInstance.dispose();
+      chartInstance = null;
+    }
+  });
 </script>
 
 <style lang="less" scoped>
-@import '/@/design/theme.less';
+  @import '/@/design/theme.less';
 
-@{theme-deepblue} {
+  @{theme-deepblue} {
     .dz-risk {
-        --image-model_risk-container: url('@/assets/images/themify/deepblue/home-container/configurable/1400.png');
-        --image-model_risk-center: url('@/assets/images/themify/deepblue/home-container/configurable/1500.png');
+      --image-model_risk-container: url('@/assets/images/themify/deepblue/home-container/configurable/1400.png');
+      --image-model_risk-center: url('@/assets/images/themify/deepblue/home-container/configurable/1500.png');
     }
-}
+  }
 
-.dz-risk {
+  .dz-risk {
     --image-model_risk-container: url('@/assets/images/home-green/1400.png');
     --image-model_risk-center: url('@/assets/images/home-green/1500.png');
     height: 100%;
 
     .risk-container {
-        position: relative;
-        height: 100%;
+      position: relative;
+      height: 100%;
+      width: 100%;
+      background: var(--image-model_risk-container) no-repeat center;
+      background-size: 55% 72%;
+
+      .risk-center {
+        position: absolute;
+        left: 50%;
+        top: 50%;
+        transform: translate(-50%, -50%);
         width: 100%;
-        background: var(--image-model_risk-container) no-repeat center;
-        background-size: 55% 72%;
-
-        .risk-center {
-            position: absolute;
-            left: 50%;
-            top: 50%;
-            transform: translate(-50%, -50%);
-            width: 100%;
-            height: 40px;
-            background: var(--image-model_risk-center) no-repeat center;
-            background-size: auto 100%;
-        }
-
-        .risk-pie {
-            width: 100%;
-            height: 100%;
-        }
+        height: 40px;
+        background: var(--image-model_risk-center) no-repeat center;
+        background-size: auto 100%;
+      }
+
+      .risk-pie {
+        width: 100%;
+        height: 100%;
+      }
     }
-}
-</style>
+  }
+</style>

+ 146 - 138
src/views/vent/home/configurable/green/components/green-nav.vue

@@ -4,14 +4,27 @@
     <div class="main-title">{{ mainTitle }}</div>
     <!-- menu区域 -->
     <div class="nav-menu">
-      <div :class="activeIndex == index ? 'nav-menu-active' : 'nav-menu-unactive'" v-for="(item, index) in menuList"
-        :key="index" @click="menuClick(index, item)">
+      <div
+        :class="currentIndex == index ? 'nav-menu-active' : 'nav-menu-unactive'"
+        v-for="(item, index) in menuList"
+        :key="index"
+        @click="menuClick(index, item)"
+      >
         <div>{{ item.name }}</div>
         <!-- menu-item -->
-        <div v-if="activeIndex == index && isShowMenuItem && item.MenuItemList.length != 0" class="nav-menu-item">
+        <div
+          v-if="currentIndex == index && isShowMenuItem && item.MenuItemList && item.MenuItemList.length != 0"
+          class="nav-menu-item"
+        >
           <div class="nav-menu-content">
-            <div :class="menuItemActive == ind ? 'menu-item-active' : 'menu-item-unactive'"
-              v-for="(ite, ind) in item.MenuItemList" :key="ind" @click.stop="menuItemClick(ind)">{{ ite.name }}</div>
+            <div
+              :class="menuItemActive == ind ? 'menu-item-active' : 'menu-item-unactive'"
+              v-for="(ite, ind) in item.MenuItemList"
+              :key="ind"
+              @click.stop="menuItemClick(ind)"
+            >
+              {{ ite.name }}
+            </div>
           </div>
         </div>
       </div>
@@ -19,159 +32,154 @@
   </div>
 </template>
 <script lang="ts" setup>
-import { onMounted, onUnmounted, ref, watch, computed } from 'vue';
-import { menuList } from '../../configurable.data'
-import { useGlobSetting } from '/@/hooks/setting';
-import { useInitPage } from '../../hooks/useInit';
-import { useRouter } from 'vue-router';
-
-let props=defineProps({
-  activeIndex:{
-    type:Number,
-  }
-})
-
-const { title = '智能通风管控系统' } = useGlobSetting();
-const { mainTitle } = useInitPage(title);
-let isShowMenuItem = ref(false)//是否显示menuItem下拉选项菜单
-let menuItemActive = ref(0)//menuItem当前激活选项
-let router = useRouter()
-
-
-
-//menu选项切换
-function menuClick(index, item) {
-  isShowMenuItem.value = !isShowMenuItem.value
-  //menu切换,界面跳转
-  switch (item.name) {
-    case '灾害预警':
-      router.push('/configurable/green/home')
-      break;
-    case '通风预警':
-      router.push('/micro-vent-3dModal/configurable/green/ventS/home')
-      break;
-    case '火灾预警':
-      router.push('/micro-vent-3dModal/configurable/green/fireS/home')
-      break;
-    case '粉尘预警':
-      router.push('/micro-vent-3dModal/configurable/green/dustS/home')
-      break;
+  import { ref, computed } from 'vue';
+  import { menuList } from '../../configurable.data';
+  import { useGlobSetting } from '/@/hooks/setting';
+  import { useInitPage } from '../../hooks/useInit';
+  import { useRouter } from 'vue-router';
+
+  const props = defineProps({
+    activeIndex: {
+      type: Number,
+      default: 0,
+    },
+  });
+
+  const { title = '智能通风管控系统' } = useGlobSetting();
+  const { mainTitle } = useInitPage(title);
+  const isShowMenuItem = ref(false);
+  const menuItemActive = ref(0);
+  const router = useRouter();
+
+  const currentIndex = computed(() => (props.activeIndex !== undefined && props.activeIndex !== null ? props.activeIndex : 0));
+
+  function menuClick(index, item) {
+    const hasMenuItems = item.MenuItemList && item.MenuItemList.length > 0;
+    if (hasMenuItems && currentIndex.value === index) {
+      isShowMenuItem.value = !isShowMenuItem.value;
+    } else {
+      isShowMenuItem.value = hasMenuItems;
+    }
 
+    // 当前高亮菜单再次点击时不整页重跳
+    if (currentIndex.value === index) return;
+
+    switch (item.name) {
+      case '灾害预警':
+        router.push('/configurable/green/home');
+        break;
+      case '通风预警':
+        router.push('/micro-vent-3dModal/configurable/green/ventS/home');
+        break;
+      case '火灾预警':
+        router.push('/micro-vent-3dModal/configurable/green/fireS/home');
+        break;
+      case '粉尘预警':
+        router.push('/micro-vent-3dModal/configurable/green/dustS/home');
+        break;
+    }
   }
-}
 
-//menuItem选项切换
-function menuItemClick(ind) {
-  menuItemActive.value = ind
-  isShowMenuItem.value = false
-}
+  function menuItemClick(ind) {
+    menuItemActive.value = ind;
+    isShowMenuItem.value = false;
+  }
 </script>
 <style lang="less" scoped>
-@import '/@/design/theme.less';
+  @import '/@/design/theme.less';
 
-@{theme-deepblue} {
-  .green-nav {
-    --image-model_green-nav: url('@/assets/images/themify/deepblue/home-container/configurable/green-nav-bg.png');
-    --image-model_nav-menu-active: url('@/assets/images/themify/deepblue/home-container/configurable/green-menu-bg.png');
-    --image-model_nav-menu-item: url('@/assets/images/themify/deepblue/home-container/configurable/green-menu-item.png');
-  }
-}
-
-.green-nav {
-  --image-model_green-nav: url('@/assets/images/home-green/green-nav-bg.png');
-  --image-model_nav-menu-active: url('@/assets/images/home-green/green-menu-bg.png');
-  --image-model_nav-menu-item: url('@/assets/images/home-green/green-menu-item.png');
-
-  position: relative;
-  position: relative;
-  position: relative;
-  width: 100%;
-  height: 100%;
-  background: var(--image-model_green-nav) no-repeat;
-  background-size: 100% 100%;
-
-  .main-title {
-    width: 518px;
-    height: 100%;
-    display: flex;
-    align-items: center;
-    padding-left: 70px;
-    font-family: 'douyuFont';
-    font-size: 20px;
-    letter-spacing: 2px;
-    color: #fff;
+  @{theme-deepblue} {
+    .green-nav {
+      --image-model_green-nav: url('@/assets/images/themify/deepblue/home-container/configurable/green-nav-bg.png');
+      --image-model_nav-menu-active: url('@/assets/images/themify/deepblue/home-container/configurable/green-menu-bg.png');
+      --image-model_nav-menu-item: url('@/assets/images/themify/deepblue/home-container/configurable/green-menu-item.png');
+    }
   }
 
-  .nav-menu {
-    position: absolute;
-    top: 0;
-    left: 675px;
-    width: 880px;
+  .green-nav {
+    --image-model_green-nav: url('@/assets/images/home-green/green-nav-bg.png');
+    --image-model_nav-menu-active: url('@/assets/images/home-green/green-menu-bg.png');
+    --image-model_nav-menu-item: url('@/assets/images/home-green/green-menu-item.png');
+
+    position: relative;
+    width: 100%;
     height: 100%;
-    display: flex;
-    justify-content: flex-start;
-    color: #fff;
-
-    .nav-menu-active {
-      position: relative;
-      width: 120px;
-      height: 60px;
-      line-height: 60px;
-      text-align: center;
-      margin: 0px 20px;
-      font-size: 18px;
-      letter-spacing: 2px;
-      background: var(--image-model_nav-menu-active) no-repeat;
-      background-size: 100% 100%;
-    }
+    background: var(--image-model_green-nav) no-repeat;
+    background-size: 100% 100%;
 
-    .nav-menu-unactive {
-      position: relative;
-      width: 120px;
-      height: 60px;
-      line-height: 60px;
-      text-align: center;
-      margin: 0px 10px;
-      font-size: 16px;
+    .main-title {
+      width: 518px;
+      height: 100%;
+      display: flex;
+      align-items: center;
+      padding-left: 70px;
+      font-family: 'douyuFont';
+      font-size: 20px;
       letter-spacing: 2px;
-      background-size: 100% 100%;
-      cursor: pointer;
+      color: #fff;
     }
 
-    .nav-menu-item {
+    .nav-menu {
       position: absolute;
-      left: -34px;
-      top: 56px;
-      width: 186px;
-      height: 273px;
-      padding: 28px 12px 12px 12px;
-      background: var(--image-model_nav-menu-item) no-repeat;
+      top: 0;
+      left: 675px;
+      width: 880px;
+      height: 100%;
       display: flex;
-      flex-direction: column;
-      align-items: center;
-      box-sizing: border-box;
-
-      .nav-menu-content {
-        width: 100%;
-        height: 100%;
-        overflow-y: auto;
+      justify-content: flex-start;
+      color: #fff;
+
+      .nav-menu-active,
+      .nav-menu-unactive {
+        position: relative;
+        width: 110px;
+        height: 60px;
+        line-height: 60px;
+        text-align: center;
+        margin: 0 8px;
+        font-size: 16px;
+        letter-spacing: 0;
+        background-size: 100% 100%;
+        cursor: pointer;
+      }
 
-        .menu-item-active {
-          width: 100%;
-          height: 36px;
-          line-height: 36px;
-          font-size: 14px;
-          background: linear-gradient(to right, transparent, rgba(47, 132, 111), transparent);
-        }
+      .nav-menu-active {
+        background: var(--image-model_nav-menu-active) no-repeat;
+        background-size: 100% 100%;
+      }
 
-        .menu-item-unactive {
+      .nav-menu-item {
+        position: absolute;
+        left: -34px;
+        top: 56px;
+        width: 186px;
+        height: 273px;
+        padding: 28px 12px 12px 12px;
+        background: var(--image-model_nav-menu-item) no-repeat;
+        display: flex;
+        flex-direction: column;
+        align-items: center;
+        box-sizing: border-box;
+
+        .nav-menu-content {
           width: 100%;
-          height: 40px;
-          line-height: 40px;
-          font-size: 14px;
+          height: 100%;
+          overflow-y: auto;
+
+          .menu-item-active,
+          .menu-item-unactive {
+            width: 100%;
+            height: 36px;
+            line-height: 36px;
+            font-size: 14px;
+            cursor: pointer;
+          }
+
+          .menu-item-active {
+            background: linear-gradient(to right, transparent, rgba(47, 132, 111), transparent);
+          }
         }
       }
     }
   }
-}
 </style>

+ 5 - 3
src/views/vent/home/configurable/green/components/module-green.vue

@@ -74,9 +74,11 @@
   const { selectedDeviceID, selectedDevice, options, init } = useInitModule(props.deviceType, props.moduleData);
 
   const style = computed(() => {
-    const size = props.showStyle.size;
-    const position = props.showStyle.position;
-    return size + position;
+    const size = (props.showStyle.size || '').trim();
+    const position = (props.showStyle.position || '').trim();
+    const sizePart = size && !size.endsWith(';') ? `${size};` : size;
+    const positionPart = position && !position.endsWith(';') ? `${position};` : position;
+    return sizePart + positionPart;
   });
 
   // 根据配置里的定位判断应该使用哪个module组件

+ 6 - 2
src/views/vent/home/configurable/green/components/moduleBottom-green.vue

@@ -2,7 +2,7 @@
   <div v-if="visible" class="module-content">
     <div v-if="title" class="module-content__title__expand">
       <span class="action-btn close-btn" @click="closeModel"></span>
-      <span @click="clickHandler">{{ title }}</span>
+      <span :class="{ 'is-clickable': clickable }" @click="clickHandler">{{ title }}</span>
     </div>
     <div class="module-slot">
       <slot></slot>
@@ -10,7 +10,7 @@
   </div>
 </template>
 <script lang="ts" setup>
-defineProps<{ title: string; visible: boolean }>();
+defineProps<{ title: string; visible: boolean; clickable?: boolean }>();
 const emit = defineEmits(['close', 'click']);
 
 function closeModel() {
@@ -54,6 +54,10 @@ function clickHandler() {
   padding-left: 36px;
   font-weight: bold;
   font-size: 16px;
+
+  .is-clickable {
+    cursor: pointer;
+  }
 }
 
 // .module-content__title {

+ 5 - 3
src/views/vent/home/configurable/green/components/moduleDual-green.vue

@@ -102,9 +102,11 @@
   } = useInitModule(props.deviceTypeB, props.moduleDataB);
 
   const style = computed(() => {
-    const size = props.showStyle.size;
-    const position = props.showStyle.position;
-    return size + position;
+    const size = (props.showStyle.size || '').trim();
+    const position = (props.showStyle.position || '').trim();
+    const sizePart = size && !size.endsWith(';') ? `${size};` : size;
+    const positionPart = position && !position.endsWith(';') ? `${position};` : position;
+    return sizePart + positionPart;
   });
 
   // 根据配置里的定位判断应该使用哪个module组件

+ 6 - 2
src/views/vent/home/configurable/green/components/moduleLeft-green.vue

@@ -2,7 +2,7 @@
   <div v-if="visible" class="module-content">
     <div v-if="title" class="module-content__title__expand">
       <span class="action-btn close-btn" @click="closeModel"></span>
-      <span @click="clickHandler">{{ title }}</span>
+      <span :class="{ 'is-clickable': clickable }" @click="clickHandler">{{ title }}</span>
     </div>
     <div class="module-slot">
       <slot></slot>
@@ -10,7 +10,7 @@
   </div>
 </template>
 <script lang="ts" setup>
-defineProps<{ title: string; visible: boolean }>();
+defineProps<{ title: string; visible: boolean; clickable?: boolean }>();
 const emit = defineEmits(['close', 'click']);
 
 function closeModel() {
@@ -53,6 +53,10 @@ function clickHandler() {
   padding-left: 36px;
   font-weight: bold;
   font-size: 16px;
+
+  .is-clickable {
+    cursor: pointer;
+  }
 }
 
 // .module-content__title {

+ 113 - 133
src/views/vent/home/configurable/green/dust-green.vue

@@ -1,110 +1,142 @@
 <template>
-  <div class="fire-green">
-    <div class="top-bg">
-      <greenNav :activeIndex="activeIndex" />
-    </div>
-    <div class="main-container">
-      <!-- 采用定位方式以避免出现各个模块隐藏时其他模块下移的问题 -->
-      <div class="left-t">
-        <div class="tcontent-area">
-          <div class="tcontent-l">
-            <div>监测</div>
-            <div>区域</div>
-          </div>
-          <div class="tcontent-c">
-            <div style="color: #3afde7; font-size: 24px; font-weight: bolder; letter-spacing: 10px">
-              {{ data.dustAllMineWarn }}
+  <div class="dust-green">
+    <!-- 有 deviceType query 时视为详情页,只展示中心模型 -->
+    <template v-if="!route.query.deviceType">
+      <div class="top-bg">
+        <greenNav :activeIndex="activeIndex" />
+      </div>
+      <div class="main-container">
+        <!-- 采用定位方式以避免出现各个模块隐藏时其他模块下移的问题 -->
+        <div class="left-t">
+          <div class="tcontent-area">
+            <div class="tcontent-l">
+              <div>监测</div>
+              <div>区域</div>
+            </div>
+            <div class="tcontent-c">
+              <div class="risk-value">
+                {{ data.dustAllMineWarn }}
+              </div>
+            </div>
+            <div class="tcontent-r">
+              <div>粉尘</div>
+              <div>风险</div>
             </div>
-          </div>
-          <div class="tcontent-r">
-            <div>粉尘</div>
-            <div>风险</div>
           </div>
         </div>
-      </div>
 
-      <ModuleGreen
-        v-for="cfg in configs"
-        :key="cfg.deviceType"
-        :show-style="cfg.showStyle"
-        :module-data="cfg.moduleData"
-        :module-name="cfg.moduleName"
-        :device-type="cfg.deviceType"
-        :data="data"
-        :visible="true"
-      />
-    </div>
+        <ModuleGreen
+          v-for="(cfg, index) in configs"
+          :key="`${cfg.deviceType}_${cfg.moduleName}_${index}`"
+          :show-style="cfg.showStyle"
+          :module-data="cfg.moduleData"
+          :module-name="cfg.moduleName"
+          :device-type="cfg.deviceType"
+          :data="data"
+          :visible="true"
+        />
+      </div>
+    </template>
 
-    <div style="width: calc(100% - 928px); height: 544px; position: absolute; left: 464px; top: 96px">
+    <div class="center-model">
       <SubApp />
     </div>
   </div>
 </template>
 <script lang="ts" setup>
-  import { ref, computed, onMounted, onUnmounted } from 'vue';
+  import { ref, onMounted, onUnmounted, watch } from 'vue';
+  import { useRoute } from 'vue-router';
   import { useInitConfigs, useInitPage } from '../hooks/useInit';
   import greenNav from './components/green-nav.vue';
   import ModuleGreen from './components/module-green.vue';
   import SubApp from '/@/components/vent/micro/createSubApp.vue';
   import { getDisHome } from '../configurable.api';
-  import { EyeFilled } from '@ant-design/icons-vue';
   import { testConfigBDDust } from '../configurable.data';
+  import { useGlobSetting } from '/@/hooks/setting';
 
-  let activeIndex = ref(3);
+  const { title = '智能通风管控系统' } = useGlobSetting();
+  const route = useRoute();
+  const activeIndex = ref(3);
   const { configs, devicesTypes, fetchConfigs } = useInitConfigs();
-  const { mainTitle, data, updateData } = useInitPage('');
+  const { data, updateData } = useInitPage(title);
   let interval: number | undefined;
+  let unmounted = false;
+
+  function fetchListData() {
+    if (unmounted) return Promise.resolve();
+    const dataList = devicesTypes.value.concat('dustAllMineWarn').join(',');
+    if (!dataList) return Promise.resolve();
+    return getDisHome({ dataList }).then((res) => {
+      if (!unmounted) updateData(res);
+    });
+  }
 
-  onMounted(() => {
-    fetchConfigs('BD_dust').then(() => {
+  function stopInterval() {
+    if (interval !== undefined) {
+      clearInterval(interval);
+      interval = undefined;
+    }
+  }
+
+  function initInterval() {
+    stopInterval();
+    interval = window.setInterval(() => {
+      fetchListData();
+    }, 2000);
+  }
+
+  function refresh() {
+    return fetchConfigs('BD_dust').then(() => {
+      if (unmounted) return;
       configs.value = testConfigBDDust;
+      return fetchListData();
+    });
+  }
 
-      getDisHome({
-        dataList: devicesTypes.value.concat('dustAllMineWarn').join(','),
-      }).then(updateData);
+  function setupPage() {
+    if (route.query.deviceType) {
+      stopInterval();
+      return;
+    }
+    refresh().then(() => {
+      if (!unmounted && !route.query.deviceType) {
+        initInterval();
+      }
     });
-    // [perf-base-v1] 定时器句柄赋值给 interval,修复卸载时清理无效的泄漏
-    interval = setInterval(() => {
-      getDisHome({
-        dataList: devicesTypes.value.concat('dustAllMineWarn').join(','),
-      }).then(updateData);
-    }, 2000);
+  }
+
+  watch(
+    () => route.query,
+    () => {
+      setupPage();
+    }
+  );
+
+  onMounted(() => {
+    setupPage();
   });
 
   onUnmounted(() => {
-    clearInterval(interval);
+    unmounted = true;
+    stopInterval();
   });
-
-  function redirectTo(url) {
-    window.open(url);
-  }
 </script>
 <style lang="less" scoped>
   @import '/@/design/theme.less';
 
   @font-face {
     font-family: 'douyuFont';
-    src: url('../../../../assets/font/douyuFont.otf');
+    src: url('/@/assets/font/douyuFont.otf');
   }
 
   @{theme-deepblue} {
-    .fire-green {
-      --image-bg: url('@/assets/images/themify/deepblue/home-container/configurable/firehome/bg.png');
-      --image-fire-title: url(/@/assets/images/themify/deepblue/home-container/configurable/firehome/fire-title.png);
+    .dust-green {
       --image-qkjaq: url('/@/assets/images/themify/deepblue/home-container/configurable/firehome/1-2.png');
-      --image-common-border2: url('/@/assets/images/themify/deepblue/home-container/configurable/firehome/green-bd-bottom.png');
-      --image-znzjxt: url(/@/assets/images/themify/deepblue/home-container/configurable/firehome/1-7.png);
-      --image-znzdxt: url(/@/assets/images/themify/deepblue/home-container/configurable/firehome/1-8.png);
     }
   }
 
-  .fire-green {
-    --image-bg: url('@/assets/images/home-container/configurable/firehome/bg.png');
-    --image-fire-title: url(/@/assets/images/home-container/configurable/firehome/fire-title.png);
+  .dust-green {
     --image-qkjaq: url('/@/assets/images/home-container/configurable/firehome/1-2.png');
-    --image-common-border2: url('@/assets/images/home-container/configurable/firehome/green-bd-bottom.png');
-    --image-znzjxt: url(/@/assets/images/home-container/configurable/firehome/1-7.png);
-    --image-znzdxt: url(/@/assets/images/home-container/configurable/firehome/1-8.png);
     position: relative;
     width: 100%;
     height: 100%;
@@ -122,42 +154,25 @@
       top: 96px;
       width: calc(100% - 30px);
       height: calc(100% - 96px);
-      margin: 0px 15px;
+      margin: 0 15px;
       box-sizing: border-box;
     }
 
-    .module-dropdown {
-      padding: 10px;
-      background-image: linear-gradient(to bottom, #036886, #072a40);
-      border-bottom: 2px solid #3df6ff;
-      color: @vent-font-color;
-      position: absolute;
-      top: 70px;
-      right: 460px;
-    }
-
-    .module-dropdown-original {
-      padding: 10px;
-      background-image: linear-gradient(to bottom, #036886, #072a40);
-      border-bottom: 2px solid #3df6ff;
-      color: @vent-font-color;
+    .center-model {
+      width: calc(100% - 928px);
+      height: 544px;
       position: absolute;
-      top: 70px;
-      right: 460px;
-    }
-
-    .module-trigger-button {
-      color: @vent-font-color;
-      background-image: linear-gradient(to bottom, #036886, #072a40);
-      border: none;
-      border-bottom: 2px solid #3df6ff;
+      left: 464px;
+      top: 96px;
+      z-index: 0;
+      pointer-events: auto;
     }
   }
 
   .left-t {
     position: absolute;
     height: 90px;
-    top: 0px;
+    top: 0;
     left: 10px;
     width: 420px;
     background-image: var(--image-qkjaq);
@@ -198,6 +213,13 @@
         align-items: center;
         justify-content: center;
         height: 100%;
+
+        .risk-value {
+          color: #3afde7;
+          font-size: 24px;
+          font-weight: bolder;
+          letter-spacing: 4px;
+        }
       }
 
       .tcontent-r {
@@ -215,48 +237,6 @@
     }
   }
 
-  .right-t {
-    position: absolute;
-    height: 90px;
-    right: 10px;
-    top: 0px;
-    width: 420px;
-    background-image: var(--image-common-border2);
-    background-repeat: no-repeat;
-    background-position: center;
-    background-size: 100% 100%;
-    display: flex;
-    align-items: center;
-    justify-content: space-around;
-    color: #fff;
-
-    .tcontent-l {
-      flex: 1;
-      height: 100%;
-      font-size: 14px;
-      font-weight: bold;
-      background-image: var(--image-znzjxt);
-      background-size: 75% 80%;
-      background-repeat: no-repeat;
-      background-position: center;
-      text-align: center;
-      padding-top: 70px;
-    }
-
-    .tcontent-r {
-      flex: 1;
-      height: 100%;
-      font-size: 14px;
-      font-weight: bold;
-      background-image: var(--image-znzdxt);
-      background-size: 75% 80%;
-      background-repeat: no-repeat;
-      background-position: center;
-      text-align: center;
-      padding-top: 70px;
-    }
-  }
-
   :deep(.loading-box) {
     position: unset;
   }

+ 155 - 120
src/views/vent/home/configurable/green/fire-green.vue

@@ -1,67 +1,71 @@
 <template>
   <div class="fire-green">
-    <div class="top-bg">
-      <greenNav :activeIndex="activeIndex" />
-    </div>
-    <div class="main-container">
-      <!-- 采用定位方式以避免出现各个模块隐藏时其他模块下移的问题 -->
-      <div class="left-t">
-        <div class="tcontent-area">
-          <div class="tcontent-l">
-            <div>监测</div>
-            <div>区域</div>
-          </div>
-          <div class="tcontent-c">
-            <div style="margin-bottom: -4px; color: #3afde7; font-size: 24px; font-weight: bolder; letter-spacing: 10px">
-              {{ data.fireAllMineWarn }}
+    <!-- 有 deviceType query 时视为详情页,只展示中心模型 -->
+    <template v-if="!route.query.deviceType">
+      <div class="top-bg">
+        <greenNav :activeIndex="activeIndex" />
+      </div>
+      <div class="main-container">
+        <!-- 采用定位方式以避免出现各个模块隐藏时其他模块下移的问题 -->
+        <div class="left-t">
+          <div class="tcontent-area">
+            <div class="tcontent-l">
+              <div>监测</div>
+              <div>区域</div>
+            </div>
+            <div class="tcontent-c">
+              <div class="risk-value">
+                {{ data.fireAllMineWarn }}
+              </div>
+              <div class="risk-label">自燃倾向性等级 : 自燃</div>
+            </div>
+            <div class="tcontent-r">
+              <div>火灾</div>
+              <div>风险</div>
             </div>
-            <div style="color: #fff; font-size: 12px">自燃倾向性等级 : 自燃</div>
-          </div>
-          <div class="tcontent-r">
-            <div>火灾</div>
-            <div>风险</div>
           </div>
         </div>
-      </div>
-      <div class="right-t">
-        <div class="tcontent-l" @click="redirectTo('/grout-home?mock-login=1')">
-          <div>智能注浆系统</div>
-        </div>
-        <div class="tcontent-r" @click="redirectTo('/nitrogen-home?mock-login=1')">
-          <div>智能注氮系统</div>
+        <div class="right-t">
+          <div class="tcontent-l" @click="redirectTo('/grout-home?mock-login=1')">
+            <div>智能注浆系统</div>
+          </div>
+          <div class="tcontent-r" @click="redirectTo('/nitrogen-home?mock-login=1')">
+            <div>智能注氮系统</div>
+          </div>
         </div>
+        <ModuleGreen
+          v-for="(cfg, index) in cfgs"
+          :key="`${cfg.deviceType}_${cfg.moduleName}_${index}`"
+          :show-style="cfg.showStyle"
+          :module-data="cfg.moduleData"
+          :module-name="cfg.moduleName"
+          :device-type="cfg.deviceType"
+          :data="data"
+          :visible="true"
+        />
+        <ModuleDual
+          v-if="cfgA && cfgB"
+          :show-style="cfgA.showStyle"
+          :module-data-a="cfgA.moduleData"
+          :module-name-a="cfgA.moduleName"
+          :device-type-a="cfgA.deviceType"
+          :module-data-b="cfgB.moduleData"
+          :module-name-b="cfgB.moduleName"
+          :device-type-b="cfgB.deviceType"
+          :data="data"
+          :visible="true"
+        />
       </div>
-      <ModuleGreen
-        v-for="cfg in cfgs"
-        :key="cfg.deviceType"
-        :show-style="cfg.showStyle"
-        :module-data="cfg.moduleData"
-        :module-name="cfg.moduleName"
-        :device-type="cfg.deviceType"
-        :data="data"
-        :visible="true"
-      />
-      <ModuleDual
-        v-if="cfgA && cfgB"
-        :show-style="cfgA.showStyle"
-        :module-data-a="cfgA.moduleData"
-        :module-name-a="cfgA.moduleName"
-        :device-type-a="cfgA.deviceType"
-        :module-data-b="cfgB.moduleData"
-        :module-name-b="cfgB.moduleName"
-        :device-type-b="cfgB.deviceType"
-        :data="data"
-        :visible="true"
-      />
-    </div>
+    </template>
 
-    <div style="width: 998px; height: 550px; position: absolute; left: calc(50% - 500px); top: 60px">
+    <div class="center-model">
       <SubApp />
     </div>
   </div>
 </template>
 <script lang="ts" setup>
-  import { ref, computed, onMounted, onUnmounted } from 'vue';
+  import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
+  import { useRoute } from 'vue-router';
   import { useInitConfigs, useInitPage } from '../hooks/useInit';
   import greenNav from './components/green-nav.vue';
   import ModuleGreen from './components/module-green.vue';
@@ -69,45 +73,82 @@
   import SubApp from '/@/components/vent/micro/createSubApp.vue';
   import { getDisHome } from '../configurable.api';
   import { testConfigBDFire } from '../configurable.data';
-  // import { getToken } from '/@/utils/auth';
-  let activeIndex = ref(2);
+  import { useGlobSetting } from '/@/hooks/setting';
 
-  const cfgs = computed(() =>
-    configs.value.filter((_, index) => {
-      return index !== 4 && index !== 3;
-    })
-  );
-  const cfgA = computed<any>(() =>
-    configs.value.find((_, index) => {
-      return index === 3;
-    })
-  );
-  const cfgB = computed<any>(() =>
-    configs.value.find((_, index) => {
-      return index === 4;
-    })
-  );
+  const DUAL_MODULE_A = '工作面束管监测';
+  const DUAL_MODULE_B = '工作面光纤监测';
+
+  const { title = '智能通风管控系统' } = useGlobSetting();
+  const route = useRoute();
+  const activeIndex = ref(2);
   const { configs, devicesTypes, fetchConfigs } = useInitConfigs();
-  const { mainTitle, data, updateData } = useInitPage('');
+  const { data, updateData } = useInitPage(title);
   let interval: number | undefined;
+  let unmounted = false;
 
-  onMounted(() => {
-    fetchConfigs('BD_fire').then(() => {
-      configs.value = testConfigBDFire;
-      getDisHome({
-        dataList: devicesTypes.value.concat('fireAllMineWarn').join(','),
-      }).then(updateData);
+  const cfgs = computed(() =>
+    configs.value.filter((cfg) => cfg.moduleName !== DUAL_MODULE_A && cfg.moduleName !== DUAL_MODULE_B)
+  );
+  const cfgA = computed<any>(() => configs.value.find((cfg) => cfg.moduleName === DUAL_MODULE_A));
+  const cfgB = computed<any>(() => configs.value.find((cfg) => cfg.moduleName === DUAL_MODULE_B));
+
+  function fetchListData() {
+    if (unmounted) return Promise.resolve();
+    const dataList = devicesTypes.value.concat('fireAllMineWarn').join(',');
+    if (!dataList) return Promise.resolve();
+    return getDisHome({ dataList }).then((res) => {
+      if (!unmounted) updateData(res);
     });
-    // [perf-base-v1] 定时器句柄赋值给 interval,修复卸载时清理无效的泄漏
-    interval = setInterval(() => {
-      getDisHome({
-        dataList: devicesTypes.value.concat('fireAllMineWarn').join(','),
-      }).then(updateData);
+  }
+
+  function stopInterval() {
+    if (interval !== undefined) {
+      clearInterval(interval);
+      interval = undefined;
+    }
+  }
+
+  function initInterval() {
+    stopInterval();
+    interval = window.setInterval(() => {
+      fetchListData();
     }, 2000);
+  }
+
+  function refresh() {
+    return fetchConfigs('BD_fire').then(() => {
+      if (unmounted) return;
+      configs.value = testConfigBDFire;
+      return fetchListData();
+    });
+  }
+
+  function setupPage() {
+    if (route.query.deviceType) {
+      stopInterval();
+      return;
+    }
+    refresh().then(() => {
+      if (!unmounted && !route.query.deviceType) {
+        initInterval();
+      }
+    });
+  }
+
+  watch(
+    () => route.query,
+    () => {
+      setupPage();
+    }
+  );
+
+  onMounted(() => {
+    setupPage();
   });
 
   onUnmounted(() => {
-    clearInterval(interval);
+    unmounted = true;
+    stopInterval();
   });
 
   function redirectTo(url) {
@@ -119,7 +160,7 @@
 
   @font-face {
     font-family: 'douyuFont';
-    src: url('../../../../assets/font/douyuFont.otf');
+    src: url('/@/assets/font/douyuFont.otf');
   }
 
   @{theme-deepblue} {
@@ -151,47 +192,31 @@
       position: absolute;
       z-index: 1;
     }
+
     .main-container {
       position: absolute;
       top: 96px;
       width: calc(100% - 30px);
       height: calc(100% - 96px);
-      margin: 0px 15px;
+      margin: 0 15px;
       box-sizing: border-box;
     }
 
-    .module-dropdown {
-      padding: 10px;
-      background-image: linear-gradient(to bottom, #036886, #072a40);
-      border-bottom: 2px solid #3df6ff;
-      color: @vent-font-color;
+    .center-model {
+      width: 998px;
+      height: 550px;
       position: absolute;
-      top: 70px;
-      right: 460px;
-    }
-
-    .module-dropdown-original {
-      padding: 10px;
-      background-image: linear-gradient(to bottom, #036886, #072a40);
-      border-bottom: 2px solid #3df6ff;
-      color: @vent-font-color;
-      position: absolute;
-      top: 70px;
-      right: 460px;
-    }
-
-    .module-trigger-button {
-      color: @vent-font-color;
-      background-image: linear-gradient(to bottom, #036886, #072a40);
-      border: none;
-      border-bottom: 2px solid #3df6ff;
+      left: calc(50% - 500px);
+      top: 60px;
+      z-index: 0;
+      pointer-events: auto;
     }
   }
 
   .left-t {
     position: absolute;
     height: 90px;
-    top: 0px;
+    top: 0;
     left: 10px;
     width: 420px;
     background-image: var(--image-qkjaq);
@@ -232,6 +257,19 @@
         align-items: center;
         justify-content: center;
         height: 100%;
+
+        .risk-value {
+          margin-bottom: -4px;
+          color: #3afde7;
+          font-size: 24px;
+          font-weight: bolder;
+          letter-spacing: 4px;
+        }
+
+        .risk-label {
+          color: #fff;
+          font-size: 12px;
+        }
       }
 
       .tcontent-r {
@@ -253,7 +291,7 @@
     position: absolute;
     height: 90px;
     right: 10px;
-    top: 0px;
+    top: 0;
     width: 420px;
     background-image: var(--image-common-border2);
     background-repeat: no-repeat;
@@ -263,30 +301,27 @@
     align-items: center;
     justify-content: space-around;
     color: #fff;
-    .tcontent-l {
+
+    .tcontent-l,
+    .tcontent-r {
       flex: 1;
       height: 100%;
       font-size: 14px;
       font-weight: bold;
-      background-image: var(--image-znzjxt);
       background-size: 75% 80%;
       background-repeat: no-repeat;
       background-position: center;
       text-align: center;
       padding-top: 70px;
+      cursor: pointer;
+    }
+
+    .tcontent-l {
+      background-image: var(--image-znzjxt);
     }
 
     .tcontent-r {
-      flex: 1;
-      height: 100%;
-      font-size: 14px;
-      font-weight: bold;
       background-image: var(--image-znzdxt);
-      background-size: 75% 80%;
-      background-repeat: no-repeat;
-      background-position: center;
-      text-align: center;
-      padding-top: 70px;
     }
   }
 

+ 117 - 106
src/views/vent/home/configurable/green/fusion-warn-green.vue

@@ -6,9 +6,16 @@
         <greenNav :activeIndex="activeIndex" />
       </div>
       <div class="main-container">
-        <ModuleOriginal v-for="(cfg, index) in configs" :key="cfg.deviceType + index" :show-style="cfg.showStyle"
-          :module-data="cfg.moduleData" :module-name="cfg.moduleName" :device-type="cfg.deviceType" :data="data"
-          :visible="true" />
+        <ModuleOriginal
+          v-for="(cfg, index) in configs"
+          :key="cfg.deviceType + index"
+          :show-style="cfg.showStyle"
+          :module-data="cfg.moduleData"
+          :module-name="cfg.moduleName"
+          :device-type="cfg.deviceType"
+          :data="data"
+          :visible="true"
+        />
       </div>
     </template>
 
@@ -18,120 +25,124 @@
   </div>
 </template>
 <script lang="ts" setup>
-import { ref, reactive, onMounted, onUnmounted } from 'vue';
-import greenNav from './components/green-nav.vue';
-import { useInitConfigs, useInitPage } from '../hooks/useInit';
-import ModuleOriginal from './components/ModuleOriginal-green.vue';
-import CenterAreaGreen from './components/center-area-green.vue';
-import { useRoute } from 'vue-router';
-import { useGlobSetting } from '/@/hooks/setting';
-import { testConfigFusionGreen } from '../configurable.data';
-import { getTotal, getDisasterProportion } from '../configurable.api';
-const { title = '智能通风管控系统' } = useGlobSetting();
-const { configs, fetchConfigs } = useInitConfigs();
-const { data, updateData } = useInitPage(title);
-const route = useRoute();
-const paramData = reactive<any>({})
-let activeIndex = ref(0)
-// https获取监测数据
-let timer: null | NodeJS.Timeout = null;
-function getMonitor(flag = false) {
-  timer = setTimeout(
-    async () => {
-      getTotal({}).then(updateData);
-      getCenterList()
-      if (timer) {
-        timer = null;
+  import { ref, reactive, onMounted, onUnmounted } from 'vue';
+  import greenNav from './components/green-nav.vue';
+  import { useInitConfigs, useInitPage } from '../hooks/useInit';
+  import ModuleOriginal from './components/ModuleOriginal-green.vue';
+  import CenterAreaGreen from './components/center-area-green.vue';
+  import { useRoute } from 'vue-router';
+  import { useGlobSetting } from '/@/hooks/setting';
+  import { testConfigFusionGreen } from '../configurable.data';
+  import { getTotal, getDisasterProportion } from '../configurable.api';
+  const { title = '智能通风管控系统' } = useGlobSetting();
+  const { configs, fetchConfigs } = useInitConfigs();
+  const { data, updateData } = useInitPage(title);
+  const route = useRoute();
+  const paramData = reactive<any>({});
+  let activeIndex = ref(0);
+  let timer: null | NodeJS.Timeout = null;
+  let unmounted = false;
+
+  function getMonitor(flag = false) {
+    if (unmounted) return;
+    if (timer) {
+      clearTimeout(timer);
+      timer = null;
+    }
+    timer = setTimeout(async () => {
+      if (unmounted) return;
+      try {
+        await getCenterList();
+      } finally {
+        if (!unmounted) {
+          getMonitor(false);
+        }
       }
-      getMonitor();
-    },
-    flag ? 0 : 20000
-  );
-}
-async function getCenterList() {
-  let res = await getTotal({})
-  paramData.fire = res.info.sysInfo.fireS.maxLevel;
-  paramData.tf = res.info.sysInfo.ventS.maxLevel;
-  paramData.ws = res.info.sysInfo.gasS.maxLevel;
-  paramData.sb = res.info.deviceWarnInfo.maxLevel;
-  paramData.fc = res.info.sysInfo.dustS.maxLevel;
-  paramData.riskLevel = res.info.riskLevel;
-  let Levels = Object.assign({}, await getDisasterProportion({}));
-  paramData.levels =
-    Levels.level > 0 && Levels.level <= 1
-      ? 101
-      : Levels.level > 1 && Levels.level <= 2
-        ? 102
-        : Levels.level > 2 && Levels.level <= 3
-          ? 103
-          : Levels.level > 3 && Levels.level <= 4
-            ? 104
-            : 0;
-  paramData.riskData = [
-    { name: '通风', value: Levels.vent },
-    { name: '火灾', value: Levels.fire },
-    { name: '瓦斯', value: Levels.gas },
-    { name: '粉尘', value: Levels.dust },
-  ]
-}
-onMounted(() => {
-  fetchConfigs('fusion-warn-green').then(() => {
-    configs.value = testConfigFusionGreen;
-    // updateEnhancedConfigs(configs.value);
-    getMonitor(true);
+    }, flag ? 0 : 20000);
+  }
+
+  async function getCenterList() {
+    const res = await getTotal({});
+    updateData(res);
+    const info = res?.info || {};
+    const sysInfo = info.sysInfo || {};
+    paramData.fire = sysInfo.fireS?.maxLevel;
+    paramData.tf = sysInfo.ventS?.maxLevel;
+    paramData.ws = sysInfo.gasS?.maxLevel;
+    paramData.sb = info.deviceWarnInfo?.maxLevel;
+    paramData.fc = sysInfo.dustS?.maxLevel;
+    paramData.riskLevel = info.riskLevel;
+    const Levels = Object.assign({}, await getDisasterProportion({}));
+    const level = Number(Levels.level);
+    paramData.levels =
+      level > 0 && level <= 1 ? 101 : level > 1 && level <= 2 ? 102 : level > 2 && level <= 3 ? 103 : level > 3 && level <= 4 ? 104 : 0;
+    paramData.riskData = [
+      { name: '通风', value: Levels.vent },
+      { name: '火灾', value: Levels.fire },
+      { name: '瓦斯', value: Levels.gas },
+      { name: '粉尘', value: Levels.dust },
+    ];
+  }
+
+  onMounted(() => {
+    fetchConfigs('fusion-warn-green').then(() => {
+      configs.value = testConfigFusionGreen;
+      getMonitor(true);
+    });
   });
-});
 
-onUnmounted(() => {
-  clearTimeout(timer);
-  timer = undefined;
-});
+  onUnmounted(() => {
+    unmounted = true;
+    if (timer) {
+      clearTimeout(timer);
+      timer = null;
+    }
+  });
 </script>
 <style lang="less" scoped>
-@import '/@/design/theme.less';
+  @import '/@/design/theme.less';
 
-@{theme-deepblue} {
-  .fusion-home {
-    --image-model_left-area: url('@/assets/images/themify/deepblue/home-container/configurable/green-bd-left.png');
-    --image-model_bottom-area: url('@/assets/images/themify/deepblue/home-container/configurable/green-bd-bottom.png');
+  @{theme-deepblue} {
+    .fusion-home {
+      --image-model_left-area: url('@/assets/images/themify/deepblue/home-container/configurable/green-bd-left.png');
+      --image-model_bottom-area: url('@/assets/images/themify/deepblue/home-container/configurable/green-bd-bottom.png');
+    }
   }
-}
 
-.fusion-home {
-  --image-model_left-area: url('@/assets/images/home-green/green-bd-left.png');
-  --image-model_bottom-area: url('@/assets/images/home-green/green-bd-bottom.png');
-  width: 100%;
-  height: 100%;
-  position: relative;
-  background-color: #181b24;
-
-  .top-bg {
+  .fusion-home {
+    --image-model_left-area: url('@/assets/images/home-green/green-bd-left.png');
+    --image-model_bottom-area: url('@/assets/images/home-green/green-bd-bottom.png');
     width: 100%;
-    height: 96px;
-    // background: var(--image-modal-top) no-repeat center;
-    position: absolute;
-    z-index: 1;
-  }
+    height: 100%;
+    position: relative;
+    background-color: #181b24;
 
-  .main-container {
-    position: absolute;
-    top: 96px;
-    width: calc(100% - 30px);
-    height: calc(100% - 96px);
-    margin: 0px 15px;
-    box-sizing: border-box;
-  }
+    .top-bg {
+      width: 100%;
+      height: 96px;
+      position: absolute;
+      z-index: 1;
+    }
+
+    .main-container {
+      position: absolute;
+      top: 96px;
+      width: calc(100% - 30px);
+      height: calc(100% - 96px);
+      margin: 0px 15px;
+      box-sizing: border-box;
+    }
 
-  .center-area {
-    position: absolute;
-    left: 450px;
-    top: 96px;
-    width: calc(100% - 900px);
-    height: calc(100% - 200px);
+    .center-area {
+      position: absolute;
+      left: 450px;
+      top: 96px;
+      width: calc(100% - 900px);
+      height: calc(100% - 200px);
+    }
   }
-}
 
-:deep(.loading-box) {
-  position: unset;
-}
+  :deep(.loading-box) {
+    position: unset;
+  }
 </style>

+ 81 - 39
src/views/vent/home/configurable/green/vent-green.vue

@@ -23,78 +23,95 @@
       </div>
     </template>
 
-    <div style="width: calc(100% - 468px); height: 530px; position: absolute; left: 436px; top: 110px">
+    <div class="center-model">
       <SubApp />
     </div>
   </div>
 </template>
 <script lang="ts" setup>
-  import { onMounted, onUnmounted, ref, watch, computed } from 'vue';
+  import { onMounted, onUnmounted, ref, watch } from 'vue';
   import { useInitConfigs, useInitPage } from '../hooks/useInit';
   import greenNav from './components/green-nav.vue';
   import ModuleOriginal from './components/ModuleOriginal-green.vue';
-  // import greenRightTag from './components/green-right-tag.vue';
+  import greenRightTag from '../components/green-right-tag.vue';
   import { list } from '../configurable.api';
-  import { useRoute, useRouter } from 'vue-router';
+  import { useRoute } from 'vue-router';
   import { useGlobSetting } from '/@/hooks/setting';
   import { testConfigVent } from '../configurable.data';
   import SubApp from '/@/components/vent/micro/createSubApp.vue';
 
   const { title = '智能通风管控系统' } = useGlobSetting();
-  //   const { configs, isOriginal, isCommon, fetchConfigs } = useInitConfigs();
-  const { isOriginal, fetchConfigs } = useInitConfigs();
-  const { mainTitle, data, updateData, updateEnhancedConfigs } = useInitPage(title);
+  const { fetchConfigs } = useInitConfigs();
+  const { data, updateData, updateEnhancedConfigs } = useInitPage(title);
   const route = useRoute();
-  let activeIndex = ref(1);
+  const activeIndex = ref(1);
   let interval: number | undefined;
-  let configs = ref<any[]>([]);
+  const configs = ref<any[]>([]);
+  let unmounted = false;
+
+  function getTypes() {
+    return configs.value
+      .filter((e) => e.deviceType)
+      .map((e) => e.deviceType)
+      .join(',');
+  }
+
+  function fetchListData() {
+    const types = getTypes();
+    if (!types || unmounted) return Promise.resolve();
+    return list({ types }).then(updateData);
+  }
+
+  function stopInterval() {
+    if (interval !== undefined) {
+      clearInterval(interval);
+      interval = undefined;
+    }
+  }
+
+  function initInterval() {
+    stopInterval();
+    interval = window.setInterval(() => {
+      fetchListData();
+    }, 60000);
+  }
+
   function refresh() {
-    fetchConfigs('vent').then(() => {
+    return fetchConfigs('vent').then(() => {
+      if (unmounted) return;
       configs.value = testConfigVent;
       updateEnhancedConfigs(configs.value);
-      list({
-        types: configs.value
-          .filter((e) => e.deviceType)
-          .map((e) => e.deviceType)
-          .join(','),
-      }).then(updateData);
+      return fetchListData();
     });
   }
 
-  function initInterval() {
-    // [perf-base-v1] 重复初始化前先清理旧句柄,并将新句柄赋值给 interval,修复定时器泄漏
-    clearInterval(interval);
-    interval = setInterval(() => {
-      list({
-        types: configs.value
-          .filter((e) => e.deviceType)
-          .map((e) => e.deviceType)
-          .join(','),
-      }).then(updateData);
-    }, 60000);
+  function setupPage() {
+    if (route.query.deviceType) {
+      // 仅展示子应用
+      stopInterval();
+      return;
+    }
+    refresh().then(() => {
+      if (!unmounted && !route.query.deviceType) {
+        initInterval();
+      }
+    });
   }
 
   watch(
     () => route.query,
     () => {
-      if (route.query.deviceType) {
-        // 仅需要展示子应用,模拟 unmounted
-        clearInterval(interval);
-      } else {
-        // 模拟 mounted
-        refresh();
-        initInterval();
-      }
+      setupPage();
     }
   );
 
   onMounted(() => {
-    refresh();
-    initInterval();
+    setupPage();
   });
 
   onUnmounted(() => {
-    clearInterval(interval);
+    unmounted = true;
+    stopInterval();
   });
 </script>
 <style lang="less" scoped>
@@ -130,9 +147,34 @@
       top: 96px;
       width: calc(100% - 30px);
       height: calc(100% - 96px);
-      margin: 0px 15px;
+      margin: 0 15px;
       box-sizing: border-box;
     }
+
+    .right-area {
+      position: absolute;
+      // 靠右上留白,避开底部模块与中心模型
+      right: 6px;
+      top: 8px;
+      width: 96px;
+      height: calc(100% - 300px);
+      z-index: 2;
+      box-sizing: border-box;
+      padding: 2px 0;
+      overflow: hidden;
+      pointer-events: none;
+    }
+
+    .center-model {
+      // 右侧为风量标签预留空间,避免遮挡
+      width: calc(100% - 560px);
+      height: 530px;
+      position: absolute;
+      left: 436px;
+      top: 110px;
+      z-index: 0;
+      pointer-events: auto;
+    }
   }
 
   :deep(.loading-box) {