瀏覽代碼

[Mod 0000]张家峁页面部分接口联调及样式修改

wangkeyi 2 天之前
父節點
當前提交
e7300d4b96

二進制
src/assets/images/vent/home/selfIgnition/1-7.png


二進制
src/assets/images/vent/home/selfIgnition/2-1.png


二進制
src/assets/images/vent/home/selfIgnition/2-2.png


二進制
src/assets/images/vent/home/selfIgnition/2-3.png


二進制
src/assets/images/vent/home/selfIgnition/2-4.png


二進制
src/assets/images/vent/home/selfIgnition/2-5.png


二進制
src/assets/images/vent/home/selfIgnition/2-6.png


二進制
src/assets/images/vent/home/selfIgnition/2-7.png


二進制
src/assets/images/vent/home/selfIgnition/3-1.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,
     () => {

+ 5 - 11
src/views/vent/home/selfIgnition/components/ManualView.vue

@@ -215,12 +215,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 +235,6 @@
       createMessage.warning('评价指标接口异常,已使用内置指标字典');
     } finally {
       loadingOptions.value = false;
-      initDefaults();
     }
   }
 
@@ -248,8 +244,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 +254,7 @@
 
   async function loadLatest() {
     evaluation.value = null;
+    clearFormValues();
     if (!props.manageSystemId) {
       return;
     }
@@ -268,8 +264,6 @@
       const entity = evalType.value === 2 ? data?.miningEval : data?.beforeEval;
       if (entity) {
         applyEvaluation(entity);
-      } else {
-        evaluation.value = null;
       }
     } catch {
       evaluation.value = null;

File diff suppressed because it is too large
+ 799 - 180
src/views/vent/home/selfIgnition/components/MonitorView.vue


+ 23 - 61
src/views/vent/home/selfIgnition/components/OverviewView.vue

@@ -3,7 +3,7 @@
     <!-- 左:矿井采空区概览 -->
     <ModuleCommon
       class="overview-module"
-      :show-style="panelShowStyle(20, 114, 393, 456)"
+      :show-style="panelShowStyle(20, 114, 393, 576)"
       :module-data="EMPTY_MODULE_DATA"
       module-name="矿井采空区概览"
       device-type="ignition_goaf_overview"
@@ -24,7 +24,7 @@
     <!-- 左下:安装设备情况 -->
     <ModuleCommon
       class="install-module"
-      :show-style="panelShowStyle(20, 602, 393, 456)"
+      :show-style="panelShowStyle(20, 722, 393, 336)"
       :module-data="EMPTY_MODULE_DATA"
       module-name="安装设备情况"
       device-type="ignition_device_install"
@@ -72,7 +72,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,16 +79,12 @@
               <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>
@@ -108,8 +103,6 @@
   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 +116,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 行时在表体内部滚动
@@ -157,6 +145,7 @@
   .overview-panel {
     display: flex;
     flex-direction: column;
+    justify-content: space-evenly;
     gap: 24px;
     height: 100%;
     padding: 16px 10px;
@@ -238,8 +227,9 @@
   .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;
@@ -419,36 +409,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%;
+    width: 25%;
   }
 
-  .col-status {
-    width: 18%;
-  }
-
-  .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 {
@@ -466,35 +457,6 @@
     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');

+ 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 });

+ 98 - 34
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 }];
+      const res: any = await getSafetySceneList({ pagetype: 'normal' });
+      list = extractSceneRows(res)
+        .map(toSafetyScene)
+        .filter((scene) => !!scene.id);
     } catch {
-      scenes.value = [{ ...FALLBACK_SCENE }];
+      list = [];
     }
+
+    // /monitor/system 未返回列表时,使用综合汇总中的采空区场景兜底
+    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) {
@@ -247,28 +295,44 @@
   .scene-toolbar {
     position: absolute;
     left: 20px;
-    top: 64px;
+    top: 76px;
     z-index: 6;
-    display: flex;
-    align-items: center;
-    gap: 8px;
-    width: 353px;
-    height: 68px;
+    width: 357px;
+    height: 52px;
+  }
+
+  :deep(.scene-toolbar .ignition-select) {
+    width: 100%;
+    height: 52px;
+    padding: 0 48px 0 86px;
     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: 17px;
+    letter-spacing: 0.5px;
+
+    &:hover,
+    &.open {
+      border-color: transparent;
+      box-shadow: none;
+    }
   }
 
-  .scene-toolbar-label {
-    flex: none;
-    color: #d9f7ff;
-    font-size: 15px;
-    white-space: nowrap;
+  :deep(.scene-toolbar .ignition-select-arrow) {
+    right: 30px;
+    width: 12px;
+    height: 12px;
+    margin-top: -7px;
+    border: none;
+    border-right: 2px solid #eaffff;
+    border-bottom: 2px solid #eaffff;
+    transform: rotate(45deg);
   }
 
-  :deep(.scene-toolbar .ignition-select) {
-    flex: 1;
-    min-width: 0;
-    width: auto;
-    height: 32px;
+  :deep(.scene-toolbar .ignition-select.open .ignition-select-arrow) {
+    margin-top: -3px;
+    transform: rotate(225deg);
   }
 
   :deep(.box1-top .title) {

+ 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 });

+ 31 - 7
src/views/vent/home/selfIgnition/safetyEvaluation.api.ts

@@ -10,7 +10,7 @@ enum Api {
   update = '/safety/safetyEvaluation/updateSafetyEvaluation',
   remove = '/safety/safetyEvaluation/deleteSafetyEvaluation',
   getByScene = '/safety/safetyEvaluation/getByScene',
-  sceneList = '/safety/ventanalyManageSystem/list',
+  sceneList = '/monitor/system',
 }
 
 /** 22 项评价指标、权重和选项字典 */
@@ -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,22 @@ 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;
+};
 
-/** 采空区场景列表,用于评价对象绑定;不传参数时默认查采空区场景 */
-export const getSafetySceneList = (params?: Record<string, any>) =>
-  defHttp.get({ url: Api.sceneList, params: params ?? { strtype: 'sys_surface_caimei', pagetype: 'normal' } });
+/** 采空区场景列表,用于评价对象绑定 */
+export const getSafetySceneList = (params?: Record<string, any>) => defHttp.post({ url: Api.sceneList, params: params ?? {} });

Some files were not shown because too many files changed in this diff