ソースを参照

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

lxh 2 日 前
親
コミット
a68f9ced6e
34 ファイル変更、1847 行追加、736 行削除
  1. BIN
      src/assets/images/vent/home/selfIgnition/1-7.png
  2. BIN
      src/assets/images/vent/home/selfIgnition/2-1.png
  3. BIN
      src/assets/images/vent/home/selfIgnition/2-2.png
  4. BIN
      src/assets/images/vent/home/selfIgnition/2-3.png
  5. BIN
      src/assets/images/vent/home/selfIgnition/2-4.png
  6. BIN
      src/assets/images/vent/home/selfIgnition/2-5.png
  7. BIN
      src/assets/images/vent/home/selfIgnition/2-6.png
  8. BIN
      src/assets/images/vent/home/selfIgnition/2-7.png
  9. BIN
      src/assets/images/vent/home/selfIgnition/3-1.png
  10. BIN
      src/assets/images/vent/home/selfIgnition/module-border-bottom-wide.png
  11. BIN
      src/assets/images/vent/home/selfIgnition/module-border-bottom.png
  12. BIN
      src/assets/images/vent/home/selfIgnition/module-border-middle-wide.png
  13. BIN
      src/assets/images/vent/home/selfIgnition/module-border-middle.png
  14. BIN
      src/assets/images/vent/home/selfIgnition/module-border-top-wide.png
  15. BIN
      src/assets/images/vent/home/selfIgnition/module-border-top.png
  16. BIN
      src/assets/images/vent/home/selfIgnition/module-title-bg.png
  17. BIN
      src/assets/images/vent/home/selfIgnition/tab-active-bg.png
  18. BIN
      src/assets/images/vent/home/selfIgnition/tab-item-bg.png
  19. BIN
      src/assets/images/vent/home/selfIgnition/tab-wing-left.png
  20. BIN
      src/assets/images/vent/home/selfIgnition/tab-wing-right.png
  21. BIN
      src/assets/images/vent/home/selfIgnition/top-bg.png
  22. 61 72
      src/views/vent/home/selfIgnition/components/FiberTemperatureChart.vue
  23. 145 187
      src/views/vent/home/selfIgnition/components/ManualView.vue
  24. 134 0
      src/views/vent/home/selfIgnition/components/Module.vue
  25. 834 231
      src/views/vent/home/selfIgnition/components/MonitorView.vue
  26. 37 157
      src/views/vent/home/selfIgnition/components/OverviewView.vue
  27. 132 22
      src/views/vent/home/selfIgnition/components/PanelSelect.vue
  28. 2 3
      src/views/vent/home/selfIgnition/data.ts
  29. 155 0
      src/views/vent/home/selfIgnition/goafOverview.adapter.ts
  30. 104 0
      src/views/vent/home/selfIgnition/goafOverview.api.ts
  31. 134 58
      src/views/vent/home/selfIgnition/index.vue
  32. 43 0
      src/views/vent/home/selfIgnition/monitorDevice.api.ts
  33. 43 6
      src/views/vent/home/selfIgnition/safetyEvaluation.api.ts
  34. 23 0
      src/views/vent/home/selfIgnition/scrollbar.less

BIN
src/assets/images/vent/home/selfIgnition/1-7.png


BIN
src/assets/images/vent/home/selfIgnition/2-1.png


BIN
src/assets/images/vent/home/selfIgnition/2-2.png


BIN
src/assets/images/vent/home/selfIgnition/2-3.png


BIN
src/assets/images/vent/home/selfIgnition/2-4.png


BIN
src/assets/images/vent/home/selfIgnition/2-5.png


BIN
src/assets/images/vent/home/selfIgnition/2-6.png


BIN
src/assets/images/vent/home/selfIgnition/2-7.png


BIN
src/assets/images/vent/home/selfIgnition/3-1.png


BIN
src/assets/images/vent/home/selfIgnition/module-border-bottom-wide.png


BIN
src/assets/images/vent/home/selfIgnition/module-border-bottom.png


BIN
src/assets/images/vent/home/selfIgnition/module-border-middle-wide.png


BIN
src/assets/images/vent/home/selfIgnition/module-border-middle.png


BIN
src/assets/images/vent/home/selfIgnition/module-border-top-wide.png


BIN
src/assets/images/vent/home/selfIgnition/module-border-top.png


BIN
src/assets/images/vent/home/selfIgnition/module-title-bg.png


BIN
src/assets/images/vent/home/selfIgnition/tab-active-bg.png


BIN
src/assets/images/vent/home/selfIgnition/tab-item-bg.png


BIN
src/assets/images/vent/home/selfIgnition/tab-wing-left.png


BIN
src/assets/images/vent/home/selfIgnition/tab-wing-right.png


BIN
src/assets/images/vent/home/selfIgnition/top-bg.png


+ 61 - 72
src/views/vent/home/selfIgnition/components/FiberTemperatureChart.vue

@@ -17,9 +17,14 @@
   let resizeObserver: ResizeObserver | undefined;
 
   function buildOption(data: FiberChartData) {
-    const peakIndex = data.peakIndex;
-    const peakPercent = ((peakIndex + 0.5) / data.categories.length) * 100;
-    const labelIndexes = [0, 3, 6, 8, 11];
+    const labelStep = Math.max(1, Math.ceil(data.categories.length / 6));
+    const dense = data.solid.length > 100;
+    const values = [...data.solid, ...data.dashed].filter((value) => Number.isFinite(Number(value))).map(Number);
+    const minValue = values.length ? Math.min(...values) : 0;
+    const maxValue = values.length ? Math.max(...values) : 1000;
+    const padding = Math.max(1, (maxValue - minValue) * 0.12);
+    const chartMin = Math.max(0, Math.floor(minValue - padding));
+    const chartMax = Math.ceil(maxValue + padding);
 
     return {
       animationDuration: 800,
@@ -41,8 +46,36 @@
         left: 40,
         right: 8,
         top: 22,
-        bottom: 22,
+        bottom: 38,
       },
+      dataZoom: [
+        {
+          type: 'inside',
+          xAxisIndex: 0,
+          filterMode: 'none',
+        },
+        {
+          type: 'slider',
+          xAxisIndex: 0,
+          filterMode: 'none',
+          height: 12,
+          bottom: 2,
+          borderColor: 'transparent',
+          backgroundColor: 'rgba(9, 48, 80, 0.55)',
+          fillerColor: 'rgba(61, 246, 255, 0.16)',
+          handleStyle: { color: '#3df6ff', borderColor: '#3df6ff' },
+          moveHandleStyle: { color: '#3df6ff' },
+          textStyle: { color: '#8fc7e8', fontSize: 9 },
+          dataBackground: {
+            lineStyle: { color: 'rgba(61, 246, 255, 0.35)' },
+            areaStyle: { color: 'rgba(61, 246, 255, 0.08)' },
+          },
+          selectedDataBackground: {
+            lineStyle: { color: 'rgba(255, 179, 71, 0.65)' },
+            areaStyle: { color: 'rgba(255, 122, 26, 0.12)' },
+          },
+        },
+      ],
       xAxis: {
         type: 'category',
         boundaryGap: false,
@@ -56,14 +89,14 @@
           fontSize: 9,
           margin: 8,
           interval: 0,
-          formatter: (value: string, index: number) => (labelIndexes.includes(index) ? value : ''),
+          formatter: (value: string, index: number) => (index === 0 || index === data.categories.length - 1 || index % labelStep === 0 ? value : ''),
         },
       },
       yAxis: {
         type: 'value',
-        min: 0,
-        max: 1000,
-        interval: 200,
+        min: chartMin,
+        max: chartMax,
+        splitNumber: 5,
         axisLine: { show: false },
         axisTick: { show: false },
         axisLabel: {
@@ -92,9 +125,9 @@
           name: '光纤温度',
           type: 'line',
           data: data.solid,
-          smooth: 0.35,
-          symbol: 'circle',
-          symbolSize: 4,
+          smooth: dense ? 0 : 0.35,
+          symbol: 'none',
+          symbolSize: 0,
           itemStyle: {
             color: '#ffb347',
             borderColor: '#fff',
@@ -102,68 +135,16 @@
           },
           lineStyle: {
             color: '#ff7a1a',
-            width: 2,
-          },
-          areaStyle: {
-            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
-              { offset: 0, color: 'rgba(255, 122, 26, 0.32)' },
-              { offset: 1, color: 'rgba(255, 122, 26, 0)' },
-            ]),
-          },
-          markLine: {
-            silent: true,
-            symbol: 'none',
-            label: { show: false },
-            lineStyle: {
-              color: 'rgba(255, 77, 79, 0.85)',
-              type: 'dashed',
-              width: 1,
-            },
-            data: [{ xAxis: data.categories[peakIndex] }],
-          },
-          markPoint: {
-            symbol: 'circle',
-            symbolSize: 8,
-            itemStyle: {
-              color: '#ffb347',
-              borderColor: '#fff',
-              borderWidth: 2,
-              shadowBlur: 12,
-              shadowColor: 'rgba(255, 122, 26, 0.9)',
-            },
-            label: { show: false },
-            data: [{ coord: [data.categories[peakIndex], data.solid[peakIndex]] }],
-          },
-        },
-      ],
-      graphic: [
-        {
-          type: 'text',
-          left: `${peakPercent - 8}%`,
-          top: '26%',
-          style: {
-            text: '预警确认',
-            fill: '#ffd15c',
-            fontSize: 10,
-            backgroundColor: 'rgba(255, 157, 28, 0.24)',
-            borderColor: '#ff9d1c',
-            borderWidth: 1,
-            padding: [2, 4],
-          },
-        },
-        {
-          type: 'text',
-          left: `${peakPercent - 7}%`,
-          top: '38%',
-          style: {
-            text: '立即报警',
-            fill: '#fff',
-            fontSize: 10,
-            backgroundColor: 'rgba(255, 77, 79, 0.5)',
-            borderColor: '#ff4d4f',
-            borderWidth: 1,
-            padding: [2, 4],
+            width: dense ? 1 : 2,
           },
+          areaStyle: dense
+            ? undefined
+            : {
+                color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
+                  { offset: 0, color: 'rgba(255, 122, 26, 0.32)' },
+                  { offset: 1, color: 'rgba(255, 122, 26, 0)' },
+                ]),
+              },
         },
       ],
     };
@@ -182,6 +163,14 @@
     chart.resize();
   }
 
+  watch(
+    () => props.data,
+    (data) => {
+      chart?.setOption(buildOption(data), true);
+    },
+    { deep: true }
+  );
+
   watch(
     () => props.active,
     () => {

+ 145 - 187
src/views/vent/home/selfIgnition/components/ManualView.vue

@@ -1,14 +1,7 @@
 <template>
   <div class="ignition-view manual-view">
     <!-- 左:人工录入 -->
-    <ModuleCommon
-      :show-style="panelShowStyle(20, 132, 353, 907)"
-      :module-data="EMPTY_MODULE_DATA"
-      module-name="人工录入"
-      device-type="ignition_manual_input"
-      :data="{}"
-      :visible="true"
-    >
+    <Module :x="20" :y="132" :width="393" :height="907" title="人工录入">
       <div class="manual-form">
         <div class="form-toolbar">
           <div class="eval-type-tabs">
@@ -21,29 +14,23 @@
           <div v-if="loadingOptions" class="form-loading">评价指标加载中...</div>
           <template v-else>
             <div v-for="group in fieldGroups" :key="group.key" class="form-group">
-              <div class="group-collapse" @click="toggleGroup(group.key)">
-                <img class="group-icon" src="@/assets/images/vent/home/selfIgnition/form-icon.svg" alt="" />
-                <span>{{ group.title }}</span>
-                <i class="collapse-arrow" :class="{ collapsed: collapsed[group.key] }"></i>
+              <div class="group-title">
+                <span class="title-bars">
+                  <i v-for="n in 4" :key="n"></i>
+                </span>
+                <span>{{ group.title }}指标</span>
+                <span class="title-dots">
+                  <i></i>
+                  <b></b>
+                  <i></i>
+                </span>
               </div>
-              <template v-if="!collapsed[group.key]">
-                <div class="group-title">
-                  <span class="title-bars">
-                    <i v-for="n in 4" :key="n"></i>
-                  </span>
-                  <span>{{ group.title }}指标</span>
-                  <span class="title-dots">
-                    <i v-for="n in 3" :key="n"></i>
-                    <b></b>
-                  </span>
+              <div class="field-list">
+                <div v-for="item in group.fields" :key="item.code" class="field-row">
+                  <span class="field-label" :title="item.name">{{ item.name }}:</span>
+                  <PanelSelect v-model="formValues[FIELD_BY_CODE[item.code]]" :options="selectOptions(item)" />
                 </div>
-                <div class="field-list">
-                  <div v-for="item in group.fields" :key="item.code" class="field-row">
-                    <span class="field-label" :title="item.name">{{ item.name }}:</span>
-                    <PanelSelect v-model="formValues[FIELD_BY_CODE[item.code]]" :options="selectOptions(item)" />
-                  </div>
-                </div>
-              </template>
+              </div>
             </div>
             <div v-if="!fieldGroups.length" class="form-loading">暂无评价指标</div>
           </template>
@@ -58,28 +45,19 @@
           </button>
         </div>
       </div>
-    </ModuleCommon>
+    </Module>
 
     <!-- 右:评价结果 -->
-    <ModuleCommon
-      :show-style="panelShowStyle(1545, 132, 353, 907)"
-      :module-data="EMPTY_MODULE_DATA"
-      module-name="评价结果"
-      device-type="ignition_evaluation_result"
-      :data="{}"
-      :visible="true"
-    >
+    <Module :x="1505" :y="132" :width="393" :height="907" title="评价结果">
       <div class="result-panel">
         <div class="result-top">
           <div class="score-line">
-            <span class="score-label">安全评价得分:</span>
-            <img class="score-info" src="@/assets/images/vent/home/selfIgnition/info-icon.svg" alt="由后端算法计算" />
+            <span class="score-label">风险分值:</span>
             <span class="score-value">{{ resultScore }}</span>
           </div>
           <div class="gauge-line">
             <div class="gauge">
-              <img class="gauge-segment gauge-segment-1" src="@/assets/images/vent/home/selfIgnition/risk-gauge.png" alt="" />
-              <img class="gauge-segment gauge-segment-2" src="@/assets/images/vent/home/selfIgnition/risk-gauge.png" alt="" />
+              <img class="gauge-segment" src="@/assets/images/vent/home/selfIgnition/risk-gauge.png" alt="" />
             </div>
             <span class="risk-tag" :style="{ '--risk-color': resultRiskColor }">{{ resultTagText }}</span>
           </div>
@@ -96,9 +74,9 @@
         </div>
 
         <div class="level-table">
-          <div class="level-head"><span>评价等级</span><span>评分范围</span><span>对应颜色</span></div>
+          <div class="level-head"><span>评价等级</span><span>风险分值范围</span><span>对应颜色</span></div>
           <div
-            v-for="item in levels"
+            v-for="(item, index) in levels"
             :key="item.level"
             class="level-row"
             :class="{ highlight: item.level === resultLevel }"
@@ -106,6 +84,7 @@
           >
             <span class="level-name">
               <i :style="{ background: item.color, boxShadow: `0 0 8px ${item.color}` }"></i>
+              <b>{{ levelRoman(index) }}</b>
               {{ item.level }}
             </span>
             <span>{{ item.range }}</span>
@@ -113,15 +92,14 @@
           </div>
         </div>
       </div>
-    </ModuleCommon>
+    </Module>
   </div>
 </template>
 
 <script lang="ts" setup>
   import { computed, onMounted, reactive, ref, watch } from 'vue';
-  import ModuleCommon from '../../configurable/components/ModuleCommon.vue';
+  import Module from './Module.vue';
   import PanelSelect from './PanelSelect.vue';
-  import { EMPTY_MODULE_DATA, panelShowStyle } from '../utils';
   import { useMessage } from '/@/hooks/web/useMessage';
   import { calculateSafetyEvaluation, getSafetyEvaluationByScene, getSafetyItemOptions, saveSafetyEvaluation } from '../safetyEvaluation.api';
   import { EVAL_RISK_LEVELS, EVAL_RISK_MAP, FIELD_BY_CODE, FALLBACK_SAFETY_ITEMS } from '../evaluation.config';
@@ -137,7 +115,6 @@
   const items = ref<SafetyItem[]>([]);
   const evalType = ref(1);
   const formValues = reactive<Record<string, string>>({});
-  const collapsed = reactive<Record<string, boolean>>({});
   const evaluation = ref<SafetyEvaluationEntity | null>(null);
   const loadingOptions = ref(false);
   const previewing = ref(false);
@@ -172,7 +149,20 @@
   const resultLevel = computed(() => evaluation.value?.riskLevel || '');
   const resultLevelMeta = computed(() => EVAL_RISK_MAP[resultLevel.value] || null);
   const resultRiskColor = computed(() => resultLevelMeta.value?.color || '#3df6ff');
-  const resultTagText = computed(() => resultLevel.value || '未评价');
+
+  const ROMAN_NUMERALS = ['I', 'II', 'III', 'IV', 'V', 'VI'];
+
+  function levelRoman(index: number) {
+    return ROMAN_NUMERALS[index] || '';
+  }
+
+  const resultTagText = computed(() => {
+    const level = resultLevel.value;
+    if (!level) return '未评价';
+    const index = levels.findIndex((item) => item.level === level);
+    return index >= 0 ? `${levelRoman(index)} ${level}` : level;
+  });
+
   const resultTarget = computed(() => evaluation.value?.workfaceName || targetName.value);
   const resultTime = computed(() => evaluation.value?.createTime || '--');
 
@@ -180,7 +170,8 @@
     const value = evaluation.value?.totalScore;
     if (value === null || value === undefined || value === '') return '--';
     const score = Number(value);
-    return Number.isFinite(score) ? score.toFixed(2) : String(value);
+    // 与设计稿一致:最多保留两位小数并去掉末尾的 0(58.60 → 58.6)
+    return Number.isFinite(score) ? `${Number(score.toFixed(2))}` : String(value);
   });
 
   function selectOptions(item: SafetyItem) {
@@ -191,10 +182,6 @@
     }));
   }
 
-  function toggleGroup(key: string) {
-    collapsed[key] = !collapsed[key];
-  }
-
   function formatDateTime(date: Date) {
     const pad = (value: number) => `${value}`.padStart(2, '0');
     return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
@@ -215,12 +202,9 @@
       .filter((item) => item.code && item.options.length);
   }
 
-  function initDefaults() {
-    items.value.forEach((item) => {
-      const field = FIELD_BY_CODE[item.code];
-      if (field && !formValues[field]) {
-        formValues[field] = item.options[0]?.name || '';
-      }
+  function clearFormValues() {
+    Object.keys(formValues).forEach((key) => {
+      formValues[key] = '';
     });
   }
 
@@ -238,7 +222,6 @@
       createMessage.warning('评价指标接口异常,已使用内置指标字典');
     } finally {
       loadingOptions.value = false;
-      initDefaults();
     }
   }
 
@@ -248,8 +231,7 @@
       ...entity,
       createTime: entity.createTime || formatDateTime(new Date()),
     };
-    items.value.forEach((item) => {
-      const field = FIELD_BY_CODE[item.code];
+    Object.values(FIELD_BY_CODE).forEach((field) => {
       const value = entity[field];
       if (field && value) {
         formValues[field] = value;
@@ -259,6 +241,7 @@
 
   async function loadLatest() {
     evaluation.value = null;
+    clearFormValues();
     if (!props.manageSystemId) {
       return;
     }
@@ -268,8 +251,6 @@
       const entity = evalType.value === 2 ? data?.miningEval : data?.beforeEval;
       if (entity) {
         applyEvaluation(entity);
-      } else {
-        evaluation.value = null;
       }
     } catch {
       evaluation.value = null;
@@ -362,6 +343,8 @@
 </script>
 
 <style lang="less" scoped>
+@import '/@/views/vent/home/selfIgnition/scrollbar.less';
+
   .manual-view {
     position: absolute;
     inset: 0;
@@ -433,14 +416,7 @@
     overflow-x: hidden;
     padding-right: 2px;
 
-    &::-webkit-scrollbar {
-      width: 4px;
-    }
-
-    &::-webkit-scrollbar-thumb {
-      background: rgba(61, 246, 255, 0.35);
-      border-radius: 2px;
-    }
+    .scrollbar-cyan();
   }
 
   .form-group {
@@ -451,56 +427,48 @@
     }
   }
 
-  .group-collapse {
+  .group-title {
+    position: relative;
     display: flex;
     align-items: center;
-    gap: 6px;
-    height: 34px;
-    padding: 0 8px;
-    border-left: 2px solid #3df6ff;
-    background: linear-gradient(90deg, rgba(15, 77, 120, 0.9), rgba(9, 43, 70, 0.15));
+    gap: 3px;
+    height: 22px;
+    margin-bottom: 4px;
+    padding: 0;
+    box-sizing: border-box;
     color: #fff;
-    font-size: 15px;
-    letter-spacing: 0.5px;
-    cursor: pointer;
-    user-select: none;
-  }
-
-  .group-icon {
-    width: 13px;
-    height: 15px;
-  }
-
-  .collapse-arrow {
-    margin-left: auto;
-    width: 0;
-    height: 0;
-    border-left: 4px solid transparent;
-    border-right: 4px solid transparent;
-    border-top: 5px solid #cfefff;
-    transition: transform 0.2s;
+    font-size: 16px;
+    font-weight: 600;
+    letter-spacing: 1px;
 
-    &.collapsed {
-      transform: rotate(-90deg);
+    // 底部的渐变光带(左侧亮、向右淡出至消失)
+    &::before {
+      content: '';
+      position: absolute;
+      left: 4px;
+      right: 0;
+      bottom: 0;
+      height: 13px;
+      background: linear-gradient(90deg, rgba(43, 143, 214, 0.55) 0%, rgba(24, 86, 140, 0.18) 45%, rgba(12, 40, 70, 0) 78%);
+      pointer-events: none;
     }
-  }
 
-  .group-title {
-    display: flex;
-    align-items: center;
-    gap: 6px;
-    height: 24px;
-    padding: 0 4px;
-    color: #a9dcf4;
-    font-size: 12px;
-    letter-spacing: 0.5px;
+    > * {
+      position: relative;
+      z-index: 1;
+    }
   }
 
   .title-bars {
+    position: relative;
     display: flex;
     align-items: flex-end;
-    gap: 2px;
-    height: 12px;
+    gap: 1px;
+    width: 14px;
+    height: 14px;
+    padding-bottom: 2px;
+    box-sizing: border-box;
+    border-bottom: 2px solid #6bd9ff;
 
     i {
       width: 3px;
@@ -511,36 +479,38 @@
       }
 
       &:nth-child(2) {
-        height: 12px;
+        height: 9px;
       }
 
       &:nth-child(3) {
-        height: 8px;
+        height: 12px;
       }
 
       &:nth-child(4) {
-        height: 4px;
+        height: 6px;
       }
     }
   }
 
   .title-dots {
+    position: absolute;
+    right: 10px;
+    bottom: 4px;
     display: flex;
     align-items: center;
     gap: 3px;
-    margin-left: auto;
 
     i {
-      width: 2px;
-      height: 2px;
-      background: rgba(61, 246, 255, 0.65);
+      width: 3px;
+      height: 3px;
+      background: rgba(110, 200, 255, 0.75);
     }
 
     b {
-      width: 3px;
-      height: 3px;
-      background: #3df6ff;
-      box-shadow: 0 0 4px rgba(61, 246, 255, 0.9);
+      width: 5px;
+      height: 5px;
+      background: #a9e6ff;
+      box-shadow: 0 0 5px rgba(61, 246, 255, 0.9);
     }
   }
 
@@ -610,90 +580,68 @@
   .result-panel {
     display: flex;
     flex-direction: column;
-    justify-content: space-evenly;
     height: 100%;
-    padding: 10px 12px 14px;
+    padding: 28px 12px 0;
     box-sizing: border-box;
   }
 
   .result-top {
-    display: flex;
     flex: none;
-    flex-direction: column;
-    justify-content: center;
-    height: 180px;
-    box-sizing: border-box;
-    background: url('@/assets/images/vent/home/selfIgnition/result-top-bg.png') no-repeat center / 100% 100%;
+    padding: 0 16px;
   }
 
   .score-line {
     display: flex;
     align-items: baseline;
-    padding: 0 20px;
+    justify-content: space-between;
   }
 
   .score-label {
     color: #fff;
-    font-size: 19px;
+    font-size: 34px;
+    line-height: 1;
     letter-spacing: 1px;
   }
 
-  .score-info {
-    width: 12px;
-    height: 12px;
-    margin-left: 5px;
-    align-self: center;
-    opacity: 0.85;
-  }
-
   .score-value {
-    margin-left: 16px;
     color: #fff;
-    font-size: 42px;
+    font-size: 54px;
     font-weight: 700;
     line-height: 1;
     letter-spacing: 1px;
-    text-shadow: 0 0 16px rgba(255, 255, 255, 0.35);
+    text-shadow: 0 0 16px rgba(255, 255, 255, 0.3);
   }
 
   .gauge-line {
     display: flex;
     align-items: center;
     justify-content: space-between;
-    margin-top: 26px;
-    padding: 0 18px;
+    margin-top: 12px;
   }
 
   .gauge {
     display: flex;
     align-items: center;
-    width: 150px;
-    height: 26px;
+    width: 149px;
+    height: 27px;
   }
 
   .gauge-segment {
-    width: 80px;
-    height: 22px;
-    object-fit: fill;
-
-    & + & {
-      margin-left: -8px;
-    }
-  }
-
-  .gauge-segment-2 {
-    opacity: 0.72;
+    display: block;
+    width: 100%;
+    height: 100%;
   }
 
   .risk-tag {
     display: flex;
     align-items: center;
+    justify-content: center;
     width: 159px;
     height: 34px;
-    padding-left: 22px;
+    padding: 0 12px 0 6px;
     box-sizing: border-box;
     color: #fff;
-    font-size: 15px;
+    font-size: 19px;
     font-weight: 600;
     letter-spacing: 0.5px;
     white-space: nowrap;
@@ -707,32 +655,34 @@
     display: flex;
     flex-direction: column;
     justify-content: center;
-    gap: 4px;
-    padding: 12px 16px;
-    border-left: 1px solid rgba(61, 246, 255, 0.16);
-    border-right: 1px solid rgba(61, 246, 255, 0.16);
-    background: rgba(7, 42, 70, 0.62);
+    width: calc(100% - 18px);
+    height: 139px;
+    margin: 38px auto 0;
+    padding: 0 20px;
+    box-sizing: border-box;
+    background: url('@/assets/images/vent/home/selfIgnition/result-top-bg.png') no-repeat center / 100% 100%;
   }
 
   .info-row {
     display: flex;
     align-items: center;
     justify-content: space-between;
-    height: 46px;
+    height: 34px;
     color: #fff;
-    font-size: 15px;
+    font-size: 19px;
     letter-spacing: 0.5px;
 
     em {
       color: #3df6ff;
       font-style: normal;
-      font-size: 16px;
+      font-size: 19px;
       text-shadow: 0 0 8px rgba(61, 246, 255, 0.35);
     }
   }
 
   .level-table {
     flex: none;
+    margin-top: 33px;
   }
 
   .level-head,
@@ -743,19 +693,17 @@
   }
 
   .level-head {
-    height: 40px;
-    border: 1px solid rgba(61, 246, 255, 0.4);
-    border-radius: 4px;
+    height: 32px;
     color: #3df6ff;
-    font-size: 13px;
+    font-size: 17px;
     background: url('@/assets/images/vent/home/selfIgnition/result-table-head.png') no-repeat center / 100% 100%;
 
     span:nth-child(1) {
-      width: 42%;
+      width: 36%;
     }
 
     span:nth-child(2) {
-      width: 32%;
+      width: 38%;
     }
 
     span:nth-child(3) {
@@ -765,17 +713,17 @@
 
   .level-row {
     position: relative;
-    height: 50px;
+    height: 59px;
     color: #e8f8ff;
-    font-size: 14px;
-    background: url('@/assets/images/vent/home/selfIgnition/result-row-glow.png') no-repeat center bottom / 100% 10px;
+    font-size: 17px;
+    background: url('@/assets/images/vent/home/selfIgnition/result-row-glow.png') no-repeat center bottom / 100% 25px;
 
     span:nth-child(1) {
-      width: 42%;
+      width: 36%;
     }
 
     span:nth-child(2) {
-      width: 32%;
+      width: 38%;
     }
 
     span:nth-child(3) {
@@ -783,10 +731,15 @@
     }
 
     &.highlight {
-      outline: 1px solid var(--highlight-color, #ff9d1c);
-      outline-offset: -1px;
-      background-color: rgba(255, 157, 28, 0.08);
-      box-shadow: inset 0 0 18px rgba(255, 157, 28, 0.16);
+      &::after {
+        content: '';
+        position: absolute;
+        inset: 2px;
+        border: 2px solid var(--highlight-color, #ff9d1c);
+        border-radius: 6px;
+        box-shadow: 0 0 10px rgba(255, 157, 28, 0.25);
+        pointer-events: none;
+      }
     }
   }
 
@@ -794,13 +747,18 @@
     display: flex;
     align-items: center;
     justify-content: center;
-    gap: 6px;
+    gap: 7px;
 
     i {
-      width: 7px;
-      height: 7px;
+      flex: none;
+      width: 9px;
+      height: 9px;
       border-radius: 50%;
     }
+
+    b {
+      font-weight: 600;
+    }
   }
 
   .action-button {

+ 134 - 0
src/views/vent/home/selfIgnition/components/Module.vue

@@ -0,0 +1,134 @@
+<template>
+  <div class="ignition-module" :class="{ 'is-wide': isWide }" :style="rootStyle">
+    <div class="module-top"></div>
+    <div class="module-body">
+      <slot></slot>
+    </div>
+    <div class="module-footer"></div>
+    <div class="module-header">
+      <span class="module-title" :title="title">{{ title }}</span>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+  import { computed } from 'vue';
+
+  const props = defineProps<{
+    /** 设计稿坐标(1920 × 1080) */
+    x: number;
+    y: number;
+    width: number;
+    height: number;
+    title: string;
+  }>();
+
+  /** 宽模块(> 800)使用宽版三段边框图 */
+  const isWide = computed(() => props.width > 800);
+
+  const rootStyle = computed(() => ({
+    left: `${props.x}px`,
+    top: `${props.y}px`,
+    width: `${props.width}px`,
+    height: `${props.height}px`,
+  }));
+</script>
+
+<style lang="less" scoped>
+  .ignition-module {
+    position: absolute;
+    z-index: 1;
+    pointer-events: auto;
+
+    .module-top,
+    .module-footer {
+      position: absolute;
+      left: 0;
+      right: 0;
+      height: 48px;
+      background-repeat: no-repeat;
+      background-position: center;
+      background-size: 100% 100%;
+    }
+    .module-footer {
+      height: 30px;
+    }
+
+    .module-top {
+      top: 0;
+      background-image: url('@/assets/images/vent/home/selfIgnition/module-border-top.png');
+    }
+
+    .module-footer {
+      bottom: 0;
+      z-index: 1;
+      background-image: url('@/assets/images/vent/home/selfIgnition/module-border-bottom.png');
+      pointer-events: none;
+    }
+
+    // 正文从中段贴图开始,向上压住上盖下缘,与标题带衔接
+    .module-body {
+      position: absolute;
+      left: 0;
+      right: 0;
+      top: 48px;
+      bottom: 30px;
+      overflow: hidden;
+      background: url('@/assets/images/vent/home/selfIgnition/module-border-middle.png') no-repeat center / 100% 100%;
+    }
+
+    // 标题带(1-6)贴在模块上边框内侧
+    .module-header {
+      position: absolute;
+      top: 8px;
+      left: 8px;
+      right: 1px;
+      z-index: 2;
+      display: flex;
+      align-items: center;
+      height: 35px;
+      width: 400px;
+      padding: 0 12px 0 20px;
+      box-sizing: border-box;
+      background: url('@/assets/images/vent/home/selfIgnition/module-title-bg.png') no-repeat center / 100% 100%;
+      pointer-events: none;
+    }
+
+    .module-title {
+      display: flex;
+      align-items: center;
+      color: #fff;
+      font-size: 16px;
+      font-weight: 700;
+      letter-spacing: 1px;
+      white-space: nowrap;
+      text-shadow: 0 0 8px rgba(61, 246, 255, 0.45);
+
+      &::before {
+        content: '';
+        flex: none;
+        width: 9px;
+        height: 9px;
+        margin-right: 6px;
+        border: 2px solid #3df6ff;
+        background: linear-gradient(135deg, transparent 46%, rgba(61, 246, 255, 0.9) 47%, rgba(61, 246, 255, 0.9) 53%, transparent 54%);
+        box-shadow: 0 0 8px rgba(61, 246, 255, 0.45);
+        transform: rotate(45deg);
+      }
+    }
+
+    &.is-wide {
+      .module-top {
+        background-image: url('@/assets/images/vent/home/selfIgnition/module-border-top-wide.png');
+      }
+
+      .module-body {
+        background-image: url('@/assets/images/vent/home/selfIgnition/module-border-middle-wide.png');
+      }
+
+      .module-footer {
+        background-image: url('@/assets/images/vent/home/selfIgnition/module-border-bottom-wide.png');
+      }
+    }
+  }
+</style>

ファイルの差分が大きいため隠しています
+ 834 - 231
src/views/vent/home/selfIgnition/components/MonitorView.vue


+ 37 - 157
src/views/vent/home/selfIgnition/components/OverviewView.vue

@@ -1,15 +1,7 @@
 <template>
   <div class="ignition-view overview-view">
     <!-- 左:矿井采空区概览 -->
-    <ModuleCommon
-      class="overview-module"
-      :show-style="panelShowStyle(20, 114, 393, 456)"
-      :module-data="EMPTY_MODULE_DATA"
-      module-name="矿井采空区概览"
-      device-type="ignition_goaf_overview"
-      :data="{}"
-      :visible="true"
-    >
+    <Module class="overview-module" :x="20" :y="114" :width="393" :height="576" title="矿井采空区概览">
       <div class="overview-panel">
         <div v-for="item in overview" :key="item.label" class="goaf-item">
           <span class="goaf-icon">
@@ -19,18 +11,10 @@
           <span class="goaf-value">{{ item.value }}</span>
         </div>
       </div>
-    </ModuleCommon>
+    </Module>
 
     <!-- 左下:安装设备情况 -->
-    <ModuleCommon
-      class="install-module"
-      :show-style="panelShowStyle(20, 602, 393, 456)"
-      :module-data="EMPTY_MODULE_DATA"
-      module-name="安装设备情况"
-      device-type="ignition_device_install"
-      :data="{}"
-      :visible="true"
-    >
+    <Module class="install-module" :x="20" :y="722" :width="393" :height="336" title="安装设备情况">
       <div class="install-panel">
         <div
           v-for="(device, index) in devices"
@@ -52,18 +36,10 @@
           >
         </div>
       </div>
-    </ModuleCommon>
+    </Module>
 
     <!-- 右下:分级预警表 -->
-    <ModuleCommon
-      class="table-module"
-      :show-style="panelShowStyle(433, 722, 1465, 336)"
-      :module-data="EMPTY_MODULE_DATA"
-      module-name="矿井采空区分级预警"
-      device-type="ignition_warning_table"
-      :data="{}"
-      :visible="true"
-    >
+    <Module class="table-module" :x="433" :y="722" :width="1465" :height="336" title="矿井采空区分级预警">
       <div class="warning-table-panel">
         <div class="warning-table">
           <div class="table-head">
@@ -72,7 +48,6 @@
             <span class="col-seam">所属煤层</span>
             <span class="col-level">预警等级</span>
             <span class="col-safety">安全评价等级</span>
-            <span class="col-status">状态</span>
           </div>
           <div class="table-body">
             <div v-for="row in warnings" :key="row.id" class="table-row">
@@ -80,36 +55,29 @@
               <span class="col-name">{{ row.name }}</span>
               <span class="col-seam">{{ row.seam }}</span>
               <span class="col-level level-cell">
-                <img class="level-icon warning-icon" :src="warningLevelIcon" alt="" />
-                <span>{{ row.level }}</span>
+                <img v-if="row.level && row.level !== '未预警'" class="level-icon warning-icon" :src="warningLevelIcon" alt="" />
+                <span :title="row.level">{{ row.level }}</span>
               </span>
               <span class="col-safety level-cell">
-                <img class="level-icon safety-icon" :src="safetyLevelIcon" alt="" />
-                <span>{{ row.level }}</span>
-              </span>
-              <span class="col-status status-cell">
-                <i class="status-dot" :class="statusDotClass(row.status)"></i>
-                <span>{{ row.status }}</span>
+                <img v-if="showSafetyIcon(row.safetyLevel || row.level)" class="level-icon safety-icon" :src="safetyLevelIcon" alt="" />
+                <span :title="row.safetyLevel || row.level">{{ row.safetyLevel || row.level }}</span>
               </span>
             </div>
             <div v-for="n in emptyRowCount" :key="`empty-${n}`" class="table-row table-row-empty"></div>
           </div>
         </div>
       </div>
-    </ModuleCommon>
+    </Module>
   </div>
 </template>
 
 <script lang="ts" setup>
   import { computed } from 'vue';
-  import ModuleCommon from '../../configurable/components/ModuleCommon.vue';
-  import { EMPTY_MODULE_DATA, panelShowStyle } from '../utils';
+  import Module from './Module.vue';
   import type { DeviceInstallItem, GoafOverviewItem, WarningRow } from '../data';
   import bundleMonitorIcon from '@/assets/images/vent/home/selfIgnition/install-bundle-monitor.svg';
   import fiberTemperatureIcon from '@/assets/images/vent/home/selfIgnition/install-fiber-temperature.svg';
   import sealedMonitorIcon from '@/assets/images/vent/home/selfIgnition/install-sealed-monitor.svg';
-  import windSpeedIcon from '@/assets/images/vent/home/selfIgnition/install-wind-speed.svg';
-  import windVolumeIcon from '@/assets/images/vent/home/selfIgnition/install-wind-volume.svg';
   import warningLevelIcon from '@/assets/images/vent/home/selfIgnition/level-warning.svg';
   import safetyLevelIcon from '@/assets/images/vent/home/selfIgnition/level-safety.svg';
 
@@ -123,19 +91,14 @@
     束管监测: bundleMonitorIcon,
     光纤测温: fiberTemperatureIcon,
     密闭监测: sealedMonitorIcon,
-    风速传感器: windSpeedIcon,
-    风量传感器: windVolumeIcon,
   };
 
   function deviceIcon(name: string) {
     return DEVICE_ICONS[name] || '';
   }
 
-  function statusDotClass(status: string) {
-    if (status === '回采中') return 'is-running';
-    if (status === '回撤中') return 'is-retreating';
-    if (status === '停产') return 'is-stopped';
-    return 'is-closed';
+  function showSafetyIcon(level: string | undefined) {
+    return !!level && level !== '安全' && level !== '未评价';
   }
 
   // 表体固定 5 行视口,数据不足时用空行补足;超过 5 行时在表体内部滚动
@@ -143,42 +106,25 @@
 </script>
 
 <style lang="less" scoped>
+@import '/@/views/vent/home/selfIgnition/scrollbar.less';
+
   .overview-view {
     position: absolute;
     inset: 0;
   }
 
-  :deep(.overview-module .box1-center),
-  :deep(.install-module .box1-center),
-  :deep(.table-module .box1-center) {
-    height: calc(100% - 35px) !important;
-  }
-
   .overview-panel {
     display: flex;
     flex-direction: column;
+    justify-content: space-evenly;
     gap: 24px;
     height: 100%;
-    padding: 16px 10px;
+    padding: 0px 10px 0px 20px;
     box-sizing: border-box;
     overflow-x: hidden;
     overflow-y: auto;
     scrollbar-gutter: stable;
-    scrollbar-width: thin;
-    scrollbar-color: rgba(61, 246, 255, 0.75) transparent;
-
-    &::-webkit-scrollbar {
-      width: 8px;
-    }
-
-    &::-webkit-scrollbar-track {
-      background: transparent;
-    }
-
-    &::-webkit-scrollbar-thumb {
-      border-radius: 4px;
-      background: linear-gradient(180deg, #3df6ff 0%, #0d7fc1 100%);
-    }
+    .scrollbar-cyan();
   }
 
   .goaf-item {
@@ -238,26 +184,13 @@
   .install-panel {
     display: flex;
     flex-direction: column;
+    justify-content: space-evenly;
     height: 100%;
-    padding: 12px 0 8px;
+    padding: 8px 0;
     box-sizing: border-box;
     overflow-x: hidden;
     overflow-y: auto;
-    scrollbar-width: thin;
-    scrollbar-color: rgba(61, 246, 255, 0.75) transparent;
-
-    &::-webkit-scrollbar {
-      width: 8px;
-    }
-
-    &::-webkit-scrollbar-track {
-      background: transparent;
-    }
-
-    &::-webkit-scrollbar-thumb {
-      border-radius: 4px;
-      background: linear-gradient(180deg, #3df6ff 0%, #0d7fc1 100%);
-    }
+    .scrollbar-cyan();
   }
 
   .install-item {
@@ -351,33 +284,19 @@
     display: flex;
     flex-direction: column;
     height: 100%;
-    padding: 23px 22px 0;
+    padding: 8px 22px 0;
     box-sizing: border-box;
   }
 
   .warning-table {
-    flex: none;
-    height: 251px;
+    flex: 1 1 auto;
+    height: 100%;
     min-height: 0;
     box-sizing: border-box;
     overflow-y: auto;
     border: 1px solid rgba(61, 246, 255, 0.55);
     background: rgba(6, 30, 52, 0.2);
-    scrollbar-width: thin;
-    scrollbar-color: rgba(61, 246, 255, 0.75) transparent;
-
-    &::-webkit-scrollbar {
-      width: 8px;
-    }
-
-    &::-webkit-scrollbar-track {
-      background: transparent;
-    }
-
-    &::-webkit-scrollbar-thumb {
-      border-radius: 4px;
-      background: linear-gradient(180deg, #3df6ff 0%, #0d7fc1 100%);
-    }
+    .scrollbar-cyan();
   }
 
   .table-head,
@@ -419,36 +338,37 @@
   }
 
   .col-index {
-    width: 8%;
+    width: 9%;
   }
 
   .col-name {
-    width: 21%;
+    width: 24%;
   }
 
   .col-seam {
-    width: 17%;
+    width: 18%;
   }
 
   .col-level {
-    width: 18%;
+    width: 24%;
   }
 
   .col-safety {
-    width: 18%;
-  }
-
-  .col-status {
-    width: 18%;
+    width: 25%;
   }
 
-  .level-cell,
-  .status-cell {
+  .level-cell {
     display: flex;
     align-items: center;
     justify-content: center;
     gap: 7px;
     min-width: 0;
+
+    > span {
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
   }
 
   .level-icon {
@@ -465,44 +385,4 @@
     width: 12px;
     height: 15px;
   }
-
-  .status-cell {
-    gap: 8px;
-  }
-
-  .status-dot {
-    flex: none;
-    width: 8px;
-    height: 8px;
-    border-radius: 50%;
-    background: var(--dot-color);
-    box-shadow: 0 0 8px var(--dot-color);
-
-    &.is-running {
-      --dot-color: #3df6a3;
-    }
-
-    &.is-retreating {
-      --dot-color: #3fa9ff;
-    }
-
-    &.is-stopped {
-      --dot-color: #a6a6a6;
-    }
-
-    &.is-closed {
-      --dot-color: #71808f;
-    }
-  }
-
-  /* 表格面板较宽,使用短边框图拉伸,保证顶部/底部装饰点居中 */
-  :deep(.table-module .box1-top) {
-    --image-box1-top: url('@/assets/images/vent/border/box1-top.png');
-    background-image: var(--image-box1-top) !important;
-  }
-
-  :deep(.table-module .box1-bottom) {
-    --image-box1-bottom: url('@/assets/images/vent/border/box1-bottom.png');
-    background-image: var(--image-box1-bottom) !important;
-  }
 </style>

+ 132 - 22
src/views/vent/home/selfIgnition/components/PanelSelect.vue

@@ -7,33 +7,34 @@
     tabindex="0"
     :aria-expanded="open"
     @click.stop="toggle"
-    @keydown.enter.prevent="toggle"
-    @keydown.space.prevent="toggle"
-    @keydown.esc="open = false"
+    @keydown="handleKeydown"
   >
     <span class="ignition-select-value" :title="selectedLabel">{{ selectedLabel }}</span>
     <i class="ignition-select-arrow"></i>
     <Transition name="ignition-drop">
-      <div v-if="open" class="ignition-select-dropdown">
+      <div v-if="open" ref="dropdownRef" class="ignition-select-dropdown" @click.stop>
+        <input v-if="options.length > 5" ref="searchRef" v-model="query" class="ignition-select-search" type="text" placeholder="搜索" @click.stop />
         <div
-          v-for="option in options"
+          v-for="(option, index) in filteredOptions"
           :key="option.value"
           class="ignition-select-option"
           role="option"
           :title="option.label"
           :aria-selected="option.value === modelValue"
-          :class="{ active: option.value === modelValue }"
+          :class="{ active: option.value === modelValue, highlighted: index === highlightIndex }"
+          @mouseenter="highlightIndex = index"
           @click.stop="choose(option)"
         >
           {{ option.label }}
         </div>
+        <div v-if="!filteredOptions.length" class="ignition-select-empty">无匹配项</div>
       </div>
     </Transition>
   </div>
 </template>
 
 <script lang="ts" setup>
-  import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue';
+  import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
   import type { ManualOption } from '../data';
 
   const props = defineProps<{
@@ -46,13 +47,27 @@
   }>();
 
   const rootRef = ref<HTMLElement>();
+  const dropdownRef = ref<HTMLElement>();
+  const searchRef = ref<HTMLInputElement>();
   const open = ref(false);
   const dropUp = ref(false);
+  const query = ref('');
+  const highlightIndex = ref(-1);
 
   const selectedLabel = computed(() => {
     return props.options.find((option) => option.value === props.modelValue)?.label ?? '';
   });
 
+  const filteredOptions = computed(() => {
+    const keyword = query.value.trim().toLowerCase();
+    if (!keyword) return props.options;
+    return props.options.filter((option) => option.label.toLowerCase().includes(keyword));
+  });
+
+  watch(query, () => {
+    highlightIndex.value = filteredOptions.value.length ? 0 : -1;
+  });
+
   function findScrollParent(element: HTMLElement | undefined): HTMLElement | null {
     let node = element?.parentElement;
     while (node) {
@@ -65,30 +80,91 @@
     return null;
   }
 
+  function updateDropDirection() {
+    const rect = rootRef.value?.getBoundingClientRect();
+    if (!rect) return;
+    const scrollParent = findScrollParent(rootRef.value);
+    const bounds = scrollParent ? scrollParent.getBoundingClientRect() : { top: 0, bottom: window.innerHeight };
+    const spaceBelow = bounds.bottom - rect.bottom;
+    const spaceAbove = rect.top - bounds.top;
+    dropUp.value = spaceBelow < 220 && spaceAbove > spaceBelow;
+  }
+
+  function openDropdown() {
+    open.value = true;
+    query.value = '';
+    highlightIndex.value = Math.max(
+      0,
+      props.options.findIndex((option) => option.value === props.modelValue)
+    );
+    nextTick(() => {
+      updateDropDirection();
+      if (props.options.length > 5) {
+        searchRef.value?.focus();
+      }
+    });
+  }
+
+  function closeDropdown() {
+    open.value = false;
+    query.value = '';
+    highlightIndex.value = -1;
+  }
+
   function toggle() {
-    open.value = !open.value;
-    if (open.value) {
-      nextTick(() => {
-        const rect = rootRef.value?.getBoundingClientRect();
-        if (!rect) return;
-        const scrollParent = findScrollParent(rootRef.value);
-        const bounds = scrollParent ? scrollParent.getBoundingClientRect() : { top: 0, bottom: window.innerHeight };
-        const spaceBelow = bounds.bottom - rect.bottom;
-        const spaceAbove = rect.top - bounds.top;
-        dropUp.value = spaceBelow < 220 && spaceAbove > spaceBelow;
-      });
-    }
+    if (open.value) closeDropdown();
+    else openDropdown();
   }
 
   function choose(option: ManualOption) {
     emit('update:modelValue', option.value);
-    open.value = false;
+    closeDropdown();
+  }
+
+  function moveHighlight(step: number) {
+    const list = filteredOptions.value;
+    if (!list.length) {
+      highlightIndex.value = -1;
+      return;
+    }
+    highlightIndex.value = (highlightIndex.value + step + list.length) % list.length;
+    nextTick(() => {
+      const target = dropdownRef.value?.querySelector('.ignition-select-option.highlighted');
+      target?.scrollIntoView({ block: 'nearest' });
+    });
+  }
+
+  function handleKeydown(event: KeyboardEvent) {
+    if (event.key === 'Escape') {
+      closeDropdown();
+      return;
+    }
+
+    if (!open.value) {
+      if (event.key === 'Enter' || event.key === 'ArrowDown') {
+        event.preventDefault();
+        openDropdown();
+      }
+      return;
+    }
+
+    if (event.key === 'ArrowDown') {
+      event.preventDefault();
+      moveHighlight(1);
+    } else if (event.key === 'ArrowUp') {
+      event.preventDefault();
+      moveHighlight(-1);
+    } else if (event.key === 'Enter') {
+      event.preventDefault();
+      const option = filteredOptions.value[highlightIndex.value];
+      if (option) choose(option);
+    }
   }
 
   function handleDocumentClick(event: MouseEvent) {
     if (!open.value) return;
     if (rootRef.value && !rootRef.value.contains(event.target as Node)) {
-      open.value = false;
+      closeDropdown();
     }
   }
 
@@ -179,6 +255,35 @@
     bottom: calc(100% + 4px);
   }
 
+  .ignition-select-search {
+    width: 100%;
+    height: 26px;
+    margin-bottom: 4px;
+    padding: 0 8px;
+    box-sizing: border-box;
+    border: 1px solid rgba(61, 246, 255, 0.35);
+    border-radius: 3px;
+    outline: none;
+    background: rgba(5, 28, 48, 0.85);
+    color: #fff;
+    font-size: 12px;
+
+    &::placeholder {
+      color: rgba(169, 220, 244, 0.65);
+    }
+
+    &:focus {
+      border-color: rgba(61, 246, 255, 0.8);
+    }
+  }
+
+  .ignition-select-empty {
+    padding: 8px;
+    color: #8fc7e8;
+    font-size: 12px;
+    text-align: center;
+  }
+
   .ignition-select-option {
     min-height: 30px;
     padding: 5px 8px;
@@ -189,7 +294,8 @@
     word-break: break-all;
     cursor: pointer;
 
-    &:hover {
+    &:hover,
+    &.highlighted {
       color: #fff;
       background: rgba(61, 246, 255, 0.16);
     }
@@ -198,6 +304,10 @@
       color: #3df6ff;
       background: rgba(61, 246, 255, 0.1);
     }
+
+    &.active.highlighted {
+      background: rgba(61, 246, 255, 0.22);
+    }
   }
 
   .ignition-drop-enter-active,

+ 2 - 3
src/views/vent/home/selfIgnition/data.ts

@@ -29,7 +29,8 @@ export interface WarningRow {
   name: string;
   seam: string;
   level: string;
-  status: string;
+  safetyLevel?: string;
+  status?: string;
 }
 
 export interface AlertItem {
@@ -110,8 +111,6 @@ export const INSTALLED_DEVICES: DeviceInstallItem[] = [
   { name: '束管监测', installed: 15, running: 13 },
   { name: '光纤测温', installed: 15, running: 13 },
   { name: '密闭监测', installed: 15, running: 13 },
-  { name: '风速传感器', installed: 15, running: 13 },
-  { name: '风量传感器', installed: 15, running: 13 },
 ];
 
 export const OVERALL_WARNING: OverallWarning = {

+ 155 - 0
src/views/vent/home/selfIgnition/goafOverview.adapter.ts

@@ -0,0 +1,155 @@
+import type { DeviceInstallItem, GoafOverviewItem, WarningRow } from './data';
+import type {
+  DeviceKindStatistic,
+  DeviceStatistics,
+  DeviceTypeStatistic,
+  GoafCountOverview,
+  GoafOverviewSummary,
+  SafetyEvaluationItem,
+  SensorAlarmItem,
+} from './goafOverview.api';
+
+export interface OverviewSummaryViewData {
+  overview: GoafOverviewItem[];
+  devices: DeviceInstallItem[];
+  warnings: WarningRow[];
+}
+
+interface DeviceBucket {
+  name: string;
+  typeKeywords: string[];
+  kindKeywords: string[];
+}
+
+const DEVICE_BUCKETS: DeviceBucket[] = [
+  { name: '束管监测', typeKeywords: ['束管', 'bundletube'], kindKeywords: ['束管', 'bundletube'] },
+  { name: '光纤测温', typeKeywords: ['光纤', 'fiber'], kindKeywords: ['光纤', 'fiber'] },
+  { name: '密闭监测', typeKeywords: ['密闭', 'sealed'], kindKeywords: ['密闭', 'sealed'] },
+];
+
+const ALARM_LEVEL_TEXT: Record<number, string> = {
+  104: '高风险',
+  103: '中等风险',
+  102: '低风险',
+  101: '一般风险',
+};
+
+function toNumber(value: unknown, fallback = 0) {
+  const result = Number(value);
+  return Number.isFinite(result) ? result : fallback;
+}
+
+function includesAny(text: string, keywords: string[]) {
+  const source = text.toLowerCase();
+  return keywords.some((keyword) => source.includes(keyword.toLowerCase()));
+}
+
+function buildOverview(counts: GoafCountOverview | undefined, fallback: GoafOverviewItem[]): GoafOverviewItem[] {
+  if (!counts) return fallback;
+
+  const fallbackMap = new Map(fallback.map((item) => [item.label, item.value]));
+  const pick = (value: number | undefined, label: string) =>
+    value === null || value === undefined ? (fallbackMap.get(label) ?? 0) : toNumber(value);
+
+  return [
+    { label: '矿井采空区总数', value: pick(counts.totalCount, '矿井采空区总数') },
+    { label: '已封闭采空区数', value: pick(counts.closedCount, '已封闭采空区数') },
+    { label: '回撤工作面采空区数', value: pick(counts.withdrawCount, '回撤工作面采空区数') },
+    { label: '回采工作面采空区数', value: pick(counts.miningCount, '回采工作面采空区数') },
+    { label: '停产工作面采空区数', value: pick(counts.stoppedCount, '停产工作面采空区数') },
+  ];
+}
+
+function sumTypeStatistics(statistics: DeviceTypeStatistic[], keywords: string[]) {
+  return statistics.reduce((total, item) => {
+    const text = `${item.deviceTypeName || ''} ${item.deviceType || ''}`;
+    return includesAny(text, keywords) ? total + toNumber(item.count) : total;
+  }, 0);
+}
+
+function sumKindStatistics(statistics: DeviceKindStatistic[], keywords: string[]) {
+  return statistics.reduce((total, item) => {
+    const text = `${item.deviceKindName || ''} ${item.deviceKind || ''}`;
+    return includesAny(text, keywords) ? total + toNumber(item.count) : total;
+  }, 0);
+}
+
+function buildDevices(statistics: DeviceStatistics | undefined, fallback: DeviceInstallItem[]): DeviceInstallItem[] {
+  if (!statistics) return fallback;
+
+  const typeStatistics = statistics.typeStatistics || [];
+  const kindStatistics = statistics.kindStatistics || [];
+  if (!typeStatistics.length && !kindStatistics.length) return fallback;
+
+  return DEVICE_BUCKETS.map((bucket) => {
+    const typeCount = sumTypeStatistics(typeStatistics, bucket.typeKeywords);
+    const count = typeCount || sumKindStatistics(kindStatistics, bucket.kindKeywords);
+    return {
+      name: bucket.name,
+      installed: count,
+      // 汇总接口只提供设备接入数量,运行数量暂按接入数量展示
+      running: count,
+    };
+  });
+}
+
+function normalizeAlarmLevel(alarm: SensorAlarmItem | null) {
+  if (!alarm) return '未预警';
+
+  const name = alarm.alarmLevelName?.trim();
+  if (name) return name;
+
+  const code = Number(alarm.alarmLevel);
+  return ALARM_LEVEL_TEXT[code] || '未预警';
+}
+
+function pickAlarm(sceneId: string, alarms: SensorAlarmItem[]) {
+  const matched = alarms.filter((alarm) => String(alarm.sysId ?? '') === sceneId && alarm.isTriggered === true);
+  if (!matched.length) return null;
+
+  return matched.reduce((current, alarm) => (toNumber(alarm.alarmLevel) > toNumber(current.alarmLevel) ? alarm : current));
+}
+
+function normalizeRiskLevel(level: string | undefined) {
+  if (!level) return '未评价';
+  return level === '中风险' ? '中等风险' : level;
+}
+
+function pickSafetyLevel(sceneId: string, evaluations: SafetyEvaluationItem[]) {
+  const matched = evaluations
+    .filter((item) => String(item.manageSystemId ?? item.sysId ?? '') === sceneId)
+    .sort((a, b) => {
+      const timeCompare = (b.updateTime || b.latestEvalTime || '').localeCompare(a.updateTime || a.latestEvalTime || '');
+      return timeCompare || toNumber(b.evalType) - toNumber(a.evalType);
+    });
+  return normalizeRiskLevel(matched[0]?.currentRiskLevel);
+}
+
+function buildWarnings(summary: GoafOverviewSummary): WarningRow[] {
+  const scenes = summary.goafCountOverview?.goafList || [];
+  const alarms = summary.goafAlarmSummary?.sensorAlarmList || [];
+  const evaluations = summary.goafAlarmSummary?.safetyEvaluationList || [];
+
+  return scenes.map((scene, index) => {
+    const sceneId = String(scene.id ?? '');
+    const alarm = pickAlarm(sceneId, alarms);
+    return {
+      id: index + 1,
+      name: scene.systemName || scene.name || '--',
+      seam: scene.coalSeam || scene.properties?.coalSeam || '--',
+      level: normalizeAlarmLevel(alarm),
+      safetyLevel: pickSafetyLevel(sceneId, evaluations),
+    };
+  });
+}
+
+/** 将综合汇总接口转换为系统概览页面的展示数据 */
+export function mapGoafSummaryData(summary: GoafOverviewSummary | undefined, fallback: OverviewSummaryViewData): OverviewSummaryViewData {
+  if (!summary) return fallback;
+
+  return {
+    overview: buildOverview(summary.goafCountOverview, fallback.overview),
+    devices: buildDevices(summary.deviceStatistics, fallback.devices),
+    warnings: buildWarnings(summary),
+  };
+}

+ 104 - 0
src/views/vent/home/selfIgnition/goafOverview.api.ts

@@ -0,0 +1,104 @@
+import { defHttp } from '/@/utils/http/axios';
+
+enum Api {
+  summary = '/safety/goafOverview/summary',
+}
+
+export interface GoafSummaryScene {
+  id?: string | number;
+  systemName?: string;
+  name?: string;
+  strType?: string;
+  strTypeName?: string;
+  coalSeam?: string;
+  status?: number;
+  sysOrgCode?: string;
+  createTime?: string;
+  properties?: Record<string, any>;
+}
+
+export interface GoafCountOverview {
+  totalCount?: number;
+  closedCount?: number;
+  withdrawCount?: number;
+  miningCount?: number;
+  stoppedCount?: number;
+  runningCount?: number;
+  goafList?: GoafSummaryScene[];
+}
+
+export interface DeviceKindStatistic {
+  deviceKind?: string;
+  deviceKindName?: string;
+  count?: number;
+}
+
+export interface DeviceTypeStatistic {
+  deviceType?: string;
+  deviceTypeName?: string;
+  deviceKind?: string;
+  count?: number;
+}
+
+export interface DeviceStatistics {
+  totalDeviceCount?: number;
+  kindStatistics?: DeviceKindStatistic[];
+  typeStatistics?: DeviceTypeStatistic[];
+}
+
+export interface TriggerDevice {
+  managesysAlarmId?: string;
+  deviceId?: string | number;
+  deviceName?: string;
+  devicePos?: string;
+  deviceType?: string;
+  monitorId?: string | number;
+  monitorName?: string;
+  fmin?: number;
+  fmax?: number;
+  currentValue?: string | number | Record<string, any>[];
+  isExceeded?: boolean;
+}
+
+export interface SensorAlarmItem {
+  sysId?: string | number;
+  sysName?: string;
+  alarmId?: string | number;
+  alarmName?: string;
+  systemType?: string;
+  alarmLevel?: number;
+  alarmLevelName?: string;
+  measureRemark?: string;
+  handSug?: string;
+  isTriggered?: boolean;
+  autoContent?: string;
+  alarmTime?: string;
+  triggerDevices?: TriggerDevice[];
+}
+
+export interface SafetyEvaluationItem {
+  evalId?: string | number;
+  manageSystemId?: string | number;
+  sysId?: string | number;
+  workfaceName?: string;
+  evalType?: number;
+  evalTypeName?: string;
+  latestTotalScore?: number;
+  currentRiskLevel?: string;
+  updateTime?: string;
+  latestEvalTime?: string;
+}
+
+export interface GoafAlarmSummary {
+  sensorAlarmList?: SensorAlarmItem[];
+  safetyEvaluationList?: SafetyEvaluationItem[];
+}
+
+export interface GoafOverviewSummary {
+  goafCountOverview?: GoafCountOverview;
+  deviceStatistics?: DeviceStatistics;
+  goafAlarmSummary?: GoafAlarmSummary;
+}
+
+/** 矿井采空区综合数据汇总:概览数量、设备统计与分级预警 */
+export const getGoafOverviewSummary = (params?: Record<string, any>) => defHttp.get({ url: Api.summary, params });

+ 134 - 58
src/views/vent/home/selfIgnition/index.vue

@@ -16,7 +16,6 @@
 
       <!-- 设备监测/人工录入共用的场景选择 -->
       <div v-if="activeTab > 0" class="scene-toolbar">
-        <span class="scene-toolbar-label">场景选择:</span>
         <PanelSelect v-model="manageSystemId" :options="sceneOptions" />
       </div>
 
@@ -47,6 +46,8 @@
   import PanelSelect from './components/PanelSelect.vue';
   import { createSelfIgnitionData } from './data';
   import { getSafetySceneList } from './safetyEvaluation.api';
+  import { getGoafOverviewSummary } from './goafOverview.api';
+  import { mapGoafSummaryData } from './goafOverview.adapter';
   import { FALLBACK_SCENE } from './evaluation.config';
   import type { SafetyScene } from './evaluation.config';
 
@@ -63,6 +64,7 @@
 
   let resizeObserver: ResizeObserver | undefined;
   let dataInterval: number | undefined;
+  let summaryLoading = false;
 
   const isEmbed = computed(() => !!route.query.embed);
 
@@ -100,28 +102,69 @@
     scaleY.value = fitScale / outerScaleY;
   }
 
-  /** 数据接口就绪后,在这里替换成接口请求 */
-  function refresh() {
-    data.value = createSelfIgnitionData();
+  /** 概览数据来自采空区综合汇总接口;接口异常时保留现有兜底数据 */
+  async function refresh() {
+    if (summaryLoading) return;
+    summaryLoading = true;
+    try {
+      const res: any = await getGoafOverviewSummary();
+      const summary = res?.result ?? res;
+      if (!summary || typeof summary !== 'object') return;
+      const mapped = mapGoafSummaryData(summary, createSelfIgnitionData());
+      data.value = { ...data.value, ...mapped };
+    } catch {
+      // 保持上一次成功数据,避免接口短暂异常导致页面回退为模拟数据
+    } finally {
+      summaryLoading = false;
+    }
+  }
+
+  function extractSceneRows(res: any): any[] {
+    const payload = res?.result ?? res;
+    if (Array.isArray(payload)) return payload;
+    if (Array.isArray(payload?.records)) return payload.records;
+    if (Array.isArray(payload?.list)) return payload.list;
+    if (Array.isArray(payload?.sysInfo)) return payload.sysInfo;
+    if (payload?.sysInfo) return [payload.sysInfo];
+    if (Array.isArray(payload?.data)) return payload.data;
+    return [];
+  }
+
+  function toSafetyScene(record: any): SafetyScene {
+    return {
+      id: String(record.id ?? record.deviceID ?? record.sysId ?? ''),
+      systemname: record.systemname || record.systemName || record.name || record.sysname || record.strname || record.strinstallpos || '未命名场景',
+    };
+  }
+
+  async function loadScenesFromSummary(): Promise<SafetyScene[]> {
+    try {
+      const res: any = await getGoafOverviewSummary();
+      const summary = res?.result ?? res;
+      const goafList = summary?.goafCountOverview?.goafList || [];
+      return goafList.map(toSafetyScene).filter((scene) => !!scene.id);
+    } catch {
+      return [];
+    }
   }
 
   async function loadScenes() {
+    let list: SafetyScene[] = [];
     try {
       const res: any = await getSafetySceneList();
-      let records = res?.records || res?.result?.records || [];
-      // 测试环境没有采空区场景时,回退到全部场景
-      if (!records.length) {
-        const allRes: any = await getSafetySceneList({ pagetype: 'normal' });
-        records = allRes?.records || allRes?.result?.records || [];
-      }
-      const list: SafetyScene[] = records.map((record: any) => ({
-        id: String(record.id ?? record.deviceID ?? ''),
-        systemname: record.systemname || record.name || record.sysname || '未命名场景',
-      }));
-      scenes.value = list.length ? list : [{ ...FALLBACK_SCENE }];
+      list = extractSceneRows(res)
+        .map(toSafetyScene)
+        .filter((scene) => !!scene.id);
     } catch {
-      scenes.value = [{ ...FALLBACK_SCENE }];
+      list = [];
     }
+
+    // 场景接口未返回列表时,使用综合汇总中的采空区场景兜底
+    if (!list.length) {
+      list = await loadScenesFromSummary();
+    }
+
+    scenes.value = list.length ? list : [{ ...FALLBACK_SCENE }];
     const queryScene = (route.query.systemID || route.query.manageSystemId || route.query.sceneId) as string;
     if (queryScene && !scenes.value.some((scene) => scene.id === queryScene)) {
       scenes.value.unshift({ id: queryScene, systemname: '当前场景' });
@@ -131,11 +174,16 @@
 
   function initDataInterval() {
     clearInterval(dataInterval);
-    dataInterval = window.setInterval(refresh, 60000);
+    dataInterval = window.setInterval(() => {
+      if (!document.hidden && activeTab.value === 0) {
+        void refresh();
+      }
+    }, 60000);
   }
 
   onMounted(() => {
     void loadScenes();
+    void refresh();
     initDataInterval();
     updateScale();
     if (typeof ResizeObserver !== 'undefined' && rootRef.value) {
@@ -202,86 +250,114 @@
   .ignition-tabs {
     position: absolute;
     left: 50%;
-    top: 64px;
+    top: 80px;
     z-index: 4;
     display: flex;
     align-items: center;
     justify-content: center;
-    gap: 12px;
-    width: 579px;
-    height: 68px;
-    padding: 13px 16px 9px;
-    box-sizing: border-box;
+    gap: 10px;
+    height: 48px;
     transform: translateX(-50%);
-    background: url('@/assets/images/vent/home/selfIgnition/tab-bg.png') no-repeat center / 100% 100%;
+
+    // 标签组两侧向外延伸的斜翼装饰线
+    &::before,
+    &::after {
+      content: '';
+      position: absolute;
+      bottom: 0;
+      width: 216px;
+      height: 39px;
+      background-repeat: no-repeat;
+      background-position: center;
+      background-size: 100% 100%;
+      pointer-events: none;
+    }
+
+    &::before {
+      right: 100%;
+      background-image: url('@/assets/images/vent/home/selfIgnition/tab-wing-left.png');
+    }
+
+    &::after {
+      left: 100%;
+      background-image: url('@/assets/images/vent/home/selfIgnition/tab-wing-right.png');
+    }
   }
 
   .ignition-tab {
     display: flex;
     align-items: center;
     justify-content: center;
-    width: 162px;
-    height: 40px;
+    width: 215px;
+    height: 48px;
     box-sizing: border-box;
     border: none;
     background: url('@/assets/images/vent/home/selfIgnition/tab-item-bg.png') no-repeat center / 100% 100%;
-    color: rgba(255, 255, 255, 0.86);
-    font-size: 15px;
+    color: #e6f7ff;
+    font-size: 18px;
+    font-weight: 700;
     letter-spacing: 0.5px;
     white-space: nowrap;
     cursor: pointer;
     transition:
       color 0.2s,
-      border-color 0.2s,
-      box-shadow 0.2s;
+      filter 0.2s;
 
     &:hover {
       color: #fff;
+      filter: drop-shadow(0 0 6px rgba(61, 246, 255, 0.45));
     }
 
     &.active {
       background: url('@/assets/images/vent/home/selfIgnition/tab-active-bg.png') no-repeat center / 100% 100%;
+      color: #fff;
+      text-shadow:
+        0 0 8px rgba(61, 246, 255, 0.55),
+        0 1px 2px rgba(0, 0, 0, 0.6);
     }
   }
 
   .scene-toolbar {
     position: absolute;
     left: 20px;
-    top: 64px;
+    top: 70px;
     z-index: 6;
-    display: flex;
-    align-items: center;
-    gap: 8px;
-    width: 353px;
-    height: 68px;
-    box-sizing: border-box;
-  }
-
-  .scene-toolbar-label {
-    flex: none;
-    color: #d9f7ff;
-    font-size: 15px;
-    white-space: nowrap;
+    width: 393px;
+    height: 58px;
   }
 
   :deep(.scene-toolbar .ignition-select) {
-    flex: 1;
-    min-width: 0;
-    width: auto;
-    height: 32px;
+    width: 100%;
+    height: 58px;
+    padding: 0 44px 0 20px;
+    box-sizing: border-box;
+    border: none;
+    border-radius: 0;
+    background: url('@/assets/images/vent/home/selfIgnition/1-7.png') no-repeat center / 100% 100%;
+    font-size: 20px;
+    font-weight: 600;
+    letter-spacing: 0.5px;
+
+    &:hover,
+    &.open {
+      border-color: transparent;
+      box-shadow: none;
+    }
   }
 
-  :deep(.box1-top .title) {
-    color: #fff;
-    font-size: 16px;
-    font-weight: 700;
-    letter-spacing: 1px;
-    text-shadow: 0 0 8px rgba(61, 246, 255, 0.35);
-    transform: translateY(2px);
+  :deep(.scene-toolbar .ignition-select-arrow) {
+    right: 18px;
+    width: 14px;
+    height: 14px;
+    margin-top: -9px;
+    border: none;
+    border-right: 2px solid #eaffff;
+    border-bottom: 2px solid #eaffff;
+    transform: rotate(45deg);
   }
 
-  :deep(.box1-center > .box-container) {
-    background-color: #062441 !important;
-    background-image: none;
+  :deep(.scene-toolbar .ignition-select.open .ignition-select-arrow) {
+    margin-top: -4px;
+    transform: rotate(225deg);
   }
 </style>

+ 43 - 0
src/views/vent/home/selfIgnition/monitorDevice.api.ts

@@ -0,0 +1,43 @@
+import { defHttp } from '/@/utils/http/axios';
+
+enum Api {
+  deviceList = '/monitor/device',
+}
+
+export interface MonitorDeviceReadData {
+  o2val?: string | number;
+  coval?: string | number;
+  co2val?: string | number;
+  gasval?: string | number;
+  ch2val?: string | number;
+  chval?: string | number;
+  temperature?: string | number;
+  tTime?: string;
+  isRun?: string | number;
+  [key: string]: any;
+}
+
+export interface MonitorDevice {
+  deviceID?: string | number;
+  strname?: string;
+  strinstallpos?: string;
+  deviceType?: string;
+  typeName?: string;
+  readTime?: string;
+  readData?: MonitorDeviceReadData;
+  [key: string]: any;
+}
+
+export interface MonitorDeviceGroup {
+  type?: string;
+  typeName?: string;
+  datalist?: MonitorDevice[];
+  [key: string]: any;
+}
+
+export interface MonitorDeviceResult {
+  msgTxt?: MonitorDeviceGroup[];
+}
+
+/** 按设备类型获取实时监测设备数据 */
+export const getMonitorDeviceList = (params: Record<string, any>) => defHttp.post({ url: Api.deviceList, params });

+ 43 - 6
src/views/vent/home/selfIgnition/safetyEvaluation.api.ts

@@ -19,8 +19,19 @@ export const getSafetyItemOptions = () => defHttp.get({ url: Api.itemOptions });
 /** 仅计算,不落库 */
 export const calculateSafetyEvaluation = (params: Record<string, any>) => defHttp.post({ url: Api.calculate, params });
 
+const sceneEvaluationCache = new Map<string, { time: number; promise: Promise<any> }>();
+
+function invalidateSceneEvaluation(sceneId?: string) {
+  if (sceneId) sceneEvaluationCache.delete(String(sceneId));
+  else sceneEvaluationCache.clear();
+}
+
 /** 计算并入库 */
-export const saveSafetyEvaluation = (params: Record<string, any>) => defHttp.post({ url: Api.calculateAndSave, params });
+export const saveSafetyEvaluation = (params: Record<string, any>) =>
+  defHttp.post({ url: Api.calculateAndSave, params }).then((res) => {
+    invalidateSceneEvaluation(params?.manageSystemId);
+    return res;
+  });
 
 /** 用 Excel 示例数据计算并入库,evalType: 1=采前 2=采中 */
 export const calculateSampleSafetyEvaluation = (evalType: number) =>
@@ -38,9 +49,35 @@ export const updateSafetyEvaluation = (params: Record<string, any>) => defHttp.p
 /** 按 id 删除评价记录 */
 export const deleteSafetyEvaluation = (id: string) => defHttp.get({ url: Api.remove, params: { id } });
 
-/** 按场景查询最新采前/采中评价 */
-export const getSafetyEvaluationByScene = (sceneId: string) => defHttp.get({ url: Api.getByScene, params: { sceneId } });
+/** 按场景查询最新采前/采中评价;短时间内的重复请求复用同一个结果 */
+export const getSafetyEvaluationByScene = (sceneId: string) => {
+  const key = String(sceneId);
+  const now = Date.now();
+  const cached = sceneEvaluationCache.get(key);
+  if (cached && now - cached.time < 2000) return cached.promise;
+
+  const promise = defHttp.get({ url: Api.getByScene, params: { sceneId } });
+  sceneEvaluationCache.set(key, { time: now, promise });
+  promise.catch(() => {
+    if (sceneEvaluationCache.get(key)?.promise === promise) {
+      sceneEvaluationCache.delete(key);
+    }
+  });
+  return promise;
+};
+
+/** 场景类型:与系统其它页面一致,按场景类型 + pagetype=normal 查询 */
+const SCENE_STRTYPE = 'sys*';
 
-/** 采空区场景列表,用于评价对象绑定;不传参数时默认查采空区场景 */
-export const getSafetySceneList = (params?: Record<string, any>) =>
-  defHttp.get({ url: Api.sceneList, params: params ?? { strtype: 'sys_surface_caimei', pagetype: 'normal' } });
+/** 场景下拉列表,复用系统统一的场景管理接口(res.records: id / systemname) */
+export const getSafetySceneList = (params?: { strtype?: string; pagetype?: string; pageSize?: number }) =>
+  defHttp.get({
+    url: Api.sceneList,
+    params: {
+      strtype: params?.strtype ?? SCENE_STRTYPE,
+      pagetype: params?.pagetype ?? 'normal',
+      // 该接口默认每页 10 条,需显式传 pageSize 才能取全
+      pageSize: params?.pageSize ?? 1000,
+      pageNo: 1,
+    },
+  });

+ 23 - 0
src/views/vent/home/selfIgnition/scrollbar.less

@@ -0,0 +1,23 @@
+/**
+ * 自燃监测页统一滚动条样式:
+ * 6px 宽、青色渐变滑块、透明轨道、无上下箭头按钮。
+ */
+.scrollbar-cyan() {
+  &::-webkit-scrollbar {
+    width: 6px;
+    height: 6px;
+  }
+
+  &::-webkit-scrollbar-button {
+    display: none;
+  }
+
+  &::-webkit-scrollbar-track {
+    background: transparent;
+  }
+
+  &::-webkit-scrollbar-thumb {
+    border-radius: 3px;
+    background: linear-gradient(180deg, #3df6ff 0%, #0d7fc1 100%);
+  }
+}

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません