Преглед изворни кода

[Wip 0000] 基础页面开发

houzekong пре 1 недеља
родитељ
комит
01d5065fe7
31 измењених фајлова са 3940 додато и 214 уклоњено
  1. 1 1
      .env
  2. 181 146
      public/js/global.js
  3. 4 0
      src/components/Configurable/content.vue
  4. 146 0
      src/components/Configurable/preset/IdentifyResult.vue
  5. 2 1
      src/components/Configurable/types.ts
  6. 2 2
      src/components/Table/src/BasicTable.vue
  7. 1 1
      src/enums/pageEnum.ts
  8. 3 0
      src/hooks/setting/useHeaderSetting.ts
  9. 80 11
      src/layouts/default/feature/SimpleMap.vue
  10. 64 38
      src/layouts/default/feature/components/LeafPopup.vue
  11. 32 0
      src/layouts/default/header/components/HeaderTime.vue
  12. 2 0
      src/layouts/default/header/components/index.ts
  13. 27 13
      src/layouts/default/header/index.vue
  14. 1 1
      src/locales/lang/zh-CN/routes/basic.ts
  15. 374 0
      src/views/analysis/warningAnalysis/windPointManage/index.vue
  16. 55 0
      src/views/analysis/warningAnalysis/windPointManage/windPointManage.api.ts
  17. 317 0
      src/views/analysis/warningAnalysis/windPointManage/windPointManage.data.ts
  18. 366 0
      src/views/analysis/warningAnalysis/windPointManage/windTopology/hooks/useTopology.ts
  19. 226 0
      src/views/analysis/warningAnalysis/windPointManage/windTopology/index.vue
  20. 38 0
      src/views/analysis/warningAnalysis/windPointManage/windTopology/windTopology.api.ts
  21. 287 0
      src/views/analysis/warningAnalysis/windPointManage/windTopology/windTopology.data.ts
  22. 378 0
      src/views/analysis/warningAnalysis/windSpeedOverlimit/index.vue
  23. 40 0
      src/views/analysis/warningAnalysis/windSpeedOverlimit/windSpeedOverlimit.api.ts
  24. 232 0
      src/views/analysis/warningAnalysis/windSpeedOverlimit/windSpeedOverlimit.data.ts
  25. 229 0
      src/views/dashboard/HiddenSurface/configurable.data.ts
  26. 102 0
      src/views/dashboard/HiddenSurface/hiddenSurface.api.ts
  27. 308 0
      src/views/dashboard/HiddenSurface/index.vue
  28. 21 0
      src/views/monitor/enforceMonitor/enforceMonitor.api.ts
  29. 172 0
      src/views/monitor/enforceMonitor/enforceMonitor.data.ts
  30. 29 0
      src/views/monitor/enforceMonitor/hooks/form.ts
  31. 220 0
      src/views/monitor/enforceMonitor/index.vue

+ 1 - 1
.env

@@ -1,5 +1,5 @@
 # port
-VITE_PORT = 3200
+VITE_PORT = 3100
 
 #  网站标题
 VITE_GLOB_APP_TITLE = 省局矿山隐蔽工作面智能判识监测系统

+ 181 - 146
public/js/global.js

@@ -6,18 +6,18 @@ const __STATIC_ROUTES__ = [
     component: 'PLAIN_LAYOUT',
     // component: 'LAYOUT',
     children: [
-      {
-        path: '/dashboard/sealedgoaf',
-        component: '/dashboard/SealedGoaf',
-        meta: {
-          keepAlive: true,
-          internalOrExternal: false,
-          icon: '',
-          componentName: 'SealedGoaf',
-          title: '老空区永久密闭首页',
-        },
-        name: 'dashboard-sealedgoaf',
-      },
+      // {
+      //   path: '/dashboard/sealedgoaf',
+      //   component: '/dashboard/SealedGoaf',
+      //   meta: {
+      //     keepAlive: true,
+      //     internalOrExternal: false,
+      //     icon: '',
+      //     componentName: 'SealedGoaf',
+      //     title: '老空区永久密闭首页',
+      //   },
+      //   name: 'dashboard-sealedgoaf',
+      // },
       // {
       //   path: '/dashboard/sealedgoaf',
       //   component: '/dashboard/SealedGoafMine',
@@ -30,7 +30,18 @@ const __STATIC_ROUTES__ = [
       //   },
       //   name: 'dashboard-sealedgoaf-mine',
       // },
-
+      {
+        path: '/dashboard/hiddenSurface',
+        component: '/dashboard/HiddenSurface',
+        meta: {
+          keepAlive: true,
+          internalOrExternal: false,
+          icon: '',
+          componentName: 'HiddenSurface',
+          title: '测风数据监测首页',
+        },
+        name: 'dashboard-hiddenSurface',
+      },
       // {
       //     path: '/dashboard/analysis',
       //     component: 'dashboard/Analysis',
@@ -145,30 +156,30 @@ const __STATIC_ROUTES__ = [
         },
         name: 'basicinfo-closed-statistics',
       },
-      {
-        path: '/basicinfo/data-quality',
-        component: 'dashboard/basicInfo/dataQuality',
-        meta: {
-          keepAlive: true,
-          internalOrExternal: false,
-          icon: '',
-          componentName: 'index',
-          title: '数据质量',
-        },
-        name: 'basicinfo-data-quality',
-      },
-      {
-        path: '/basicinfo/problem-report',
-        component: 'dashboard/basicInfo/problemReport',
-        meta: {
-          keepAlive: true,
-          internalOrExternal: false,
-          icon: '',
-          componentName: 'index',
-          title: '密闭墙问题反馈',
-        },
-        name: 'basicinfo-problem-report',
-      },
+      // {
+      //   path: '/basicinfo/data-quality',
+      //   component: 'dashboard/basicInfo/dataQuality',
+      //   meta: {
+      //     keepAlive: true,
+      //     internalOrExternal: false,
+      //     icon: '',
+      //     componentName: 'index',
+      //     title: '数据质量',
+      //   },
+      //   name: 'basicinfo-data-quality',
+      // },
+      // {
+      //   path: '/basicinfo/problem-report',
+      //   component: 'dashboard/basicInfo/problemReport',
+      //   meta: {
+      //     keepAlive: true,
+      //     internalOrExternal: false,
+      //     icon: '',
+      //     componentName: 'index',
+      //     title: '密闭墙问题反馈',
+      //   },
+      //   name: 'basicinfo-problem-report',
+      // },
     ],
     meta: {
       keepAlive: false,
@@ -181,12 +192,12 @@ const __STATIC_ROUTES__ = [
   },
   {
     redirect: undefined,
-    path: '/sealed',
-    component: '/monitor/sealedMonitor/index',
+    path: '/enforce',
+    component: '/monitor/enforceMonitor/index',
     children: [
       {
-        path: '/sealed/${id}',
-        component: '/monitor/sealedMonitor/index',
+        path: '/enforce/${id}',
+        component: '/monitor/enforceMonitor/index',
         meta: {
           keepAlive: true,
           internalOrExternal: false,
@@ -197,7 +208,7 @@ const __STATIC_ROUTES__ = [
           generation: false,
           // orderNo: '${departOrder}',
         },
-        name: 'sealed-${id}',
+        name: 'enforce-${id}',
       }
     ],
     meta: {
@@ -215,125 +226,149 @@ const __STATIC_ROUTES__ = [
     path: '/warning-analysis',
     component: 'LAYOUT',
     children: [
+      // {
+      //   path: '/warning-analysis/air-leak-status',
+      //   component: '/analysis/warningAnalysis/airLeakStatus/index',
+      //   meta: {
+      //     keepAlive: true,
+      //     internalOrExternal: false,
+      //     icon: '',
+      //     componentName: 'index',
+      //     title: '密闭漏风状态判定',
+      //   },
+      //   name: 'airLeakStatus',
+      // },
+      // {
+      //   path: '/warning-analysis/pressure-diff-analysis',
+      //   component: '/analysis/warningAnalysis/pressureDiffAnalysis/index',
+      //   meta: {
+      //     keepAlive: true,
+      //     internalOrExternal: false,
+      //     icon: '',
+      //     componentName: 'index',
+      //     title: '内外压差变化风险分析',
+      //   },
+      //   name: 'pressureDiffAnalysis',
+      // },
+      // {
+      //   path: '/warningAnalysis/autoFire-analysis',
+      //   component: '/analysis/warningAnalysis/autoFireAnalysis/index',
+      //   meta: {
+      //     keepAlive: true,
+      //     internalOrExternal: false,
+      //     icon: '',
+      //     componentName: 'index',
+      //     title: '闭内自燃发火隐患分级',
+      //   },
+      //   name: 'warningAnalysis-autoFire-analysis',
+      // },
+      // {
+      //   path: '/warningAnalysis/autoFireOut-analysis',
+      //   component: '/analysis/warningAnalysis/autoFireOutAnalysis/index',
+      //   meta: {
+      //     keepAlive: true,
+      //     internalOrExternal: false,
+      //     icon: '',
+      //     componentName: 'index',
+      //     title: '闭外自燃发火隐患分级',
+      //   },
+      //   name: 'warningAnalysis-autoFireOut-analysis',
+      // },
+      // {
+      //   path: '/warningAnalysis/fireArea-judge',
+      //   component: '/analysis/warningAnalysis/fireAreaJudgeAnalysis/index',
+      //   meta: {
+      //     keepAlive: true,
+      //     internalOrExternal: false,
+      //     icon: '',
+      //     componentName: 'index',
+      //     title: '火区密闭启封判定',
+      //   },
+      //   name: 'warningAnalysis-fireArea-judge',
+      // },
+      // {
+      //   path: '/warningAnalysis/sealRisk-judge',
+      //   component: '/analysis/warningAnalysis/sealRiskJudgeAnalysis/index',
+      //   meta: {
+      //     keepAlive: true,
+      //     internalOrExternal: false,
+      //     icon: '',
+      //     componentName: 'index',
+      //     title: '密闭爆炸危险性判定',
+      //   },
+      //   name: 'warningAnalysis-sealRisk-judge',
+      // },
+      // {
+      //   path: '/warningAnalysis/overlimit-alarm',
+      //   component: '/analysis/warningAnalysis/overlimitAlarm/index',
+      //   meta: {
+      //     keepAlive: true,
+      //     internalOrExternal: false,
+      //     icon: '',
+      //     componentName: 'index',
+      //     title: '闭内超限报警',
+      //   },
+      //   name: 'warningAnalysis-overlimit-alarm',
+      // },
+      // {
+      //   path: '/warningAnalysis/overlimit-alarm-out',
+      //   component: '/analysis/warningAnalysis/overlimitAlarmOut/index',
+      //   meta: {
+      //     keepAlive: true,
+      //     internalOrExternal: false,
+      //     icon: '',
+      //     componentName: 'index',
+      //     title: '闭外超限报警',
+      //   },
+      //   name: 'warningAnalysis-overlimit-alarm-out',
+      // },
+      // {
+      //   path: '/warningAnalysis/report-analysis',
+      //   component: '/analysis/warningAnalysis/reportAnalysis/index',
+      //   meta: {
+      //     keepAlive: true,
+      //     internalOrExternal: false,
+      //     icon: '',
+      //     componentName: 'index',
+      //     title: '数据中断分析',
+      //   },
+      //   name: 'warningAnalysis-report-analysis',
+      // },
+      // {
+      //   path: '/warningAnalysis/connectAnalysis',
+      //   component: '/analysis/warningAnalysis/connectAnalysis/index',
+      //   meta: {
+      //     keepAlive: true,
+      //     internalOrExternal: false,
+      //     icon: '',
+      //     componentName: 'index',
+      //     title: '关联分析',
+      //   },
+      //   name: 'warningAnalysis-connect',
+      // },
       {
-        path: '/warning-analysis/air-leak-status',
-        component: '/analysis/warningAnalysis/airLeakStatus/index',
-        meta: {
-          keepAlive: true,
-          internalOrExternal: false,
-          icon: '',
-          componentName: 'index',
-          title: '密闭漏风状态判定',
-        },
-        name: 'airLeakStatus',
-      },
-      {
-        path: '/warning-analysis/pressure-diff-analysis',
-        component: '/analysis/warningAnalysis/pressureDiffAnalysis/index',
-        meta: {
-          keepAlive: true,
-          internalOrExternal: false,
-          icon: '',
-          componentName: 'index',
-          title: '内外压差变化风险分析',
-        },
-        name: 'pressureDiffAnalysis',
-      },
-      {
-        path: '/warningAnalysis/autoFire-analysis',
-        component: '/analysis/warningAnalysis/autoFireAnalysis/index',
-        meta: {
-          keepAlive: true,
-          internalOrExternal: false,
-          icon: '',
-          componentName: 'index',
-          title: '闭内自燃发火隐患分级',
-        },
-        name: 'warningAnalysis-autoFire-analysis',
-      },
-      {
-        path: '/warningAnalysis/autoFireOut-analysis',
-        component: '/analysis/warningAnalysis/autoFireOutAnalysis/index',
-        meta: {
-          keepAlive: true,
-          internalOrExternal: false,
-          icon: '',
-          componentName: 'index',
-          title: '闭外自燃发火隐患分级',
-        },
-        name: 'warningAnalysis-autoFireOut-analysis',
-      },
-      {
-        path: '/warningAnalysis/fireArea-judge',
-        component: '/analysis/warningAnalysis/fireAreaJudgeAnalysis/index',
-        meta: {
-          keepAlive: true,
-          internalOrExternal: false,
-          icon: '',
-          componentName: 'index',
-          title: '火区密闭启封判定',
-        },
-        name: 'warningAnalysis-fireArea-judge',
-      },
-      {
-        path: '/warningAnalysis/sealRisk-judge',
-        component: '/analysis/warningAnalysis/sealRiskJudgeAnalysis/index',
-        meta: {
-          keepAlive: true,
-          internalOrExternal: false,
-          icon: '',
-          componentName: 'index',
-          title: '密闭爆炸危险性判定',
-        },
-        name: 'warningAnalysis-sealRisk-judge',
-      },
-      {
-        path: '/warningAnalysis/overlimit-alarm',
-        component: '/analysis/warningAnalysis/overlimitAlarm/index',
-        meta: {
-          keepAlive: true,
-          internalOrExternal: false,
-          icon: '',
-          componentName: 'index',
-          title: '闭内超限报警',
-        },
-        name: 'warningAnalysis-overlimit-alarm',
-      },
-      {
-        path: '/warningAnalysis/overlimit-alarm-out',
-        component: '/analysis/warningAnalysis/overlimitAlarmOut/index',
-        meta: {
-          keepAlive: true,
-          internalOrExternal: false,
-          icon: '',
-          componentName: 'index',
-          title: '闭外超限报警',
-        },
-        name: 'warningAnalysis-overlimit-alarm-out',
-      },
-      {
-        path: '/warningAnalysis/report-analysis',
-        component: '/analysis/warningAnalysis/reportAnalysis/index',
+        path: '/warningAnalysis/wind-speed-overlimit',
+        component: '/analysis/warningAnalysis/windSpeedOverlimit/index',
         meta: {
           keepAlive: true,
           internalOrExternal: false,
           icon: '',
           componentName: 'index',
-          title: '数据中断分析',
+          title: '风速风量超限判识',
         },
-        name: 'warningAnalysis-report-analysis',
+        name: 'warningAnalysis-wind-speed-overlimit',
       },
       {
-        path: '/warningAnalysis/connectAnalysis',
-        component: '/analysis/warningAnalysis/connectAnalysis/index',
+        path: '/warningAnalysis/wind-point-manage',
+        component: '/analysis/warningAnalysis/windPointManage/index',
         meta: {
           keepAlive: true,
           internalOrExternal: false,
           icon: '',
           componentName: 'index',
-          title: '关联分析',
+          title: '测风点位与模型管理',
         },
-        name: 'warningAnalysis-connect',
+        name: 'warningAnalysis-wind-point-manage',
       },
     ],
     meta: {

+ 4 - 0
src/components/Configurable/content.vue

@@ -78,6 +78,9 @@
         <template v-if="config.name === 'simple_board'">
           <SimpleBoard v-bind="config.config" :data="config.data" />
         </template>
+        <template v-if="config.name === 'identify_result'">
+          <IdentifyResult class="content__module" :config="config.config" :data="config.data" />
+        </template>
         <!-- <template v-if="config.key === 'fire_control'">
         <FIreControl class="content__module" />
       </template>
@@ -114,6 +117,7 @@
   const QHCurve = defineAsyncComponent(() => import('./preset/QHCurve.vue'));
   const Partition = defineAsyncComponent(() => import('./preset/partition.vue'));
   const BoardTable = defineAsyncComponent(() => import('./preset/BoardTable.vue'));
+  const IdentifyResult = defineAsyncComponent(() => import('./preset/IdentifyResult.vue'));
 
   const props = defineProps<{
     data: any;

+ 146 - 0
src/components/Configurable/preset/IdentifyResult.vue

@@ -0,0 +1,146 @@
+<template>
+  <div class="identify-result-preset">
+    <!-- 判识结论区 -->
+    <div class="conclusion-section flex items-center py-1">
+      <div class="section-title">判识结论:</div>
+      <div class="conclusion-value font-bold" :class="conclusionClass">{{ conclusionText }}</div>
+    </div>
+
+    <!-- 判定依据列表 -->
+    <div v-if="basisItems.length > 0" class="basis-section">
+      <div class="section-title">判定依据</div>
+      <div class="basis-list flex flex-col gap-1">
+        <div v-for="(item, idx) in basisItems" :key="'basis-' + idx" class="basis-item flex justify-between items-center px-2.5 py-1.5">
+          <span class="basis-label">{{ item.label }}</span>
+          <span class="basis-value font-semibold" :class="item.colorClass">{{ item.value }}</span>
+        </div>
+      </div>
+    </div>
+
+    <!-- 建议措施 -->
+    <div v-if="suggestionItems.length > 0" class="suggestion-section">
+      <div class="section-title">建议措施</div>
+      <div class="suggestion-list flex flex-col gap-1">
+        <div v-for="(item, idx) in suggestionItems" :key="'sug-' + idx" class="suggestion-item flex items-start gap-1.5 px-2 py-1">
+          <span class="suggestion-dot inline-block rounded-full flex-shrink-0"></span>
+          <span class="suggestion-text">{{ item }}</span>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { computed } from 'vue';
+
+  const props = withDefaults(
+    defineProps<{
+      config?: any;
+      data?: any;
+    }>(),
+    {
+      config: () => ({}),
+      data: () => ({}),
+    }
+  );
+
+  const conclusionText = computed(() => {
+    const raw = props.data?.conclusion ?? props.config?.conclusion ?? '-';
+    return raw;
+  });
+
+  const conclusionClass = computed(() => {
+    const val = String(conclusionText.value);
+    if (val === '疑似' || val === 'suspected') return 'text-red-500';
+    if (val === '正常' || val === 'normal') return 'text-green-500';
+    return 'text-yellow-500';
+  });
+
+  const basisItems = computed(() => {
+    const items = props.config?.items || [];
+    return items.map((item: any) => {
+      const rawValue = props.data?.[item.prop] ?? item.value ?? '-';
+      return {
+        label: item.label,
+        value: rawValue + (item.unit || ''),
+        colorClass: '',
+      };
+    });
+  });
+
+  const suggestionItems = computed(() => {
+    const raw = props.data?.suggestions ?? props.config?.suggestions ?? [];
+    if (Array.isArray(raw)) return raw;
+    return [];
+  });
+</script>
+
+<style lang="less" scoped>
+  .identify-result-preset {
+    width: 100%;
+    height: 100%;
+    box-sizing: border-box;
+    // border: 2px dashed #0070ff;
+    // border-radius: 10px;
+    // padding: 12px;
+    display: flex;
+    flex-direction: column;
+    gap: 10px;
+    overflow-y: auto;
+    // background: #fff;
+
+    .conclusion-section {
+      background: #eff6ff;
+      border-radius: 8px;
+
+      .conclusion-label {
+        font-size: 14px;
+        font-weight: 600;
+        color: @text-color-base;
+        white-space: nowrap;
+      }
+      .conclusion-value {
+        font-size: 20px;
+        // letter-spacing: 4px;
+      }
+    }
+
+    .section-title {
+      font-size: 13px;
+      font-weight: 600;
+      color: @text-color-base;
+      // margin-bottom: 6px;
+      padding-left: 4px;
+    }
+
+    .basis-item {
+      background: #f9fafb;
+      border-radius: 6px;
+
+      .basis-label {
+        font-size: 13px;
+        color: #6b7280;
+      }
+      .basis-value {
+        font-size: 13px;
+        color: #d97706;
+      }
+    }
+
+    .suggestion-item {
+      line-height: 1.5;
+
+      .suggestion-dot {
+        width: 6px;
+        height: 6px;
+        margin-top: 6px;
+        background: #60a5fa;
+      }
+      .suggestion-text {
+        font-size: 12px;
+        color: #4b5563;
+        flex: 1;
+      }
+    }
+  }
+</style>

+ 2 - 1
src/components/Configurable/types.ts

@@ -91,7 +91,8 @@ export interface ModuleData {
         | 'qh_curve'
         | 'partition'
         | 'simple_board'
-        | 'board_table';
+        | 'board_table'
+        | 'identify_result';
       /** 分区大小 */
       basis: string;
       overflow?: boolean;

+ 2 - 2
src/components/Table/src/BasicTable.vue

@@ -77,7 +77,7 @@
   // import expandIcon from './components/ExpandIcon';
   import HeaderCell from './components/HeaderCell.vue';
   import TableSummary from './components/TableSummary';
-  import TableTitle from './components/TableTitle.vue';
+  // import TableTitle from './components/TableTitle.vue';
   import { InnerHandlers } from './types/table';
   import { usePagination } from './hooks/usePagination';
   import { useColumns } from './hooks/useColumns';
@@ -107,7 +107,7 @@
       HeaderCell,
       TableSummary,
       CustomSelectHeader,
-      TableTitle,
+      // TableTitle,
     },
     props: basicProps,
     emits: [

+ 1 - 1
src/enums/pageEnum.ts

@@ -2,7 +2,7 @@ export enum PageEnum {
   // basic login path
   BASE_LOGIN = '/login',
   // basic home path
-  BASE_HOME = '/dashboard/sealedGoaf',
+  BASE_HOME = '/dashboard/hiddenSurface',
   // secondary home path
   SECONDARY_HOME = '/basicinfo/overhaul',
   // tertiary home path

+ 3 - 0
src/hooks/setting/useHeaderSetting.ts

@@ -47,6 +47,8 @@ export function useHeaderSetting() {
 
   const getShowNotice = computed(() => appStore.getHeaderSetting.showNotice);
 
+  const getShowHeaderTime = computed(() => appStore.getHeaderSetting.showHeaderTime);
+
   const getShowBread = computed(() => {
     return unref(getMenuMode) !== MenuModeEnum.HORIZONTAL && unref(getShowBreadCrumb) && !unref(getSplit);
   });
@@ -75,6 +77,7 @@ export function useHeaderSetting() {
     getUseLockPage,
     getShowFullScreen,
     getShowNotice,
+    getShowHeaderTime,
     getShowBread,
     getShowContent,
     getShowHeaderLogo,

+ 80 - 11
src/layouts/default/feature/SimpleMap.vue

@@ -12,6 +12,22 @@
       @change="toggleCADMap(true)"
     ></a-select>
   </div>
+  <!-- 省域地图监测区图例(图3-1) -->
+  <div v-if="!cadOpened" class="map-legend">
+    <div class="legend-title">图例</div>
+    <div class="legend-row"><span class="legend-dot legend-dot-red"></span>疑似隐蔽工作面</div>
+    <div class="legend-row"><span class="legend-dot legend-dot-orange"></span>风量异常</div>
+    <div class="legend-row"><span class="legend-dot legend-dot-yellow"></span>数据掉线</div>
+    <div class="legend-row"><span class="legend-dot legend-dot-green"></span>监测正常</div>
+  </div>
+  <!-- 矿端通风系统图监测区图例(图3-3) -->
+  <div v-if="cadOpened" class="map-legend">
+    <div class="legend-title">图例</div>
+    <div class="legend-row"><span class="legend-dot legend-dot-green"></span>测风正常</div>
+    <div class="legend-row"><span class="legend-dot legend-dot-orange"></span>风量异常</div>
+    <div class="legend-row"><span class="legend-dot legend-dot-red-dashed"></span>缺失测风点</div>
+    <div class="legend-row"><span class="legend-line-red"></span>疑似隐蔽巷道</div>
+  </div>
 </template>
 
 <script lang="ts" setup>
@@ -54,7 +70,7 @@
   // 标记单位集合,用来为后续计算历史记录提供支持
   let markerCollection = [];
   // CAD是否可见
-  let cadOpened: boolean;
+  const cadOpened = ref<boolean>();
 
   const GEO_LAYER_ID = 'geojson-layer';
   const GEO_BORDER_ID = 'geoline-layer';
@@ -242,9 +258,9 @@
   /** 切换CAD底图和瓦片地图底图的显示,通过重新初始化进行切换,避免出现动画异常和多个DOM节点 */
   async function toggleCADMap(visible: boolean) {
     // CAD底图不显示的时候无需再次初始化地图,显示时需要根据data来切换不同图纸
-    if (cadOpened === visible && visible === false) return;
+    if (cadOpened.value === visible && visible === false) return;
 
-    cadOpened = visible;
+    cadOpened.value = visible;
     mapLoading.value = true;
     map?.destory();
     map = null;
@@ -262,18 +278,13 @@
         markers = mks;
       }
 
-      if (cadOpened) {
+      if (cadOpened.value) {
         const [__, res] = await Promise.all([
           initMap2d(mapContainer.value!, { wjId: fileId.value, style: { backcolor: 0xe6f3ff } }),
-          props.silence
-            ? Promise.resolve([])
-            : getGoafList({
-                mineCode: mineStore.getDepartId,
-                deptId: mineStore.getDepartId,
-              }),
+          props.silence ? Promise.resolve([]) : getGoafList({ deptId: mineStore.getDepartId }),
         ]);
 
-        renderGoafMarkers(res); // 渲染标记
+        renderGoafMarkers(res); // 渲染密闭标记
       } else {
         // const hide = message.loading('地图加载中...', 0);
         const [m, res] = await Promise.all([initMap(mapContainer.value!), getShanxiGeoJSON()]);
@@ -408,4 +419,62 @@
     // padding: 10px 15px;
     margin: 10px 0;
   }
+
+  /* 图例浮层 */
+  .map-legend {
+    position: fixed;
+    bottom: 24px;
+    left: 24px;
+    z-index: @simple-map-z-index + 1;
+    background: rgba(255, 255, 255, 0.92);
+    border-radius: 8px;
+    padding: 10px 14px;
+    font-size: 12px;
+    line-height: 1.8;
+    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+    color: #333;
+    min-width: 140px;
+    pointer-events: none;
+
+    .legend-title {
+      font-weight: 600;
+      margin-bottom: 4px;
+      font-size: 13px;
+    }
+    .legend-row {
+      display: flex;
+      align-items: center;
+      gap: 6px;
+    }
+    .legend-dot {
+      display: inline-block;
+      width: 10px;
+      height: 10px;
+      border-radius: 50%;
+      flex-shrink: 0;
+    }
+    .legend-dot-green {
+      background: #52c41a;
+    }
+    .legend-dot-orange {
+      background: #fa8c16;
+    }
+    .legend-dot-yellow {
+      background: #fadb14;
+    }
+    .legend-dot-red {
+      background: #f5222d;
+    }
+    .legend-dot-red-dashed {
+      background: transparent;
+      border: 2px dashed #f5222d;
+    }
+    .legend-line-red {
+      display: inline-block;
+      width: 14px;
+      height: 3px;
+      background: #f5222d;
+      flex-shrink: 0;
+    }
+  }
 </style>

+ 64 - 38
src/layouts/default/feature/components/LeafPopup.vue

@@ -7,15 +7,32 @@
       <div>{{ node.parentArea }}</div>
     </div>
     <div class="vjmapgis-popup-content__board mb-1">
-      <div class="mr-5px">瓦斯等级</div>
-      <div>{{ node.gasLevelName }}</div>
+      <div class="mr-5px">测风点数</div>
+      <div>11</div>
+    </div>
+    <div class="vjmapgis-popup-content__board mb-1">
+      <div class="mr-5px">在线测风点</div>
+      <div>22</div>
+    </div>
+    <div class="vjmapgis-popup-content__board mb-1">
+      <div class="mr-5px">风量异常点</div>
+      <div>11</div>
     </div>
     <div class="vjmapgis-popup-content__board">
-      <div class="mr-5px">生产状态</div>
-      <div>{{ node.gjMineStatus }}</div>
+      <div class="mr-5px">判识结果</div>
+      <div>正常</div>
     </div>
     <div class="vjmapgis-popup-content__divider"></div>
-    <div class="vjmapgis-popup-content__table_container">
+    <!-- 主要风险(判定依据) -->
+    <div class="risk-section" v-if="riskItems.length > 0">
+      <div class="risk-list">
+        <div class="risk-item" v-for="(item, idx) in riskItems" :key="'risk-' + idx">
+          <span class="risk-label">{{ item.label }}</span>
+          <span class="risk-value">{{ item.value }}</span>
+        </div>
+      </div>
+    </div>
+    <!-- <div class="vjmapgis-popup-content__table_container">
       <table class="vjmapgis-popup-content__table w-full mt-1">
         <thead>
           <tr>
@@ -69,13 +86,11 @@
         </tbody>
       </table>
       <div v-if="!goafDataList.length" class="vjmapgis-popup-content__table_empty">暂无数据</div>
-    </div>
+    </div> -->
   </div>
 </template>
 <script lang="ts" setup>
-  import { get } from 'lodash-es';
-  import { computed, ref } from 'vue';
-  import { StatusColorEnum } from '/@/enums/jeecgEnum';
+  import { computed } from 'vue';
 
   const props = defineProps<{
     node: any;
@@ -85,36 +100,21 @@
   function clickHandler() {
     props.callback(props.node);
   }
-  // const linkStatusText = ref(['断线', '正常', '标校']);
-  const linkStatusText = ref({
-    '-1': '断线',
-    '0': '在线',
-  });
-  const alarmLevelText = ref({
-    '0': '暂无信息',
-    '1': '低风险',
-    '2': '一般风险',
-    '3': '较高风险',
-    '4': '高风险',
-  });
-  // const alarmLevelColor = ref([StatusColorEnum.red, StatusColorEnum.blue, StatusColorEnum.gold, StatusColorEnum.purple, StatusColorEnum.red]);
-  const linkStatusColor = ref({
-    '-1': StatusColorEnum.red,
-    '0': StatusColorEnum.green,
-  });
-  const alarmLevelColor = ref({
-    '0': StatusColorEnum.red,
-    '1': StatusColorEnum.blue,
-    '2': StatusColorEnum.gold,
-    '3': StatusColorEnum.purple,
-    '4': StatusColorEnum.red,
-  });
 
-  const coalSeamList = computed(() => {
-    return JSON.parse(props.node.coalSeamList);
-  });
-  const goafDataList = computed(() => {
-    return JSON.parse(props.node.goafDataList);
+  // 主要风险字段(与 IdentifyResult 一致)
+  const riskFields = [
+    { label: '风量较均值偏低', prop: 'volumeLowPercent', unit: '%' },
+    { label: '进回风不闭合差值', prop: 'unclosedDiff', unit: 'm³/min' },
+    { label: '缺失规定测风点数', prop: 'missingCount', unit: '个' },
+    { label: '多测点关联异常数', prop: 'relatedAbnormal', unit: '个' },
+  ];
+  const riskItems = computed(() => {
+    return riskFields
+      .map((f) => {
+        const val = props.node[f.prop];
+        return { label: f.label, value: (val || '- ') + (f.unit || '') };
+      })
+      .filter(Boolean);
   });
 </script>
 <style lang="less">
@@ -183,4 +183,30 @@
       padding: 10px;
     }
   }
+
+  .risk-section {
+    margin: 6px 0;
+
+    .risk-list {
+      display: flex;
+      flex-direction: column;
+      gap: 4px;
+    }
+    .risk-item {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: 6px 10px;
+      background: @map-popup-board-bg;
+      border-radius: 5px;
+      font-size: 13px;
+    }
+    .risk-label {
+      color: @text-color-base;
+    }
+    .risk-value {
+      font-weight: 600;
+      color: @text-color-base;
+    }
+  }
 </style>

+ 32 - 0
src/layouts/default/header/components/HeaderTime.vue

@@ -0,0 +1,32 @@
+<template>
+  <span :class="prefixCls">{{ timeStr }}</span>
+</template>
+<script lang="ts">
+  import { defineComponent, computed } from 'vue';
+  import { useNow } from '@vueuse/core';
+  import { useDesign } from '/@/hooks/web/useDesign';
+  import { dateUtil } from '/@/utils/dateUtil';
+
+  export default defineComponent({
+    name: 'HeaderTime',
+    setup() {
+      const { prefixCls } = useDesign('header-time');
+      const now = useNow({ interval: 1000 });
+
+      const timeStr = computed(() => dateUtil(now.value).format('YYYY-MM-DD HH:mm:ss'));
+
+      return {
+        prefixCls,
+        timeStr,
+      };
+    },
+  });
+</script>
+<style lang="less" scoped>
+  @prefix-cls: ~'@{namespace}-header-time';
+
+  .@{prefix-cls} {
+    color: @white;
+    font-size: 12px;
+  }
+</style>

+ 2 - 0
src/layouts/default/header/components/index.ts

@@ -9,6 +9,8 @@ export const LayoutBreadcrumb = createAsyncComponent(() => import('./Breadcrumb.
 
 export const Notify = createAsyncComponent(() => import('./notify/index.vue'));
 
+export const HeaderTime = createAsyncComponent(() => import('./HeaderTime.vue'));
+
 export const ErrorAction = createAsyncComponent(() => import('./ErrorAction.vue'));
 
 export const LockScreen = createAsyncComponent(() => import('./LockScreen.vue'));

+ 27 - 13
src/layouts/default/header/index.vue

@@ -7,6 +7,11 @@
     </div>
     <!-- left end -->
 
+    <!-- menu start -->
+    <!-- <div :class="`${prefixCls}-menu`" v-if="getShowTopMenu && !getIsMobile">
+      <LayoutMenu :isHorizontal="true" :theme="getHeaderTheme" :splitType="getSplitType" :menuMode="getMenuMode" />
+    </div> -->
+    <!-- menu-end -->
     <!-- menu start -->
     <div :class="`${prefixCls}-menu`">
       <MineCascader v-model:value="getDepartId" :changeOnSelect="true" :initFromStore="true" @change="setDepartById">
@@ -31,6 +36,8 @@
 
       <UserDropDown :theme="getHeaderTheme" />
 
+      <HeaderTime v-if="getShowHeaderTime" :class="`${prefixCls}-action__item`" />
+
       <SettingDrawer v-if="getShowSetting" :class="`${prefixCls}-action__item`" />
     </div>
   </LHeader>
@@ -52,12 +59,12 @@
   import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
   import { useRootSetting } from '/@/hooks/setting/useRootSetting';
 
-  // import { MenuModeEnum, MenuSplitTyeEnum } from '/@/enums/menuEnum';
+  import { MenuModeEnum, MenuSplitTyeEnum } from '/@/enums/menuEnum';
   import { SettingButtonPositionEnum } from '/@/enums/appEnum';
   import { AppLocalePicker } from '/@/components/Application';
 
-  import { UserDropDown, FullScreen, Notify, ErrorAction, LockScreen } from './components';
-  // import { useAppInject } from '/@/hooks/web/useAppInject';
+  import { UserDropDown, FullScreen, Notify, ErrorAction, LockScreen, HeaderTime } from './components';
+  import { useAppInject } from '/@/hooks/web/useAppInject';
   import { useDesign } from '/@/hooks/web/useDesign';
 
   import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
@@ -78,6 +85,7 @@
     components: {
       LHeader: Layout.Header,
       AppLogo,
+      // LayoutMenu,
       UserDropDown,
       AppLocalePicker,
       FullScreen,
@@ -85,6 +93,7 @@
       AppSearch,
       ErrorAction,
       LockScreen,
+      HeaderTime,
       LoginSelect,
       MineCascader,
       SettingDrawer: createAsyncComponent(() => import('/@/layouts/default/setting/index.vue'), {
@@ -97,16 +106,16 @@
     setup(props) {
       const { prefixCls } = useDesign('layout-header');
       const userStore = useUserStore();
-      const { getSplit, getIsMixMode, getMenuWidth } = useMenuSetting();
+      const { getSplit, getIsMixMode, getMenuWidth, getShowTopMenu } = useMenuSetting();
       const { getUseErrorHandle, getShowSettingButton, getSettingButtonPosition } = useRootSetting();
       const { title } = useGlobSetting();
 
-      const { getHeaderTheme, getShowFullScreen, getShowNotice, getShowHeaderLogo, getShowHeader, getShowSearch, getUseLockPage } =
+      const { getHeaderTheme, getShowFullScreen, getShowNotice, getShowHeaderLogo, getShowHeader, getShowSearch, getUseLockPage, getShowHeaderTime } =
         useHeaderSetting();
 
       const { getShowLocalePicker } = useLocale();
 
-      // const { getIsMobile } = useAppInject();
+      const { getIsMobile } = useAppInject();
 
       const getHeaderClass = computed(() => {
         const theme = unref(getHeaderTheme);
@@ -139,13 +148,13 @@
         return { width: `${width}px` };
       });
 
-      // const getSplitType = computed(() => {
-      //   return unref(getSplit) ? MenuSplitTyeEnum.TOP : MenuSplitTyeEnum.NONE;
-      // });
+      const getSplitType = computed(() => {
+        return unref(getSplit) ? MenuSplitTyeEnum.TOP : MenuSplitTyeEnum.NONE;
+      });
 
-      // const getMenuMode = computed(() => {
-      //   return unref(getSplit) ? MenuModeEnum.HORIZONTAL : null;
-      // });
+      const getMenuMode = computed(() => {
+        return unref(getSplit) ? MenuModeEnum.HORIZONTAL : null;
+      });
 
       /**
        * 首页多租户部门弹窗逻辑
@@ -175,8 +184,12 @@
         prefixCls,
         getHeaderClass,
         getShowHeaderLogo,
+        getShowTopMenu,
         getHeaderTheme,
         getSplit,
+        getIsMobile,
+        getSplitType,
+        getMenuMode,
         getShowLocalePicker,
         getShowFullScreen,
         getShowNotice,
@@ -186,6 +199,7 @@
         getShowSetting,
         getShowSearch,
         getUseLockPage,
+        getShowHeaderTime,
         loginSelectOk,
         loginSelectRef,
         title,
@@ -205,7 +219,7 @@
     display: flex;
     padding: 0 8px;
     // 代码逻辑说明: 【QQYUN-8762】顶栏高度
-    height: @header-height;
+    // height: @header-height;
     align-items: center;
     position: relative;
     z-index: @layout-basic-z-index;

+ 1 - 1
src/locales/lang/zh-CN/routes/basic.ts

@@ -2,6 +2,6 @@ export default {
   login: '登录',
   errorLogList: '错误日志列表',
   defaultHomePage: '默认首页',
-  baseHome: '隐蔽工作面智能判识一张图',
+  baseHome: '隐蔽工作面一张图',
   secondaryHome: '工作面判识监测与预警',
 };

+ 374 - 0
src/views/analysis/warningAnalysis/windPointManage/index.vue

@@ -0,0 +1,374 @@
+<!-- eslint-disable vue/multi-word-component-names -->
+<template>
+  <div class="wind-point-manage">
+    <Tabs v-model:activeKey="activeTab" type="line" class="common-page-tabs">
+      <!-- 测风点位管理 -->
+      <TabPane tab="测风点位管理" key="pointManage">
+        <a-row :gutter="12" class="tab-content">
+          <a-col :span="14">
+            <BasicTable @register="registerPointTable" :scroll="{ x: 'max-content', y: 420 }" :style="{ padding: 0 }">
+              <template #action="{ record }">
+                <button @click="handleEdit(record)" class="action-btn" title="编辑">
+                  <SvgIcon name="edit" />
+                </button>
+                <a-popconfirm title="确认删除?" @confirm="handleDelete(record)">
+                  <button class="action-btn" title="删除">
+                    <SvgIcon name="delete" />
+                  </button>
+                </a-popconfirm>
+              </template>
+              <template #resetBefore>
+                <a-button class="ml-2" preIcon="ant-design:plus-outlined" @click="openAddModal"> 新增 </a-button>
+                <a-button class="ml-2" preIcon="ant-design:import-outlined"> 导入 </a-button>
+              </template>
+            </BasicTable>
+          </a-col>
+          <a-col :span="10">
+            <BasicTable @register="registerComplianceTable" :scroll="{ x: 'max-content', y: 420 }" :pagination="false" :style="{ padding: 0 }" />
+          </a-col>
+        </a-row>
+      </TabPane>
+
+      <!-- 判识模型管理 -->
+      <TabPane tab="判识模型管理" key="modelConfig">
+        <BasicTable @register="registerModelConfigTable" :scroll="{ x: 'max-content' }" :style="{ padding: 0 }">
+          <template #action="{ record }">
+            <button @click="handleEditModel(record)" class="action-btn" title="编辑">
+              <SvgIcon name="edit" />
+            </button>
+          </template>
+          <template #resetBefore>
+            <a-button class="ml-2" type="primary" preIcon="ant-design:save-outlined" @click="handleSaveModelConfig"> 保存配置 </a-button>
+            <a-button class="ml-2" preIcon="ant-design:reload-outlined" @click="handleResetConfig"> 恢复默认 </a-button>
+          </template>
+        </BasicTable>
+        <div class="config-note">
+          <a-alert type="info" message="修改参数后,系统将重新计算判识指标。请确保参数值在合理范围内。" show-icon :closable="false" />
+        </div>
+      </TabPane>
+
+      <!-- 测风网络拓扑 -->
+      <TabPane tab="测风网络拓扑" key="windTopology">
+        <WindTopology />
+      </TabPane>
+    </Tabs>
+
+    <!-- 新增/编辑点位弹窗 -->
+    <BasicModal @register="registerPointModal" :width="800" :minHeight="200" centered :title="modalTitle" @ok="handleSubmit" destroyOnClose>
+      <BasicForm @register="registerForm" />
+    </BasicModal>
+
+    <!-- 判识模型参数编辑弹窗 -->
+    <BasicModal @register="registerModelModal" :width="600" :minHeight="200" centered :title="modelModalTitle" @ok="handleSubmitModel" destroyOnClose>
+      <BasicForm @register="registerModelForm" />
+    </BasicModal>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref, computed } from 'vue';
+  import { BasicTable } from '/@/components/Table';
+  import { BasicForm, useForm } from '/@/components/Form';
+  import { Tabs, TabPane, message } from 'ant-design-vue';
+  import { SvgIcon } from '/@/components/Icon';
+  import { BasicModal } from '/@/components/Modal/index';
+  import { useModal } from '/@/components/Modal';
+  import { useListPage } from '/@/hooks/system/useListPage';
+  import { pointColumns, complianceColumns, modelConfigColumns, defaultModelConfigs, formSchema, modelConfigFormSchema } from './windPointManage.data';
+  import { getPointList, savePoint, deletePoint, getComplianceCheck, getModelConfigs, saveModelConfigs } from './windPointManage.api';
+  import WindTopology from './windTopology/index.vue';
+
+  // 激活的Tab页签
+  const activeTab = ref('pointManage');
+
+  // ====== 注册测风点位表格 ======
+  const { tableContext: pointCtx } = useListPage({
+    tableProps: {
+      title: '注册测风点位',
+      formConfig: {
+        autoSearch: true,
+        labelWidth: 120,
+        schemas: [
+          {
+            label: '煤矿名称',
+            field: 'deptId',
+            component: 'MineCascader',
+            colProps: { span: 10 },
+          },
+          {
+            label: '层级',
+            field: 'level',
+            component: 'Select',
+            colProps: { span: 10 },
+            componentProps: {
+              options: [
+                { label: '省局', value: 1 },
+                { label: '执法处', value: 2 },
+                { label: '煤矿', value: 3 },
+                { label: '测风点', value: 4 },
+              ],
+            },
+          },
+        ],
+        showAdvancedButton: false,
+        schemaGroupNames: ['注册测风点位查询'],
+        actionColOptions: {
+          lg: 16,
+          xl: 14,
+          xxl: 10,
+        },
+      },
+      useSearchForm: true,
+      immediate: true,
+      columns: pointColumns,
+      api: getPointList,
+      pagination: true,
+      striped: true,
+      bordered: true,
+      showIndexColumn: true,
+      actionColumn: {
+        width: 120,
+        title: '操作',
+        dataIndex: 'action',
+        slots: { customRender: 'action' },
+      },
+    },
+  });
+  const [registerPointTable, pointTable] = pointCtx;
+
+  // ====== 布点合规校验表格 ======
+  const { tableContext: complianceCtx } = useListPage({
+    tableProps: {
+      title: '上传数据布点合规校验',
+      formConfig: {
+        autoSearch: true,
+        labelWidth: 120,
+        schemas: [
+          {
+            label: '煤矿名称',
+            field: 'deptId',
+            component: 'MineCascader',
+            colProps: { span: 14 },
+          },
+        ],
+        showAdvancedButton: false,
+        schemaGroupNames: ['上传数据布点查询'],
+        actionColOptions: {
+          lg: 14,
+          xl: 12,
+          xxl: 10,
+        },
+      },
+      useSearchForm: true,
+      immediate: true,
+      columns: complianceColumns,
+      api: getComplianceCheck,
+      pagination: false,
+      striped: true,
+      bordered: true,
+      showIndexColumn: true,
+      showActionColumn: false,
+    },
+  });
+  const [registerComplianceTable] = complianceCtx;
+
+  // ====== 判识模型参数表格 ======
+  const modelConfigs = ref<any[]>([]);
+
+  async function loadModelConfigs() {
+    try {
+      const result = await getModelConfigs();
+      modelConfigs.value = result || defaultModelConfigs;
+    } catch {
+      modelConfigs.value = defaultModelConfigs;
+    }
+  }
+
+  const { tableContext: modelCtx } = useListPage({
+    tableProps: {
+      title: '隐蔽工作面判识模型参数配置',
+      formConfig: {
+        autoSearch: true,
+        labelWidth: 120,
+        schemas: [
+          {
+            label: '煤矿名称',
+            field: 'deptId',
+            component: 'MineCascader',
+            colProps: { span: 6 },
+          },
+        ],
+        showAdvancedButton: false,
+        schemaGroupNames: ['常规查询'],
+        actionColOptions: {
+          lg: 12,
+          xl: 10,
+          xxl: 8,
+        },
+      },
+      useSearchForm: true,
+      immediate: true,
+      columns: modelConfigColumns,
+      api: async () => {
+        await loadModelConfigs();
+        return modelConfigs.value;
+      },
+      pagination: false,
+      striped: true,
+      bordered: true,
+      showIndexColumn: true,
+      actionColumn: {
+        width: 80,
+        title: '操作',
+        dataIndex: 'action',
+        slots: { customRender: 'action' },
+      },
+    },
+  });
+  const [registerModelConfigTable] = modelCtx;
+
+  // ====== 保存模型配置 ======
+  async function handleSaveModelConfig() {
+    try {
+      await saveModelConfigs({ configs: modelConfigs.value });
+      message.success('配置保存成功');
+    } catch {
+      message.error('保存失败');
+    }
+  }
+
+  async function handleResetConfig() {
+    modelConfigs.value = JSON.parse(JSON.stringify(defaultModelConfigs));
+    message.success('已恢复默认配置');
+  }
+
+  // ====== 新增/编辑点位弹窗 ======
+  const isUpdate = ref(true);
+  const modalTitle = computed(() => (!isUpdate.value ? '新增测风点位' : '编辑测风点位'));
+
+  const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
+    schemas: formSchema,
+    showActionButtonGroup: false,
+    labelCol: {
+      xs: { span: 24 },
+      sm: { span: 4 },
+    },
+    wrapperCol: {
+      xs: { span: 24 },
+      sm: { span: 18 },
+    },
+  });
+
+  const [registerPointModal, { openModal, closeModal, setModalProps }] = useModal();
+
+  function openAddModal() {
+    isUpdate.value = false;
+    openModal(true, {});
+    return resetFields();
+  }
+
+  async function handleEdit(record: any) {
+    isUpdate.value = true;
+    openModal(true, {});
+    await resetFields();
+    return setFieldsValue({ ...record });
+  }
+
+  // ====== 判识模型参数编辑弹窗 ======
+  const isModelUpdate = ref(true);
+  const modelModalTitle = computed(() => (!isModelUpdate.value ? '新增判识模型参数' : '编辑判识模型参数'));
+
+  const [registerModelForm, { resetFields: resetModelFields, setFieldsValue: setModelFieldsValue, validate: validateModel }] = useForm({
+    schemas: modelConfigFormSchema,
+    showActionButtonGroup: false,
+    labelCol: { xs: { span: 24 }, sm: { span: 4 } },
+    wrapperCol: { xs: { span: 24 }, sm: { span: 18 } },
+  });
+
+  const [registerModelModal, { openModal: openModelModal, closeModal: closeModelModal, setModalProps: setModelModalProps }] = useModal();
+
+  async function handleEditModel(record: any) {
+    isModelUpdate.value = true;
+    openModelModal(true, {});
+    await resetModelFields();
+    return setModelFieldsValue({ ...record });
+  }
+
+  async function handleSubmitModel() {
+    try {
+      const values = await validateModel();
+      setModelModalProps({ confirmLoading: true });
+      // 将编辑后的值更新到 modelConfigs 中
+      const idx = modelConfigs.value.findIndex((item) => item.paramKey === values.paramKey);
+      if (idx > -1) {
+        modelConfigs.value[idx] = { ...modelConfigs.value[idx], ...values };
+      }
+      message.success('编辑成功');
+      closeModelModal();
+    } catch {
+      // validate 未通过时不处理
+    } finally {
+      setModelModalProps({ confirmLoading: false });
+    }
+  }
+
+  async function handleDelete(record: any) {
+    try {
+      await deletePoint({ ids: record.id });
+      message.success('删除成功');
+      pointTable?.reload();
+    } catch {
+      message.error('删除失败');
+    }
+  }
+
+  async function handleSubmit() {
+    try {
+      const values = await validate();
+      setModalProps({ confirmLoading: true });
+      await savePoint(values);
+      message.success('保存成功');
+      closeModal();
+      pointTable?.reload();
+    } catch {
+      // validate 未通过时不处理
+    } finally {
+      setModalProps({ confirmLoading: false });
+    }
+  }
+</script>
+
+<style lang="less" scoped>
+  .wind-point-manage {
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+  }
+
+  .wind-point-manage :deep(.common-page-tabs) {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+    margin: 8px;
+    padding: 8px;
+  }
+
+  .wind-point-manage :deep(.common-page-tabs .ant-tabs-content-holder) {
+    flex: 1;
+    overflow: hidden;
+  }
+
+  .wind-point-manage :deep(.common-page-tabs .ant-tabs-content) {
+    height: 100%;
+  }
+
+  .wind-point-manage :deep(.common-page-tabs .ant-tabs-tabpane) {
+    height: 100%;
+  }
+
+  .config-note {
+    margin-top: 12px;
+  }
+</style>

+ 55 - 0
src/views/analysis/warningAnalysis/windPointManage/windPointManage.api.ts

@@ -0,0 +1,55 @@
+import { defHttp } from '/@/utils/http/axios';
+
+enum Api {
+  getPointList = '/workingface/mineArea/getMineAreaList',
+  addMineArea = '/workingface/mineArea/addMineArea',
+  updateMineArea = '/workingface/mineArea/updateMineArea',
+  deleteMineArea = '/workingface/mineArea/deleteMineArea',
+  getComplianceCheck = '/province/windpoint/getComplianceCheck',
+  getModelConfigs = '/province/windpoint/getModelConfigs',
+  saveModelConfigs = '/province/windpoint/saveModelConfigs',
+}
+
+/** 模拟成功响应 */
+const mockSuccess = { success: true, code: 200, message: '操作成功' };
+
+/** 模拟点位列表数据 */
+const mockPointList = [
+  { id: 'p001', mineCode: 'M001', name: '榆林执法处-大柳塔煤矿北翼回风巷', level: 4, airVolume: 1800, windrectId: 'W001', status: 'active', createTime: '2026-07-27 10:00:00' },
+  { id: 'p002', mineCode: 'M001', name: '榆林执法处-大柳塔煤矿南翼进风巷', level: 4, airVolume: 2100, windrectId: 'W002', status: 'active' },
+  { id: 'p003', mineCode: 'M002', name: '延安执法处-黄陵一号矿101工作面', level: 4, airVolume: 720, windrectId: 'W003', status: 'active' },
+  { id: 'p004', mineCode: 'M003', name: '铜川执法处-红柳林煤矿东翼回风大巷', level: 4, airVolume: 1600, windrectId: 'W004', status: 'active' },
+  { id: 'p005', mineCode: 'M001', name: '榆林执法处-活鸡兔煤矿副立井', level: 4, airVolume: 2800, windrectId: 'W005', status: 'active' },
+  { id: 'm001', mineCode: 'M101', name: '大柳塔煤矿', level: 3, airVolume: 0, windrectId: '', status: 'active' },
+  { id: 'm002', mineCode: 'M102', name: '活鸡兔煤矿', level: 3, airVolume: 0, windrectId: '', status: 'active' },
+  { id: 'm003', mineCode: 'M106', name: '黄陵一号矿', level: 3, airVolume: 0, windrectId: '', status: 'active' },
+  { id: 'm004', mineCode: 'M107', name: '红柳林煤矿', level: 3, airVolume: 0, windrectId: '', status: 'active' },
+  { id: 'e001', mineCode: 'M001', name: '榆林执法处', level: 2, airVolume: 0, windrectId: '', status: 'active' },
+  { id: 'e002', mineCode: 'M002', name: '延安执法处', level: 2, airVolume: 0, windrectId: '', status: 'active' },
+  { id: 'e003', mineCode: 'M003', name: '铜川执法处', level: 2, airVolume: 0, windrectId: '', status: 'active' },
+];
+
+/** 获取 MineArea 列表(后端未部署时回退到模拟数据) */
+export const getPointList = (params?: any) =>
+  defHttp.post({ url: Api.getPointList, params }, { joinParamsToUrl: true }).catch(() => mockPointList);
+
+/** 新增或编辑 MineArea(根据是否有 id 自动切换;后端未部署时返回模拟成功) */
+export const savePoint = (params?: any) => {
+  const url = params?.id ? Api.updateMineArea : Api.addMineArea;
+  return defHttp.post({ url, params }).catch(() => mockSuccess);
+};
+
+/** 删除 MineArea(直接模拟成功,不请求后端) */
+export const deletePoint = (_params?: any) => Promise.resolve(mockSuccess);
+
+/** 获取上传数据合规校验结果 */
+export const getComplianceCheck = (params?: any) =>
+  defHttp.post({ url: Api.getComplianceCheck, params }, { joinParamsToUrl: true });
+
+/** 获取判识模型配置 */
+export const getModelConfigs = (params?: any) =>
+  defHttp.post({ url: Api.getModelConfigs, params }, { joinParamsToUrl: true });
+
+/** 保存判识模型配置 */
+export const saveModelConfigs = (params?: any) =>
+  defHttp.post({ url: Api.saveModelConfigs, params }, { joinParamsToUrl: true });

+ 317 - 0
src/views/analysis/warningAnalysis/windPointManage/windPointManage.data.ts

@@ -0,0 +1,317 @@
+import type { BasicColumn } from '/@/components/Table';
+import type { FormSchema } from '/@/components/Table';
+import { h } from 'vue';
+import { StatusColorEnum } from '/@/enums/jeecgEnum';
+
+/** 注册测风点位管理表格列 */
+export const pointColumns: BasicColumn[] = [
+  {
+    title: '煤矿编号',
+    dataIndex: 'mineCode',
+    width: 100,
+    fixed: 'left',
+  },
+  {
+    title: '区域名称',
+    dataIndex: 'name',
+    width: 150,
+    fixed: 'left',
+  },
+  {
+    title: '层级',
+    dataIndex: 'level',
+    width: 100,
+    customRender: ({ text }) => {
+      const map: Record<number, string> = {
+        1: '省局',
+        2: '执法处',
+        3: '煤矿',
+        4: '测风点',
+      };
+      return map[text] || text;
+    },
+  },
+  {
+    title: '风量(m³/min)',
+    dataIndex: 'airVolume',
+    width: 100,
+  },
+  {
+    title: '测风设备',
+    dataIndex: 'windrectId',
+    width: 100,
+  },
+  {
+    title: '创建时间',
+    dataIndex: 'createTime',
+    width: 150,
+  },
+  // {
+  //   title: '状态',
+  //   dataIndex: 'status',
+  //   width: 80,
+  //   customRender: ({ text }) => {
+  //     const val = text ?? 'active';
+  //     const color = val === 'active' ? StatusColorEnum.green : StatusColorEnum.gray;
+  //     const label = val === 'active' ? '在用' : '停用';
+  //     return h('span', { style: { color } }, label);
+  //   },
+  // },
+];
+
+/** 上传数据布点合规校验表格列 */
+export const complianceColumns: BasicColumn[] = [
+  {
+    title: '注册点位',
+    dataIndex: 'pointCode',
+    width: 130,
+    fixed: 'left',
+  },
+  {
+    title: '上传数据',
+    dataIndex: 'pointName',
+    width: 180,
+  },
+  {
+    title: '校验结果',
+    dataIndex: 'checkResult',
+    width: 100,
+    customRender: ({ text }) => {
+      const map: Record<string, { label: string; color: string }> = {
+        match: { label: '一致', color: StatusColorEnum.green },
+        missing: { label: '缺失报警', color: StatusColorEnum.red },
+        unregistered: { label: '未注册提醒', color: StatusColorEnum.orange },
+      };
+      const info = map[text];
+      return info ? h('span', { style: { color: info.color, fontWeight: 'bold' } }, info.label) : text;
+    },
+  },
+  {
+    title: '问题说明',
+    dataIndex: 'uploadPointName',
+    width: 160,
+  },
+];
+
+/** 新增/编辑 MineArea 表单 */
+export const formSchema: FormSchema[] = [
+  {
+    label: '煤矿编号',
+    field: 'mineCode',
+    component: 'MineCascader',
+    componentProps: {
+      placeholder: '请输入煤矿编号',
+    },
+  },
+  {
+    label: '矿井名称',
+    field: 'name',
+    required: true,
+    component: 'Input',
+    componentProps: {
+      placeholder: '请输入矿井名称',
+    },
+  },
+  {
+    label: '层级',
+    field: 'level',
+    component: 'Select',
+    componentProps: {
+      placeholder: '请选择层级',
+      options: [
+        { label: '省局', value: 1 },
+        { label: '执法处', value: 2 },
+        { label: '煤矿', value: 3 },
+        { label: '测风点', value: 4 },
+      ],
+    },
+  },
+  {
+    label: '风量(m³/min)',
+    field: 'airVolume',
+    component: 'InputNumber',
+    componentProps: {
+      placeholder: '请输入风量',
+      min: 0,
+      style: 'width: 100%',
+    },
+  },
+  {
+    label: '测风设备',
+    field: 'windrectId',
+    component: 'Input',
+    componentProps: {
+      placeholder: '请输入测风设备 ID',
+    },
+  },
+];
+
+/** 判识模型参数配置表格列 */
+export const modelConfigColumns: BasicColumn[] = [
+  {
+    title: '判识指标',
+    dataIndex: 'paramName',
+    width: 200,
+    fixed: 'left',
+  },
+  {
+    title: '判别条件',
+    dataIndex: 'paramFormula',
+    width: 150,
+  },
+  {
+    title: '阈值参数',
+    dataIndex: 'paramKey',
+    width: 150,
+  },
+  {
+    title: '阈值参数',
+    dataIndex: 'currentValue',
+    width: 120,
+    edit: true,
+    editComponent: 'Input',
+  },
+  {
+    title: '默认值',
+    dataIndex: 'defaultValue',
+    width: 120,
+  },
+  {
+    title: '当前值',
+    dataIndex: 'value',
+    width: 120,
+  },
+  {
+    title: '权重',
+    dataIndex: 'weight',
+    width: 120,
+  },
+];
+
+/** 判识模型参数编辑表单(与 modelConfigColumns 字段对齐) */
+export const modelConfigFormSchema: FormSchema[] = [
+  {
+    label: '判识指标',
+    field: 'paramName',
+    component: 'Input',
+    componentProps: { disabled: true },
+  },
+  {
+    label: '判别条件',
+    field: 'paramFormula',
+    component: 'Input',
+    componentProps: { placeholder: '请输入判别条件' },
+  },
+  {
+    label: '阈值参数',
+    field: 'paramKey',
+    required: true,
+    component: 'Input',
+    componentProps: { placeholder: '请输入阈值参数字段名' },
+  },
+  {
+    label: '阈值参数值',
+    field: 'currentValue',
+    required: true,
+    component: 'Input',
+    componentProps: { placeholder: '请输入阈值参数值' },
+  },
+  {
+    label: '默认值',
+    field: 'defaultValue',
+    component: 'Input',
+    componentProps: { placeholder: '请输入默认值' },
+  },
+  {
+    label: '当前值',
+    field: 'value',
+    component: 'Input',
+    componentProps: { placeholder: '请输入当前值' },
+  },
+  {
+    label: '权重',
+    field: 'weight',
+    component: 'InputNumber',
+    componentProps: { placeholder: '请输入权重', min: 0, max: 1, step: 0.01, style: 'width:100%' },
+  },
+  {
+    label: '单位',
+    field: 'unit',
+    component: 'Input',
+    componentProps: { placeholder: '请输入单位' },
+  },
+  {
+    label: '说明',
+    field: 'description',
+    component: 'InputTextArea',
+    componentProps: { placeholder: '请输入说明', rows: 3 },
+  },
+];
+
+/** 判识模型默认配置数据 */
+export const defaultModelConfigs = [
+  {
+    paramName: '风速上限',
+    paramKey: 'windSpeedUpperLimit',
+    currentValue: '6.0',
+    defaultValue: '6.0',
+    unit: 'm/s',
+    description: '实测风速超过此值即触发风速超限报警',
+  },
+  {
+    paramName: '风量不足裕度',
+    paramKey: 'volumeLowMargin',
+    currentValue: '20',
+    defaultValue: '20',
+    unit: '%',
+    description: '实测风量低于标准风量×(1-裕度)即触发风量不足报警',
+  },
+  {
+    paramName: '不闭合阈值',
+    paramKey: 'unclosedThreshold',
+    currentValue: '10',
+    defaultValue: '10',
+    unit: '%',
+    description: '进风量与回风量差值占比超过此值即触发风量不闭合报警',
+  },
+  {
+    paramName: '缺失点位报警数',
+    paramKey: 'missingPointAlarm',
+    currentValue: '1',
+    defaultValue: '1',
+    unit: '个',
+    description: '上传测风点数少于注册测风点数超过此值即触发报警',
+  },
+  {
+    paramName: '关联异常点数',
+    paramKey: 'relatedAbnormalPoints',
+    currentValue: '2',
+    defaultValue: '2',
+    unit: '个',
+    description: '同一区域多个测风点同时异常超过此值即触发关联异常报警',
+  },
+  {
+    paramName: '综合判识阈值',
+    paramKey: 'compositeThreshold',
+    currentValue: '0.60',
+    defaultValue: '0.60',
+    unit: '',
+    description: '加权风险得分达到此值判定为"疑似"隐蔽工作面',
+  },
+  {
+    paramName: '采区风量差值异常周期',
+    paramKey: 'diffAbnormalCycles',
+    currentValue: '3',
+    defaultValue: '3',
+    unit: '个测风周期',
+    description: '差值持续为正且超过此测风周期数即触发异常',
+  },
+  {
+    paramName: '采区有效风量率警戒线',
+    paramKey: 'effectiveRateWarning',
+    currentValue: '70',
+    defaultValue: '70',
+    unit: '%',
+    description: '采区有效风量率低于此值即触发预警',
+  },
+];

+ 366 - 0
src/views/analysis/warningAnalysis/windPointManage/windTopology/hooks/useTopology.ts

@@ -0,0 +1,366 @@
+import { ref } from 'vue';
+import * as echarts from 'echarts';
+import type { EChartsOption } from 'echarts';
+import { message, Modal } from 'ant-design-vue';
+import {
+  createGraphOption,
+  categories,
+  statusColorMap,
+  nodeDetailFields,
+  levelY,
+  weightKey,
+  isOneToManyChild,
+} from '../windTopology.data';
+import type { TopologyData, TopoNodeData } from '../windTopology.data';
+import { addMineAreaRelation, deleteMineAreaRelation, getTopologyData } from '../windTopology.api';
+
+export function useTopology() {
+  const chartRef = ref<HTMLDivElement | null>(null);
+  let chartInstance: echarts.ECharts | null = null;
+  let topologyData: TopologyData = { nodes: [], links: [] };
+
+  // —— 模式状态 ——
+  const editMode = ref(false);
+  /** 绑定选择态:双击节点后等待单击目标 */
+  const selectMode = ref(false);
+  const selectedSourceId = ref('');
+
+  // —— 选中节点详情 ——
+  const selectedNode = ref<TopoNodeData | null>(null);
+  const detailFields = ref<any[]>([]);
+
+  // —— 初始化 ——
+  function initChart(el: HTMLDivElement) {
+    chartInstance = echarts.init(el);
+    window.addEventListener('resize', handleResize);
+    return chartInstance;
+  }
+
+  // ==================== 层级布局 ====================
+
+  function applyHierarchicalLayout(echartsNodes: any[], forceUnfixed = false, chartW = 1200, chartH = 700) {
+    const groups: Record<number, any[]> = {};
+    for (const en of echartsNodes) {
+      const cat = en.category ?? 0;
+      if (!groups[cat]) groups[cat] = [];
+      groups[cat].push(en);
+    }
+    for (let cat = 0; cat < categories.length; cat++) {
+      const list = groups[cat];
+      if (!list || list.length === 0) continue;
+      const wk = weightKey[cat];
+      list.sort((a, b) => {
+        const va = wk && a.raw ? Number(a.raw[wk]) || 0 : 0;
+        const vb = wk && b.raw ? Number(b.raw[wk]) || 0 : 0;
+        return vb - va;
+      });
+      const margin = 60;
+      const tw = chartW - margin * 2;
+      const gap = list.length > 1 ? tw / (list.length - 1) : 0;
+      const sx = margin + (tw - (list.length - 1) * gap) / 2;
+      const yPct = levelY[cat] ?? 50;
+      const y = (yPct / 100) * chartH;
+      list.forEach((en, idx) => {
+        en.x = sx + idx * gap;
+        en.y = y;
+        en.fixed = !forceUnfixed;
+      });
+    }
+  }
+
+  // —— 渲染拓扑 ——
+  function renderTopology(data: TopologyData, forceUnfixed = false) {
+    topologyData = data;
+    if (!chartInstance) return;
+
+    const option: EChartsOption = createGraphOption();
+    const series: any = (option.series as any[])[0];
+
+    const echartsNodes = data.nodes.map((n) => {
+      const cat = categories[n.category] || categories[0];
+      const statusColor = statusColorMap[n.status || 'normal'];
+      return {
+        id: n.id,
+        name: n.name,
+        category: n.category,
+        value: n.name,
+        symbol: cat.symbol,
+        symbolSize: cat.symbolSize,
+        itemStyle: { color: statusColor || cat.color },
+        raw: n,
+      };
+    });
+
+    const cw = chartInstance.getWidth();
+    const ch = chartInstance.getHeight();
+    applyHierarchicalLayout(echartsNodes, forceUnfixed, cw, ch);
+    series.nodes = echartsNodes;
+    series.links = data.links.map((l) => ({
+      source: l.source,
+      target: l.target,
+    }));
+
+    if (editMode.value) {
+      series.lineStyle = { ...series.lineStyle, type: 'dashed' as const, width: 2, opacity: 0.6 };
+    }
+
+    chartInstance.setOption(option, true);
+    registerEvents();
+  }
+
+  // ==================== 交互事件 ====================
+
+  function registerEvents() {
+    if (!chartInstance) return;
+    chartInstance.off('click');
+    chartInstance.off('dblclick');
+    chartInstance.off('dragend');
+
+    // ——— 单击 ———
+    chartInstance.on('click', (params: any) => {
+      if (!params.data) return;
+
+      if (params.dataType === 'node') {
+        if (editMode.value && selectMode.value) {
+          // 选择态下单击另一节点 → 尝试绑定
+          attemptBind(selectedSourceId.value, params.data.id);
+          return;
+        }
+        // 普通单击 → 显示详情
+        const raw = params.data.raw as TopoNodeData;
+        if (raw) showNodeDetail(raw);
+      }
+    });
+
+    // ——— 双击 ———
+    chartInstance.on('dblclick', (params: any) => {
+      if (!params.data) return;
+
+      if (params.dataType === 'edge' && editMode.value) {
+        // 双击连线 → 解绑
+        handleUnbind(params.data.source, params.data.target);
+        return;
+      }
+
+      if (params.dataType === 'node' && editMode.value) {
+        // 双击节点 → 进入选择目标态
+        enterSelectMode(params.data.id);
+      }
+    });
+
+    // ——— 拖拽结束 ———
+    chartInstance.on('dragend', (params: any) => {
+      if (!params.data || params.dataType !== 'node') return;
+
+      // 普通模式下拖拽 → 高亮关联路径(节点不移动)
+      if (!editMode.value) {
+        highlightPath(params.data.id);
+        // fixed 节点拖不动,但触发高亮后复位
+        return;
+      }
+
+      // 绑定模式下拖拽 → 物理移动 + 高亮
+      highlightPath(params.data.id);
+    });
+  }
+
+  // ==================== 选择目标态 ====================
+
+  function enterSelectMode(sourceId: string) {
+    selectMode.value = true;
+    selectedSourceId.value = sourceId;
+    const src = topologyData.nodes.find((n) => n.id === sourceId);
+    highlightPath(sourceId);
+    message.info(`已选中"${src?.name || sourceId}",请单击另一节点进行绑定。双击空白处取消。`);
+  }
+
+  function exitSelectMode() {
+    selectMode.value = false;
+    selectedSourceId.value = '';
+    chartInstance?.dispatchAction({ type: 'downplay' });
+  }
+
+  // ==================== 绑定逻辑 ====================
+
+  async function attemptBind(sourceId: string, targetId: string) {
+    exitSelectMode();
+    if (sourceId === targetId) {
+      message.warning('不能绑定到自身');
+      return;
+    }
+    const src = topologyData.nodes.find((n) => n.id === sourceId);
+    const tgt = topologyData.nodes.find((n) => n.id === targetId);
+    if (!src || !tgt) return;
+
+    // 检查是否已存在该绑定
+    const exists = topologyData.links.some((l) => l.source === sourceId && l.target === targetId);
+    if (exists) {
+      message.warning('已存在该绑定关系');
+      return;
+    }
+
+    async function doBind() {
+      try {
+        await addMineAreaRelation({ parentId: sourceId, childId: targetId });
+        const freshData = await getTopologyData({ mineCode: 'M001' });
+        renderTopology(freshData, true);
+        message.success(`绑定成功:${src!.name} → ${tgt!.name}`);
+      } catch {
+        message.error('绑定失败');
+      }
+    }
+
+    if (isOneToManyChild(tgt.category)) {
+      // ——— 一对多:覆盖绑定 ———
+      const existingLink = topologyData.links.find((l) => l.target === targetId);
+      const oldName = existingLink
+        ? topologyData.nodes.find((n) => n.id === existingLink.source)?.name || '未知'
+        : '无';
+
+      Modal.confirm({
+        title: '确认覆盖绑定',
+        content: `"${tgt.name}" 当前从属于 "${oldName}",是否覆盖为从属于 "${src.name}"?`,
+        okText: '确认覆盖',
+        cancelText: '取消',
+        onOk: async () => {
+          if (existingLink?.id) {
+            await deleteMineAreaRelation({ ids: existingLink.id });
+          }
+          await doBind();
+        },
+      });
+    } else {
+      // ——— 多对多:直接绑定 ———
+      Modal.confirm({
+        title: '确认绑定',
+        content: `确认建立"${src.name}" → "${tgt.name}"的绑定关系?`,
+        okText: '确认绑定',
+        cancelText: '取消',
+        onOk: doBind,
+      });
+    }
+  }
+
+  // ==================== 解绑逻辑 ====================
+
+  async function handleUnbind(sourceId: string, targetId: string) {
+    const src = topologyData.nodes.find((n) => n.id === sourceId);
+    const tgt = topologyData.nodes.find((n) => n.id === targetId);
+    Modal.confirm({
+      title: '确认解绑',
+      content: `确定解除"${src?.name || sourceId}" → "${tgt?.name || targetId}"的绑定关系?`,
+      okText: '确认解绑',
+      cancelText: '取消',
+      onOk: async () => {
+        const link = topologyData.links.find((l) => l.source === sourceId && l.target === targetId);
+        if (link?.id) {
+          try {
+            await deleteMineAreaRelation({ ids: link.id });
+            const freshData = await getTopologyData({ mineCode: 'M001' });
+            renderTopology(freshData, true);
+            message.success('解绑成功');
+          } catch { message.error('解绑失败'); }
+        } else {
+          message.warning('未找到对应绑定关系');
+        }
+      },
+    });
+  }
+
+  // ==================== 节点详情 ====================
+
+  function showNodeDetail(node: TopoNodeData) {
+    selectedNode.value = node;
+    const catName = categories[node.category]?.name || '';
+    const fields = nodeDetailFields[catName] || [];
+    detailFields.value = fields.map((f) => ({
+      label: f.label,
+      value: (node as any)[f.key] ?? '-',
+    }));
+  }
+
+  function clearSelection() {
+    selectedNode.value = null;
+    detailFields.value = [];
+    exitSelectMode();
+  }
+
+  // ==================== 高亮路径 ====================
+
+  function highlightPath(nodeId: string) {
+    if (!chartInstance) return;
+    const s = (chartInstance.getOption().series as any[])[0];
+    const links = s.links || [];
+    const indices: number[] = [];
+    links.forEach((l: any, i: number) => {
+      if (l.source === nodeId || l.target === nodeId) indices.push(i);
+    });
+    chartInstance.dispatchAction({ type: 'downplay' });
+    chartInstance.dispatchAction({ type: 'highlight', seriesIndex: 0, name: nodeId });
+    indices.forEach((i) => {
+      chartInstance!.dispatchAction({ type: 'highlight', seriesIndex: 0, dataIndex: i, dataType: 'edge' });
+    });
+  }
+
+  // ==================== 缩放 ====================
+
+  function zoomIn() {
+    const cur = chartInstance?.getOption()?.series?.[0] as any;
+    const z = cur?.zoom ?? 1;
+    chartInstance?.setOption({ series: [{ zoom: Math.min(z * 1.3, 5) }] as any });
+  }
+
+  function zoomOut() {
+    const cur = chartInstance?.getOption()?.series?.[0] as any;
+    const z = cur?.zoom ?? 1;
+    chartInstance?.setOption({ series: [{ zoom: Math.max(z / 1.3, 0.3) }] as any });
+  }
+
+  function resetView() {
+    chartInstance?.setOption({ series: [{ zoom: 1, center: undefined }] as any });
+    clearSelection();
+  }
+
+  // ==================== 编辑模式切换 ====================
+
+  function setEditMode(on: boolean) {
+    editMode.value = on;
+    if (!chartInstance) return;
+    exitSelectMode();
+
+    if (on) {
+      renderTopology(topologyData, true);
+      chartInstance.setOption({ series: [{ roam: false }] as any });
+      message.info('绑定模式已开启:双击节点选择源 → 单击另一节点绑定;双击连线解绑');
+    } else {
+      renderTopology(topologyData);
+      message.info('已退出绑定模式');
+    }
+  }
+
+  // ==================== resize / dispose ====================
+
+  function handleResize() { chartInstance?.resize(); }
+
+  function dispose() {
+    window.removeEventListener('resize', handleResize);
+    chartInstance?.dispose();
+    chartInstance = null;
+  }
+
+  return {
+    chartRef,
+    editMode,
+    selectMode,
+    selectedNode,
+    detailFields,
+    initChart,
+    renderTopology,
+    zoomIn, zoomOut, resetView,
+    highlightPath,
+    setEditMode,
+    clearSelection,
+    exitSelectMode,
+    dispose,
+  };
+}

+ 226 - 0
src/views/analysis/warningAnalysis/windPointManage/windTopology/index.vue

@@ -0,0 +1,226 @@
+<!-- eslint-disable vue/multi-word-component-names -->
+<template>
+  <div class="wind-topology">
+    <!-- 工具栏 -->
+    <div class="topo-toolbar">
+      <div class="toolbar-left">
+        <a-button-group>
+          <a-button @click="zoomIn" title="放大">+</a-button>
+          <a-button @click="zoomOut" title="缩小">-</a-button>
+          <a-button @click="resetView" title="复位">复位</a-button>
+        </a-button-group>
+        <a-divider type="vertical" />
+        <a-button :type="editMode ? 'primary' : 'default'" danger @click="toggleEditMode">
+          {{ editMode ? '完成绑定' : '绑定模式' }}
+        </a-button>
+        <a-divider type="vertical" />
+        <a-button @click="refreshData" title="刷新">
+          <template #icon><SvgIcon name="refresh" /></template>
+          刷新
+        </a-button>
+        <a-divider type="vertical" />
+        <span v-if="editMode" class="select-text">双击节点启用节点绑定,单击目标节点确认;双击连线解绑</span>
+      </div>
+
+      <!-- 图例 -->
+      <div class="toolbar-right">
+        <div class="legend">
+          <span v-for="cat in categories" :key="cat.name" class="legend-item">
+            <span class="legend-dot" :style="{ background: cat.color }"></span>
+            {{ cat.name }}
+          </span>
+          <span class="legend-item">
+            <span class="legend-dot" style="background: #fa8c16"></span>
+            异常
+          </span>
+          <span class="legend-item">
+            <span class="legend-dot" style="background: #999"></span>
+            离线
+          </span>
+        </div>
+      </div>
+    </div>
+
+    <!-- 主体 -->
+    <div class="topo-body">
+      <div ref="chartRef" class="topo-canvas"></div>
+
+      <!-- 详情面板 -->
+      <div v-if="selectedNode" class="detail-panel">
+        <div class="detail-header">
+          <span class="detail-title">节点详情</span>
+          <a-button size="small" type="text" @click="clearSelection">✕</a-button>
+        </div>
+        <div class="detail-category" :style="{ color: categories[selectedNode.category]?.color }">
+          {{ categories[selectedNode.category]?.name }}
+        </div>
+        <div class="detail-name">{{ selectedNode.name }}</div>
+        <div class="detail-fields">
+          <div v-for="field in detailFields" :key="field.label" class="detail-row">
+            <span class="detail-label">{{ field.label }}:</span>
+            <span class="detail-value">{{ field.value }}</span>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref, onMounted, onUnmounted } from 'vue';
+  import { SvgIcon } from '/@/components/Icon';
+  import { categories } from './windTopology.data';
+  import { getTopologyData } from './windTopology.api';
+  import { useTopology } from './hooks/useTopology';
+
+  const {
+    chartRef,
+    editMode,
+    // selectMode,
+    selectedNode,
+    detailFields,
+    initChart,
+    renderTopology,
+    zoomIn,
+    zoomOut,
+    resetView,
+    setEditMode,
+    clearSelection,
+    // exitSelectMode,
+    dispose,
+  } = useTopology();
+
+  function toggleEditMode() {
+    setEditMode(!editMode.value);
+  }
+
+  const mineCode = ref('M001');
+
+  async function refreshData() {
+    try {
+      const data = await getTopologyData({ mineCode: mineCode.value });
+      renderTopology(data);
+    } catch {
+      /* cache */
+    }
+  }
+
+  onMounted(async () => {
+    if (!chartRef.value) return;
+    initChart(chartRef.value);
+    const data = await getTopologyData({ mineCode: mineCode.value });
+    renderTopology(data);
+  });
+
+  onUnmounted(() => {
+    dispose();
+  });
+</script>
+
+<style lang="less" scoped>
+  .wind-topology {
+    height: 100%;
+    display: flex;
+    flex-direction: column;
+    background: #f5f7fa;
+  }
+
+  .topo-toolbar {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    padding: 8px 16px;
+    background: #fff;
+    border-bottom: 1px solid #e8e8e8;
+    flex-shrink: 0;
+
+    .toolbar-left {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      .toolbar-title {
+        font-size: 15px;
+        font-weight: 600;
+        color: #333;
+      }
+    }
+
+    .toolbar-right .legend {
+      display: flex;
+      gap: 12px;
+      align-items: center;
+      .legend-item {
+        display: flex;
+        align-items: center;
+        font-size: 12px;
+        color: #666;
+        gap: 4px;
+        .legend-dot {
+          display: inline-block;
+          width: 10px;
+          height: 10px;
+          border-radius: 50%;
+        }
+      }
+    }
+  }
+
+  .topo-body {
+    flex: 1;
+    position: relative;
+    overflow: hidden;
+  }
+
+  .topo-canvas {
+    width: 100%;
+    height: 100%;
+  }
+
+  .detail-panel {
+    position: absolute;
+    top: 12px;
+    right: 12px;
+    width: 240px;
+    background: #fff;
+    border-radius: 6px;
+    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.12);
+    padding: 14px 16px;
+    z-index: 10;
+
+    .detail-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      margin-bottom: 8px;
+      .detail-title {
+        font-size: 14px;
+        font-weight: 600;
+      }
+    }
+    .detail-category {
+      font-size: 12px;
+      font-weight: 500;
+      margin-bottom: 2px;
+    }
+    .detail-name {
+      font-size: 16px;
+      font-weight: 600;
+      color: #333;
+      margin-bottom: 10px;
+    }
+    .detail-fields .detail-row {
+      display: flex;
+      justify-content: space-between;
+      padding: 4px 0;
+      font-size: 13px;
+      border-bottom: 1px solid #f0f0f0;
+      .detail-label {
+        color: #888;
+      }
+      .detail-value {
+        color: #333;
+        font-weight: 500;
+      }
+    }
+  }
+</style>

+ 38 - 0
src/views/analysis/warningAnalysis/windPointManage/windTopology/windTopology.api.ts

@@ -0,0 +1,38 @@
+import { defHttp } from '/@/utils/http/axios';
+import type { TopologyData, MineAreaApiResponse, MineAreaRelation } from './windTopology.data';
+import { transformToTopologyData, mockMineAreaApiResponse } from './windTopology.data';
+
+enum Api {
+  getMineAreaRelation = '/workingface/mineArea/getMineAreaRelation',
+  addMineAreaRelation = '/workingface/mineArea/addMineAreaRelation',
+  deleteMineAreaRelation = '/workingface/mineArea/deleteMineAreaRelation',
+}
+
+/** 模拟成功响应 */
+const mockSuccess = { success: true, code: 200, message: '操作成功' };
+
+/** 获取拓扑结构(后端未部署时回退到模拟数据) */
+export const getTopologyData = (params?: any): Promise<TopologyData> =>
+  defHttp
+    .post<MineAreaApiResponse>({ url: Api.getMineAreaRelation, params }, { joinParamsToUrl: true })
+    .then((res) => transformToTopologyData(res))
+    .catch(() => transformToTopologyData(mockMineAreaApiResponse));
+
+/** 绑定关联关系(直接更新模拟数据,供下次 getTopologyData 读取) */
+export const addMineAreaRelation = (params: { mineCode?: string; parentId: string; childId: string }): Promise<any> => {
+  const newRel: MineAreaRelation = {
+    id: `rel-mock-${Date.now()}`,
+    mineCode: params.mineCode || 'M001',
+    parentId: params.parentId,
+    childId: params.childId,
+  };
+  mockMineAreaApiResponse.mineAreaRelationList.push(newRel);
+  return defHttp.post({ url: Api.addMineAreaRelation, params }).catch(() => mockSuccess);
+};
+
+/** 解绑关联关系(直接更新模拟数据,供下次 getTopologyData 读取) */
+export const deleteMineAreaRelation = (params: { ids: string }): Promise<any> => {
+  const idx = mockMineAreaApiResponse.mineAreaRelationList.findIndex((r) => r.id === params.ids);
+  if (idx > -1) mockMineAreaApiResponse.mineAreaRelationList.splice(idx, 1);
+  return defHttp.post({ url: Api.deleteMineAreaRelation, params }, { joinParamsToUrl: true }).catch(() => mockSuccess);
+};

+ 287 - 0
src/views/analysis/warningAnalysis/windPointManage/windTopology/windTopology.data.ts

@@ -0,0 +1,287 @@
+import type { EChartsOption } from 'echarts';
+
+// ==================== 节点分类定义 ====================
+
+export interface CategoryDef {
+  name: string; // 分类名称
+  color: string; // 节点颜色
+  symbol: string; // ECharts 图形:circle / rect / diamond / roundRect
+  symbolSize: number; // 节点大小
+}
+
+/** 4 类节点:省局、执法处、煤矿、测风点 */
+export const categories: CategoryDef[] = [
+  { name: '省局', color: '#0960bd', symbol: 'diamond', symbolSize: 50 },
+  { name: '执法处', color: '#1890ff', symbol: 'roundRect', symbolSize: 40 },
+  { name: '煤矿', color: '#52c41a', symbol: 'circle', symbolSize: 35 },
+  { name: '测风点', color: '#fa8c16', symbol: 'pin', symbolSize: 25 },
+];
+
+// ==================== 状态颜色映射 ====================
+
+export const statusColorMap: Record<string, string> = {
+  normal: '#52c41a', // 正常
+  abnormal: '#fa8c16', // 异常
+  offline: '#999999', // 离线
+};
+
+// ==================== 层级布局参数(树形但本质是图) ====================
+
+/** 每个层级在画布高度中的百分比 y 坐标(从上到下) */
+export const levelY = [8, 30, 55, 85]; // 省局、执法处、煤矿、测风点
+
+/** 每类节点用于排序的权重字段(值越大越靠左) */
+export const weightKey: Record<number, string> = {
+  0: 'childCount', // 省局
+  1: 'childCount', // 执法处
+  2: 'childCount', // 煤矿(按管辖测风点数排序)
+  3: 'windVolume', // 测风点
+};
+
+/** 节点横向间距基数(px),实际 = basePadding / 该层节点数 */
+export const basePadding = 800;
+
+/** 画布宽高占位(chart 100% 容器,此处设参考值) */
+export const LAYOUT_WIDTH = 1200;
+export const LAYOUT_HEIGHT = 700;
+
+// ==================== ECharts graph 基础配置 ====================
+
+export function createGraphOption(): EChartsOption {
+  return {
+    tooltip: { trigger: 'item', formatter: '{b}' },
+    series: [
+      {
+        type: 'graph',
+        layout: 'none', // 固定位置,由 applyHierarchicalLayout 分配坐标
+        roam: true,
+        draggable: true, // 始终可拖,编辑模式只影响 roam
+        categories: categories.map((c) => ({ name: c.name, itemStyle: { color: c.color } })),
+        edgeSymbol: ['none', 'arrow'],
+        edgeSymbolSize: [0, 10],
+        label: {
+          show: true,
+          position: 'bottom',
+          fontSize: 11,
+          formatter: (p: any) => p.name || '',
+        },
+        lineStyle: { color: 'source', curveness: 0.3, width: 2, opacity: 0.7 },
+        emphasis: {
+          focus: 'adjacency',
+          lineStyle: { width: 3, opacity: 1 },
+        },
+        nodes: [],
+        links: [],
+      },
+    ],
+  };
+}
+
+// ==================== 节点详情字段 ====================
+
+export interface DetailField {
+  label: string;
+  key: string;
+}
+
+export const nodeDetailFields: Record<string, DetailField[]> = {
+  省局: [
+    { label: '机构名称', key: 'name' },
+    { label: '管辖执法处数', key: 'childCount' },
+  ],
+  执法处: [
+    { label: '名称', key: 'name' },
+    { label: '管辖煤矿数', key: 'childCount' },
+    { label: '在线率', key: 'onlineRate' },
+  ],
+  煤矿: [
+    { label: '煤矿名称', key: 'name' },
+    { label: '测风点数', key: 'childCount' },
+    { label: '在线/应接', key: 'onlineRatio' },
+    { label: '风量异常', key: 'windAbnormal' },
+  ],
+  测风点: [
+    { label: '测风点名称', key: 'name' },
+    { label: '实时风速', key: 'windSpeed' },
+    { label: '实时风量', key: 'windVolume' },
+    { label: '状态', key: 'status' },
+  ],
+};
+
+// ==================== 多对多 + 一对多 关系类型 ====================
+
+/** 测风点分类索引(一对多的子端) */
+export const CHILD_CATEGORY = 3;
+
+/**
+ * 判断目标分类是否为一对多关系的"子端"
+ * 一对多:煤矿(2)→测风点(3),一个测风点只能属于一个煤矿
+ * 多对多:执法处(1)↔煤矿(2),一个执法处管多个矿,一个矿可被多个执法处管
+ */
+export function isOneToManyChild(category: number): boolean {
+  return category === CHILD_CATEGORY;
+}
+
+/** 拖拽绑定时判定"吸附"的距离阈值(像素) */
+export const BIND_THRESHOLD = 35;
+
+// ==================== 模拟拓扑数据 ====================
+
+export interface TopoNodeData {
+  id: string;
+  name: string;
+  category: number; // categories 索引
+  status?: string; // normal / abnormal / offline
+  windSpeed?: number;
+  windVolume?: number;
+  childCount?: number;
+  onlineRate?: string;
+  onlineRatio?: string;
+  windAbnormal?: number;
+  [key: string]: any;
+}
+
+export interface TopoLinkData {
+  source: string;
+  target: string;
+  label?: string;
+  id?: string; // 关联关系数据库主键,用于解绑
+}
+
+export interface TopologyData {
+  nodes: TopoNodeData[];
+  links: TopoLinkData[];
+}
+
+// ==================== API 响应类型 ====================
+
+/** API -> MineArea 节点 */
+export interface MineAreaNode {
+  id: string;
+  mineCode: string;
+  name: string;
+  level: number; // 1=省局 2=执法处 3=煤矿 4=测风点
+  airVolume: number;
+  windrectId: string;
+  createTime?: string;
+  updateTime?: string;
+  [key: string]: any;
+}
+
+/** API -> MineArea 关联关系 */
+export interface MineAreaRelation {
+  id: string;
+  mineCode: string;
+  parentId: string;
+  childId: string;
+  createTime?: string;
+  updateTime?: string;
+}
+
+/** API -> getMineAreaRelation 返回值 */
+export interface MineAreaApiResponse {
+  mineAreaList: MineAreaNode[];
+  mineAreaRelationList: MineAreaRelation[];
+}
+
+/** level(1-4) 转 category(0-3) */
+const levelToCategory: Record<number, number> = { 1: 0, 2: 1, 3: 2, 4: 3 };
+
+/**
+ * 将 API 响应转换为 TopologyData
+ * mineAreaList -> nodes, mineAreaRelationList -> links
+ */
+export function transformToTopologyData(apiResp: MineAreaApiResponse): TopologyData {
+  const { mineAreaList = [], mineAreaRelationList = [] } = apiResp;
+
+  // 计算每个节点的子节点数
+  const childCountMap: Record<string, number> = {};
+  for (const rel of mineAreaRelationList) {
+    childCountMap[rel.parentId] = (childCountMap[rel.parentId] || 0) + 1;
+  }
+
+  const nodes: TopoNodeData[] = mineAreaList.map((item) => ({
+    id: item.id,
+    name: item.name,
+    category: levelToCategory[item.level] ?? 0,
+    status: 'normal',
+    windVolume: item.airVolume,
+    childCount: childCountMap[item.id] || 0,
+    mineCode: item.mineCode,
+    windrectId: item.windrectId,
+  }));
+
+  const links: TopoLinkData[] = mineAreaRelationList.map((rel) => ({
+    source: rel.parentId,
+    target: rel.childId,
+    id: rel.id,
+  }));
+
+  return { nodes, links };
+}
+
+// ==================== 模拟 API 响应数据(后端未部署时回退) ====================
+
+/** 模拟 getMineAreaRelation 的 API 响应 */
+export const mockMineAreaApiResponse: MineAreaApiResponse = {
+  mineAreaList: [
+    // === 省局 level=1 ===
+    { id: 'province', mineCode: 'M000', name: '陕西省煤矿安全监察局', level: 1, airVolume: 0, windrectId: '' },
+    // === 执法处 level=2 ===
+    { id: 'enf-1', mineCode: 'M001', name: '榆林执法处', level: 2, airVolume: 0, windrectId: '' },
+    { id: 'enf-2', mineCode: 'M002', name: '延安执法处', level: 2, airVolume: 0, windrectId: '' },
+    { id: 'enf-3', mineCode: 'M003', name: '铜川执法处', level: 2, airVolume: 0, windrectId: '' },
+    { id: 'enf-4', mineCode: 'M004', name: '渭南执法处', level: 2, airVolume: 0, windrectId: '' },
+    { id: 'enf-5', mineCode: 'M005', name: '咸阳执法处', level: 2, airVolume: 0, windrectId: '' },
+    // === 煤矿 level=3 ===
+    { id: 'mine-1', mineCode: 'M101', name: '大柳塔煤矿', level: 3, airVolume: 0, windrectId: 'W001' },
+    { id: 'mine-2', mineCode: 'M102', name: '活鸡兔煤矿', level: 3, airVolume: 0, windrectId: 'W002' },
+    { id: 'mine-3', mineCode: 'M103', name: '哈拉沟煤矿', level: 3, airVolume: 0, windrectId: 'W003' },
+    { id: 'mine-4', mineCode: 'M104', name: '榆家梁煤矿', level: 3, airVolume: 0, windrectId: 'W004' },
+    { id: 'mine-5', mineCode: 'M105', name: '三道沟煤矿', level: 3, airVolume: 0, windrectId: 'W005' },
+    { id: 'mine-6', mineCode: 'M106', name: '黄陵一号矿', level: 3, airVolume: 0, windrectId: 'W006' },
+    { id: 'mine-7', mineCode: 'M107', name: '红柳林煤矿', level: 3, airVolume: 0, windrectId: 'W007' },
+    { id: 'mine-8', mineCode: 'M108', name: '凉水井煤矿', level: 3, airVolume: 0, windrectId: 'W008' },
+    // === 测风点 level=4 ===
+    { id: 'point-1', mineCode: 'M001', name: '北翼回风巷', level: 4, airVolume: 1800, windrectId: 'W101' },
+    { id: 'point-2', mineCode: 'M001', name: '南翼进风巷', level: 4, airVolume: 2100, windrectId: 'W102' },
+    { id: 'point-3', mineCode: 'M001', name: '三采区回风巷', level: 4, airVolume: 950, windrectId: 'W103' },
+    { id: 'point-4', mineCode: 'M001', name: '主斜井', level: 4, airVolume: 3200, windrectId: 'W104' },
+    { id: 'point-5', mineCode: 'M002', name: '副立井', level: 4, airVolume: 2800, windrectId: 'W105' },
+    { id: 'point-6', mineCode: 'M002', name: '二盘区回风巷', level: 4, airVolume: 1350, windrectId: 'W106' },
+    { id: 'point-7', mineCode: 'M002', name: '101工作面', level: 4, airVolume: 720, windrectId: 'W107' },
+    { id: 'point-8', mineCode: 'M003', name: '102工作面', level: 4, airVolume: 880, windrectId: 'W108' },
+    { id: 'point-9', mineCode: 'M003', name: '东翼回风大巷', level: 4, airVolume: 1600, windrectId: 'W109' },
+    { id: 'point-10', mineCode: 'M004', name: '西翼进风大巷', level: 4, airVolume: 1500, windrectId: 'W110' },
+  ],
+  mineAreaRelationList: [
+    // 省局→执法处
+    { id: 'rel-1', mineCode: 'M000', parentId: 'province', childId: 'enf-1' },
+    { id: 'rel-2', mineCode: 'M000', parentId: 'province', childId: 'enf-2' },
+    { id: 'rel-3', mineCode: 'M000', parentId: 'province', childId: 'enf-3' },
+    { id: 'rel-4', mineCode: 'M000', parentId: 'province', childId: 'enf-4' },
+    { id: 'rel-5', mineCode: 'M000', parentId: 'province', childId: 'enf-5' },
+    // 执法处→煤矿
+    { id: 'rel-6', mineCode: 'M001', parentId: 'enf-1', childId: 'mine-1' },
+    { id: 'rel-7', mineCode: 'M001', parentId: 'enf-1', childId: 'mine-2' },
+    { id: 'rel-8', mineCode: 'M001', parentId: 'enf-1', childId: 'mine-3' },
+    { id: 'rel-9', mineCode: 'M002', parentId: 'enf-2', childId: 'mine-4' },
+    { id: 'rel-10', mineCode: 'M002', parentId: 'enf-2', childId: 'mine-5' },
+    { id: 'rel-11', mineCode: 'M003', parentId: 'enf-3', childId: 'mine-6' },
+    { id: 'rel-12', mineCode: 'M003', parentId: 'enf-3', childId: 'mine-7' },
+    { id: 'rel-13', mineCode: 'M004', parentId: 'enf-4', childId: 'mine-1' },
+    { id: 'rel-14', mineCode: 'M005', parentId: 'enf-5', childId: 'mine-7' },
+    { id: 'rel-15', mineCode: 'M005', parentId: 'enf-5', childId: 'mine-8' },
+    // 煤矿→测风点
+    { id: 'rel-16', mineCode: 'M101', parentId: 'mine-1', childId: 'point-1' },
+    { id: 'rel-17', mineCode: 'M101', parentId: 'mine-1', childId: 'point-2' },
+    { id: 'rel-18', mineCode: 'M101', parentId: 'mine-1', childId: 'point-3' },
+    { id: 'rel-19', mineCode: 'M101', parentId: 'mine-1', childId: 'point-4' },
+    { id: 'rel-20', mineCode: 'M102', parentId: 'mine-2', childId: 'point-5' },
+    { id: 'rel-21', mineCode: 'M102', parentId: 'mine-2', childId: 'point-6' },
+    { id: 'rel-22', mineCode: 'M102', parentId: 'mine-2', childId: 'point-7' },
+    { id: 'rel-23', mineCode: 'M103', parentId: 'mine-3', childId: 'point-8' },
+    { id: 'rel-24', mineCode: 'M103', parentId: 'mine-3', childId: 'point-9' },
+    { id: 'rel-25', mineCode: 'M104', parentId: 'mine-4', childId: 'point-10' },
+  ],
+};

+ 378 - 0
src/views/analysis/warningAnalysis/windSpeedOverlimit/index.vue

@@ -0,0 +1,378 @@
+<!-- eslint-disable vue/multi-word-component-names -->
+<template>
+  <div class="wind-speed-overlimit">
+    <!-- 顶部统计卡片 -->
+    <div class="board-info">
+      <MiniBoard
+        v-for="(item, index) in boardData"
+        :key="index"
+        type="A"
+        :label="item.label"
+        :value="item.value"
+        layout="label-top"
+        class="board-item"
+      />
+    </div>
+
+    <!-- Tab 切换:实时报警 / 历史报警 -->
+    <Tabs v-model:activeKey="activeTab" type="line" class="common-page-tabs">
+      <TabPane tab="实时报警" key="realtime">
+        <BasicTable @register="registerRealtimeTable" :scroll="{ x: 'max-content' }" :style="{ padding: 0 }">
+          <template #action="{ record }">
+            <div class="action-buttons">
+              <button v-if="record.status !== '2'" @click="openResolveModal(record)" class="action-btn resolved-btn" title="处理">
+                <SvgIcon name="solved" />
+              </button>
+              <button @click="handleViewDetail(record)" class="action-btn" title="详情">
+                <SvgIcon name="details" />
+              </button>
+            </div>
+          </template>
+          <template #resetBefore>
+            <a-button type="default" class="ml-8px" preIcon="mdi:download" @click="onExportXlsTime"> 导出 </a-button>
+          </template>
+          <template #empty>
+            <span class="empty-placeholder">-</span>
+          </template>
+        </BasicTable>
+      </TabPane>
+
+      <TabPane tab="历史报警" key="history">
+        <BasicTable @register="registerHistoryTable" :scroll="{ x: 'max-content' }" :style="{ padding: 0 }">
+          <template #action="{ record }">
+            <div class="action-buttons">
+              <button @click="handleViewDetail(record)" class="action-btn" title="详情">
+                <SvgIcon name="details" />
+              </button>
+            </div>
+          </template>
+          <template #resetBefore>
+            <a-button type="default" class="ml-8px" preIcon="mdi:download" @click="onExportXls"> 导出 </a-button>
+          </template>
+          <template #form-goaf-select>
+            <a-select v-model:value="goafId" :options="goafOptions" placeholder="请选择工作面" @change="historyTable.reload" />
+          </template>
+        </BasicTable>
+      </TabPane>
+
+      <TabPane tab="处理记录" key="handleRecord">
+        <BasicTable @register="registerHandleRecordTable" :scroll="{ x: 'max-content' }" :style="{ padding: 0 }">
+          <template #resetBefore>
+            <a-button type="default" class="ml-8px" preIcon="mdi:download" @click="onExportHandleRecord"> 导出 </a-button>
+          </template>
+        </BasicTable>
+      </TabPane>
+    </Tabs>
+
+    <!-- 下方:趋势图 + 测风点列表 -->
+    <a-row :gutter="12" class="bottom-section">
+      <a-col :span="14">
+        <div class="chart-card">
+          <div class="chart-header">
+            <span class="chart-title">近7日报警趋势</span>
+          </div>
+          <div ref="chartRef" class="trend-chart"></div>
+        </div>
+      </a-col>
+      <a-col :span="10">
+        <div class="point-list-card">
+          <div class="chart-header">
+            <span class="chart-title">测风点监测地点列表</span>
+          </div>
+          <BasicTable @register="registerPointTable" :scroll="{ x: 'max-content', y: 240 }" :pagination="false" />
+        </div>
+      </a-col>
+    </a-row>
+
+    <!-- 处理弹窗 -->
+    <BasicModal @register="registerModal" :width="600" :minHeight="100" centered title="报警处理" @ok="handleResolve">
+      <a-textarea
+        class="ml-10px"
+        :style="{ width: 'calc(100% - 20px)' }"
+        v-model:value="resolveValue"
+        placeholder="请输入处理情况"
+        :rows="4"
+      />
+    </BasicModal>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref, onMounted, onUnmounted, nextTick } from 'vue';
+  import { BasicTable } from '/@/components/Table';
+  import { Tabs, TabPane, message } from 'ant-design-vue';
+  import MiniBoard from '/@/components/Configurable/detail/MiniBoard.vue';
+  import { SvgIcon } from '/@/components/Icon';
+  import { BasicModal } from '/@/components/Modal/index';
+  import { useModal } from '/@/components/Modal';
+  import { useListPage } from '/@/hooks/system/useListPage';
+  import { useInitForm } from '../../common/analysis';
+  import { historicalFormSchema } from '/@/views/monitor/sealedMonitor/monitor.data';
+  import { advancedRoutePush } from '/@/utils';
+  import * as echarts from 'echarts';
+  import { boardData, columns, pointListColumns, handleRecordColumns, schemas } from './windSpeedOverlimit.data';
+  import { getAlarmList, getAlarmHistory, getBoardNum, getPointList, getAlarmTrend, getHandleRecords, resolveAlarm as resolveAlarmApi } from './windSpeedOverlimit.api';
+
+  // 激活的Tab页签
+  const activeTab = ref('realtime');
+
+  // 卡片数据
+  const boardDataRef = ref(boardData.map((item) => ({ ...item })));
+
+  // ====== 实时数据表格 ======
+  const wrappedGetMineData = (params) => {
+    return Promise.all([loadBoardData(params), getAlarmList(params)]).then(([__, res]) => res);
+  };
+
+  const { tableContext: ctxRealtime, onExportXls: onExportXlsTime } = useListPage({
+    tableProps: {
+      immediate: false,
+      columns,
+      api: wrappedGetMineData,
+      formConfig: {
+        autoSearch: true,
+        labelWidth: 120,
+        schemas,
+        showAdvancedButton: false,
+        schemaGroupNames: ['常规查询'],
+      },
+      pagination: true,
+      striped: true,
+      useSearchForm: true,
+      bordered: true,
+      showIndexColumn: false,
+      actionColumn: {
+        width: 90,
+        title: '操作',
+        dataIndex: 'action',
+        slots: { customRender: 'action' },
+        fixed: undefined,
+      },
+    },
+    exportConfig: {
+      url: '/province/wind/exportWindOverlimitAlarm',
+      name: '风速风量超限报警',
+      params: {},
+    },
+  });
+  const [registerRealtimeTable] = ctxRealtime;
+
+  // ====== 历史数据表格 ======
+  const { goafOptions, goafId, hiscode, resolveValue, registerModal, initGoafOptions } = useInitForm();
+
+  const { tableContext, onExportXls } = useListPage({
+    tableProps: {
+      immediate: false,
+      columns,
+      api: async (params) => {
+        await initGoafOptions(params.deptId);
+        if (!goafId.value) {
+          return Promise.resolve([]);
+        }
+        params.goafId = goafId.value;
+        return getAlarmHistory(params);
+      },
+      formConfig: {
+        autoSearch: true,
+        model: { deptId: hiscode },
+        labelWidth: 120,
+        schemas: [
+          {
+            label: '煤矿名称',
+            field: 'deptId',
+            component: 'MineCascader',
+            componentProps: {
+              initFromStore: false,
+              syncToStore: false,
+              changeOnSelect: false,
+            },
+            colProps: { span: 6 },
+            rules: [],
+          },
+          ...historicalFormSchema,
+        ],
+        showAdvancedButton: false,
+        schemaGroupNames: ['常规查询'],
+      },
+      pagination: true,
+      striped: true,
+      useSearchForm: true,
+      bordered: true,
+      showIndexColumn: true,
+    },
+    exportConfig: {
+      url: '/province/wind/exportWindOverlimitAlarmHistory',
+      name: '历史报警',
+      params: { goafId },
+    },
+  });
+  const [registerHistoryTable, historyTable] = tableContext;
+
+  // ====== 测风点监测地点列表 ======
+  const { tableContext: pointCtx } = useListPage({
+    tableProps: {
+      immediate: true,
+      columns: pointListColumns,
+      api: getPointList,
+      pagination: false,
+      striped: true,
+      bordered: true,
+      showIndexColumn: true,
+      showActionColumn: false,
+    },
+  });
+  const [registerPointTable] = pointCtx;
+
+  // ====== 处理记录表格 ======
+  const { tableContext: handleRecordCtx, onExportXls: onExportHandleRecord } = useListPage({
+    tableProps: {
+      immediate: true,
+      columns: handleRecordColumns,
+      api: getHandleRecords,
+      pagination: true,
+      striped: true,
+      bordered: true,
+      showIndexColumn: true,
+      showActionColumn: false,
+    },
+    exportConfig: {
+      url: '/province/wind/exportHandleRecords',
+      name: '处理记录',
+      params: {},
+    },
+  });
+  const [registerHandleRecordTable] = handleRecordCtx;
+
+  // ====== 加载卡片数据 ======
+  async function loadBoardData(params?: any) {
+    try {
+      const result = await getBoardNum(params);
+      boardDataRef.value[0].value = result.windSpeedOver ?? '-';
+      boardDataRef.value[1].value = result.windVolumeLow ?? '-';
+      boardDataRef.value[2].value = result.windVolumeUnclosed ?? '-';
+      boardDataRef.value[3].value = result.todayResolved ?? '-';
+    } catch {
+      // 使用默认值
+    }
+  }
+
+  // ====== 弹窗处理 ======
+  const [, modalCtx] = useModal();
+  const resolveId = ref('');
+
+  function openResolveModal(record) {
+    resolveId.value = record.id;
+    resolveValue.value = '';
+    modalCtx.openModal(true);
+  }
+
+  async function handleResolve() {
+    try {
+      await resolveAlarmApi({ id: resolveId.value, content: resolveValue.value });
+      message.success('处理成功');
+      modalCtx.closeModal();
+    } catch {
+      message.error('处理失败');
+    }
+  }
+
+  // ====== 趋势图 ======
+  const chartRef = ref<HTMLDivElement>();
+  let chartInstance: echarts.ECharts | null = null;
+
+  async function renderChart() {
+    try {
+      const data = await getAlarmTrend({ days: 7 });
+      if (!chartRef.value) return;
+      if (!chartInstance) {
+        chartInstance = echarts.init(chartRef.value);
+      }
+      chartInstance.setOption({
+        tooltip: { trigger: 'axis' },
+        legend: {
+          data: ['风速超限', '风量不足', '风量不闭合'],
+          bottom: 0,
+        },
+        grid: { left: 50, right: 20, top: 20, bottom: 40 },
+        xAxis: {
+          type: 'category',
+          data: data.dates || [],
+          axisLabel: { fontSize: 12 },
+        },
+        yAxis: { type: 'value', minInterval: 1 },
+        series: [
+          { name: '风速超限', type: 'line', data: data.windSpeedOver || [], smooth: true, lineStyle: { width: 2 } },
+          { name: '风量不足', type: 'line', data: data.windVolumeLow || [], smooth: true, lineStyle: { width: 2 } },
+          { name: '风量不闭合', type: 'line', data: data.windVolumeUnclosed || [], smooth: true, lineStyle: { width: 2 } },
+        ],
+        color: ['#f5222d', '#fa8c16', '#1890ff'],
+      });
+    } catch {
+      // 图表数据加载失败时保持空白
+    }
+  }
+
+  // ====== 页面跳转 ======
+  function handleViewDetail(record: any) {
+    advancedRoutePush({
+      path: '/warningAnalysis/connectAnalysis',
+      query: { deptId: record.deptId, filter: 'windSpeed,windVolume' },
+    });
+  }
+
+  function handleResize() {
+    chartInstance?.resize();
+  }
+
+  onMounted(() => {
+    nextTick(() => renderChart());
+    window.addEventListener('resize', handleResize);
+  });
+
+  onUnmounted(() => {
+    window.removeEventListener('resize', handleResize);
+    chartInstance?.dispose();
+  });
+</script>
+
+<style lang="less" scoped>
+  @import url(../../common/board.less);
+
+  .wind-speed-overlimit {
+    height: 100%;
+    overflow-y: auto;
+  }
+
+  .bottom-section {
+    margin-top: 12px;
+    padding: 0 12px 12px;
+
+    .chart-card,
+    .point-list-card {
+      background: #fff;
+      border-radius: 4px;
+      padding: 12px;
+    }
+
+    .chart-header {
+      margin-bottom: 8px;
+      .chart-title {
+        font-size: 14px;
+        font-weight: 600;
+        color: #333;
+      }
+    }
+
+    .trend-chart {
+      width: 100%;
+      height: 280px;
+    }
+  }
+
+  .empty-placeholder {
+    display: block;
+    text-align: center;
+    color: #999;
+    padding: 16px 0;
+  }
+</style>

+ 40 - 0
src/views/analysis/warningAnalysis/windSpeedOverlimit/windSpeedOverlimit.api.ts

@@ -0,0 +1,40 @@
+import { defHttp } from '/@/utils/http/axios';
+
+enum Api {
+  getAlarmList = '/province/wind/getWindOverlimitAlarm',
+  getAlarmHistory = '/province/wind/getWindOverlimitAlarmHistory',
+  getBoardNum = '/province/wind/getWindOverlimitBoardNum',
+  getPointList = '/province/wind/getWindMonitorPointList',
+  getAlarmTrend = '/province/wind/getAlarmTrend',
+  resolveAlarm = '/province/wind/resolveAlarm',
+  getHandleRecords = '/province/wind/getHandleRecords',
+  exportAlarm = '/province/wind/exportWindOverlimitAlarm',
+}
+
+/** 获取顶部统计卡片数据 */
+export const getBoardNum = (params?: any) =>
+  defHttp.post({ url: Api.getBoardNum, params }, { joinParamsToUrl: true });
+
+/** 查询超限报警实时列表 */
+export const getAlarmList = (params?: any) =>
+  defHttp.post({ url: Api.getAlarmList, params }, { joinParamsToUrl: true });
+
+/** 查询超限报警历史数据 */
+export const getAlarmHistory = (params?: any) =>
+  defHttp.post({ url: Api.getAlarmHistory, params }, { joinParamsToUrl: true });
+
+/** 查询测风点监测地点列表 */
+export const getPointList = (params?: any) =>
+  defHttp.post({ url: Api.getPointList, params }, { joinParamsToUrl: true });
+
+/** 查询报警趋势数据 */
+export const getAlarmTrend = (params?: any) =>
+  defHttp.post({ url: Api.getAlarmTrend, params }, { joinParamsToUrl: true });
+
+/** 处理报警 */
+export const resolveAlarm = (params?: any) =>
+  defHttp.post({ url: Api.resolveAlarm, params }, { joinParamsToUrl: true });
+
+/** 查询处理记录 */
+export const getHandleRecords = (params?: any) =>
+  defHttp.post({ url: Api.getHandleRecords, params }, { joinParamsToUrl: true });

+ 232 - 0
src/views/analysis/warningAnalysis/windSpeedOverlimit/windSpeedOverlimit.data.ts

@@ -0,0 +1,232 @@
+import type { BasicColumn, FormSchema } from '/@/components/Table';
+import { h } from 'vue';
+import { StatusColorEnum } from '/@/enums/jeecgEnum';
+
+/** 顶部统计卡片 */
+export const boardData = [
+  { label: '风速超限报警', value: '-' },
+  { label: '风量不足报警', value: '-' },
+  { label: '风量不闭合报警', value: '-' },
+  { label: '今日已处理报警', value: '-' },
+];
+
+/** 报警级别映射 */
+export const alarmLevelMap: Record<string, { label: string; color: string }> = {
+  '1': { label: '一级', color: StatusColorEnum.red },
+  '2': { label: '二级', color: StatusColorEnum.orange },
+  '3': { label: '三级', color: StatusColorEnum.yellow },
+};
+
+/** 报警状态映射 */
+export const alarmStatusMap: Record<string, { label: string; color: string }> = {
+  '0': { label: '未处理', color: StatusColorEnum.red },
+  '1': { label: '处理中', color: StatusColorEnum.orange },
+  '2': { label: '已处理', color: StatusColorEnum.green },
+};
+
+/** 超限报警列表表格列 */
+export const columns: BasicColumn[] = [
+  {
+    title: '煤矿名称',
+    dataIndex: 'mineName',
+    fixed: 'left',
+    width: 180,
+  },
+  {
+    title: '测风点名称',
+    dataIndex: 'pointName',
+    width: 160,
+  },
+  {
+    title: '监测区域',
+    dataIndex: 'monitorArea',
+    width: 120,
+  },
+  {
+    title: '报警类型',
+    dataIndex: 'alarmType',
+    width: 120,
+    customRender: ({ text }) => {
+      const map: Record<string, string> = {
+        windSpeedOver: '风速超限',
+        windVolumeLow: '风量不足',
+        windVolumeUnclosed: '风量不闭合',
+      };
+      return map[text] || text;
+    },
+  },
+  {
+    title: '实测值',
+    dataIndex: 'measuredValue',
+    width: 100,
+  },
+  {
+    title: '标准/阈值',
+    dataIndex: 'thresholdValue',
+    width: 100,
+  },
+  {
+    title: '偏差',
+    dataIndex: 'deviation',
+    width: 90,
+    customRender: ({ text }) => (text != null ? `${text}%` : '-'),
+  },
+  {
+    title: '报警级别',
+    dataIndex: 'alarmLevel',
+    width: 90,
+    customRender: ({ text }) => {
+      const info = alarmLevelMap[text];
+      return info ? h('span', { style: { color: info.color, fontWeight: 'bold' } }, info.label) : text;
+    },
+  },
+  {
+    title: '报警时间',
+    dataIndex: 'alarmTime',
+    width: 170,
+  },
+  {
+    title: '状态',
+    dataIndex: 'status',
+    width: 90,
+    customRender: ({ text }) => {
+      const info = alarmStatusMap[text];
+      return info ? h('span', { style: { color: info.color } }, info.label) : text;
+    },
+  },
+];
+
+/** 测风点监测地点列表表格列 */
+export const pointListColumns: BasicColumn[] = [
+  {
+    title: '测风点编号',
+    dataIndex: 'pointCode',
+    width: 130,
+  },
+  {
+    title: '监测地点名称',
+    dataIndex: 'locationName',
+    width: 200,
+  },
+  {
+    title: '类型',
+    dataIndex: 'pointType',
+    width: 120,
+    customRender: ({ text }) => {
+      const map: Record<string, string> = {
+        totalInlet: '总进风',
+        totalOutlet: '总回风',
+        workingFace: '工作面',
+        districtReturn: '采区回风',
+        headingFace: '掘进面',
+      };
+      return map[text] || text;
+    },
+  },
+  {
+    title: '当前状态',
+    dataIndex: 'currentStatus',
+    width: 100,
+    customRender: ({ text }) => {
+      const color = text === 'normal' ? StatusColorEnum.green : StatusColorEnum.red;
+      const label = text === 'normal' ? '正常' : '异常';
+      return h('span', { style: { color, fontWeight: 'bold' } }, label);
+    },
+  },
+];
+
+/** 查询表单 */
+export const schemas: FormSchema[] = [
+  {
+    label: '煤矿名称',
+    field: 'deptId',
+    component: 'MineCascader',
+    colProps: { span: 6 },
+    rules: [],
+  },
+  {
+    label: '报警类型',
+    field: 'alarmType',
+    component: 'Select',
+    colProps: { span: 6 },
+    componentProps: {
+      options: [
+        { label: '全部', value: '' },
+        { label: '风速超限', value: 'windSpeedOver' },
+        { label: '风量不足', value: 'windVolumeLow' },
+        { label: '风量不闭合', value: 'windVolumeUnclosed' },
+      ],
+    },
+    rules: [],
+  },
+  {
+    label: '报警级别',
+    field: 'alarmLevel',
+    component: 'Select',
+    colProps: { span: 6 },
+    componentProps: {
+      options: [
+        { label: '全部', value: '' },
+        { label: '一级', value: '1' },
+        { label: '二级', value: '2' },
+        { label: '三级', value: '3' },
+      ],
+    },
+    rules: [],
+  },
+  {
+    label: '日期范围',
+    field: 'dateRange',
+    component: 'RangePicker',
+    colProps: { span: 8 },
+    componentProps: {
+      valueFormat: 'YYYY-MM-DD HH:mm:ss',
+      showTime: { format: 'HH:mm:ss' },
+    },
+    rules: [],
+  },
+];
+
+/** 处理记录表格列 */
+export const handleRecordColumns: BasicColumn[] = [
+  {
+    title: '煤矿名称',
+    dataIndex: 'mineName',
+    fixed: 'left',
+    width: 180,
+  },
+  {
+    title: '报警类型',
+    dataIndex: 'alarmType',
+    width: 120,
+    customRender: ({ text }) => {
+      const map: Record<string, string> = {
+        windSpeedOver: '风速超限',
+        windVolumeLow: '风量不足',
+        windVolumeUnclosed: '风量不闭合',
+      };
+      return map[text] || text;
+    },
+  },
+  {
+    title: '测风点名称',
+    dataIndex: 'pointName',
+    width: 160,
+  },
+  {
+    title: '处理人',
+    dataIndex: 'handler',
+    width: 100,
+  },
+  {
+    title: '处理情况',
+    dataIndex: 'handleContent',
+    width: 250,
+    ellipsis: true,
+  },
+  {
+    title: '处理时间',
+    dataIndex: 'handleTime',
+    width: 170,
+  },
+];

+ 229 - 0
src/views/dashboard/HiddenSurface/configurable.data.ts

@@ -0,0 +1,229 @@
+import type { Config } from '@/components/Configurable/types';
+
+/**
+ * 省域地图监测区配置(图3-1)
+ * 省级"一张图"首页 — 集成全省测风监测信息
+ */
+export const testConfigProvinceMonitor: Config[] = [
+  // 1. 省域监测总览(统计指标卡片)
+  {
+    deviceType: 'province_overview',
+    moduleName: '省域监测总览',
+    pageType: 'hidden_surface',
+    moduleData: {
+      header: {
+        show: true,
+        readFrom: '',
+        selector: { show: false, value: '' },
+        slot: { show: true, value: '(总计:${totalMines})' },
+      },
+      background: { show: false, type: 'video', link: '' },
+      layout: {
+        direction: 'row',
+        items: [{ name: 'simple_board', basis: '100%' }],
+      },
+      list: [],
+      board: [],
+      chart: [],
+      gallery: [],
+      gallery_list: [],
+      table: [],
+      complex_list: [],
+      preset: [
+        {
+          readFrom: '',
+          items: [
+            {
+              label: '接入矿井总数',
+              value: '386',
+              to: '/basicinfo/mines-info',
+            },
+            {
+              label: '应接入测风点',
+              value: '1380',
+              to: '/basicinfo/mines-info',
+            },
+            {
+              label: '测风点在线数',
+              value: '1245',
+              to: '/basicinfo/mines-info',
+            },
+            {
+              label: '掉线测风点',
+              value: '135',
+              to: '/basicinfo/mines-info',
+            },
+            {
+              label: '风量异常矿井',
+              value: '23',
+              to: '/warningAnalysis/windSpeedOverlimit',
+            },
+            {
+              label: '疑似隐蔽工作面',
+              value: '8',
+              to: '/warningAnalysis/windSpeedOverlimit',
+            },
+          ],
+        },
+      ],
+    },
+    showStyle: {
+      size: 'width:400px;height:200px;',
+      version: '原版',
+      position: 'top:30px;right:25px;',
+    },
+  },
+  // 2. 执法处判识情况列表
+  {
+    deviceType: 'province_identify',
+    moduleName: '执法处判识情况',
+    pageType: 'hidden_surface',
+    moduleData: {
+      header: {
+        show: true,
+        readFrom: '',
+        selector: { show: false, value: '' },
+        slot: { show: false, value: '' },
+      },
+      background: { show: false, type: 'video', link: '' },
+      layout: { direction: 'column', items: [{ name: 'table', basis: '100%', overflow: true }] },
+      board: [],
+      chart: [],
+      gallery: [],
+      gallery_list: [],
+      table: [
+        {
+          type: 'B',
+          readFrom: 'enforceOffices',
+          columns: [
+            { name: '执法处', prop: 'officeName' },
+            { name: '矿井数', prop: 'mineCount' },
+            { name: '在线率', prop: 'onlineRate' },
+            { name: '风量异常', prop: 'abnormalCount' },
+            { name: '疑似工作面', prop: 'suspectedCount' },
+          ],
+        },
+      ],
+      list: [],
+      complex_list: [],
+      preset: [],
+    },
+    showStyle: {
+      size: 'width:400px;height:600px;',
+      version: '原版',
+      position: 'top:250px;right:25px;',
+    },
+  },
+];
+
+/**
+ * 通风系统图监测区配置(图3-3)
+ * 矿端通风系统图 + 测风点实时数据 + 判识结果展示框
+ */
+export const testConfigVentilationMonitor: Config[] = [
+  // 1. 矿井监测总览
+  {
+    deviceType: 'mine_overview',
+    moduleName: '矿井监测总览',
+    pageType: 'hidden_surface',
+    moduleData: {
+      header: {
+        show: true,
+        readFrom: '',
+        selector: { show: false, value: '' },
+        slot: { show: false, value: '' },
+      },
+      background: { show: false, type: 'video', link: '' },
+      layout: {
+        direction: 'row',
+        items: [{ name: 'simple_board', basis: '100%' }],
+      },
+      list: [],
+      board: [],
+      chart: [],
+      gallery: [],
+      gallery_list: [],
+      table: [],
+      complex_list: [],
+      preset: [
+        {
+          readFrom: '',
+          items: [
+            {
+              label: '总进风量(m³/min)',
+              value: '${totalInletVolume}',
+            },
+            {
+              label: '总回风量(m³/min)',
+              value: '${totalOutletVolume}',
+            },
+            {
+              label: '平均风速(m/s)',
+              value: '${avgWindSpeed}',
+            },
+            {
+              label: '在线测风点',
+              value: '${onlinePointCount}',
+            },
+            {
+              label: '异常测风点',
+              value: '${abnormalPointCount}',
+            },
+          ],
+        },
+      ],
+    },
+    showStyle: {
+      size: 'width:400px;height:180px;',
+      version: '原版',
+      position: 'top:30px;right:25px;',
+    },
+  },
+  {
+    deviceType: 'identify_result',
+    moduleName: '判识结果',
+    pageType: 'hidden_surface',
+    moduleData: {
+      header: {
+        show: true,
+        readFrom: '',
+        selector: { show: false, value: '' },
+        slot: { show: false, value: '' },
+      },
+      background: { show: false, type: 'video', link: '' },
+      layout: {
+        direction: 'column',
+        items: [{ name: 'identify_result', basis: '100%', overflow: true }],
+      },
+      board: [],
+      chart: [],
+      gallery: [],
+      gallery_list: [],
+      table: [],
+      list: [],
+      complex_list: [],
+      preset: [
+        {
+          readFrom: 'identifyData',
+          conclusion: '${conclusion}',
+          items: [
+            { label: '风量较均值偏低', prop: 'volumeLowPercent', unit: '%' },
+            { label: '进回风不闭合差值', prop: 'unclosedDiff', unit: 'm³/min' },
+            { label: '缺失规定测风点数', prop: 'missingCount', unit: '个' },
+            { label: '多测点关联异常数', prop: 'relatedAbnormal', unit: '个' },
+          ],
+          suggestions: [
+            '请现场核查疑似隐蔽巷道的通风设施状态',
+            '建议对该区域风量不闭合点进行复测',
+            '缺失测风点应尽快补装并接入系统',
+          ],
+        },
+      ],
+    },
+    showStyle: {
+      size: 'width:400px;height:400px;',
+      version: '原版',
+      position: 'top:220px;right:25px;',
+    },
+  },
+];

+ 102 - 0
src/views/dashboard/HiddenSurface/hiddenSurface.api.ts

@@ -0,0 +1,102 @@
+// 模拟数据 — 首页所有接口返回模拟数据用于展示
+// 后端接口完成后替换为真实的 defHttp 请求
+
+// ========== 省域地图监测区(图3-1)==========
+
+/** 省域监测总览统计卡片 */
+export const getProvinceOverview = (params?: any) =>
+  Promise.resolve({
+    connectedMines: 386,
+    onlinePoints: 1245,
+    abnormalMines: 23,
+    suspectedWorkface: 8,
+    offlinePoints: 135,
+    totalMines: 386,
+  });
+
+/** 联网状态 — 应接入/在线/中断/未接入 统计 */
+export const getProvinceAccessCount = (params?: any) =>
+  Promise.resolve([
+    { name: '执法一处', yjNum: 84, zxNum: 80, zdNum: 2, wjNum: 2, deptId: '1' },
+    { name: '执法二处', yjNum: 72, zxNum: 68, zdNum: 3, wjNum: 1, deptId: '2' },
+    { name: '执法三处', yjNum: 65, zxNum: 60, zdNum: 3, wjNum: 2, deptId: '3' },
+    { name: '执法四处', yjNum: 78, zxNum: 75, zdNum: 1, wjNum: 2, deptId: '4' },
+    { name: '执法五处', yjNum: 56, zxNum: 52, zdNum: 2, wjNum: 2, deptId: '5' },
+  ]);
+
+/** 当日报警情况统计 */
+export const getProvinceAlarmNum = (params?: any) =>
+  Promise.resolve({
+    windSpeedOverlimit: 12,
+    volumeLow: 8,
+    unclosed: 5,
+    missingPoints: 3,
+    resolved: 6,
+  });
+
+/** 执法处判识情况列表 */
+export const getEnforceOfficeList = (params?: any) =>
+  Promise.resolve([
+    { officeName: '执法一处', mineCount: 84, onlineRate: '95%', abnormalCount: 5, suspectedCount: 2 },
+    { officeName: '执法二处', mineCount: 72, onlineRate: '94%', abnormalCount: 4, suspectedCount: 1 },
+    { officeName: '执法三处', mineCount: 65, onlineRate: '92%', abnormalCount: 7, suspectedCount: 3 },
+    { officeName: '执法四处', mineCount: 78, onlineRate: '96%', abnormalCount: 3, suspectedCount: 1 },
+    { officeName: '执法五处', mineCount: 56, onlineRate: '93%', abnormalCount: 4, suspectedCount: 1 },
+  ]);
+
+// ========== 通风系统图监测区(图3-3)==========
+
+/** 通风系统状态总览 */
+export const getVentilationStatus = (params?: any) =>
+  Promise.resolve({
+    totalInletVolume: 485,
+    totalOutletVolume: 472,
+    avgWindSpeed: 4.2,
+    onlinePointCount: 22,
+    abnormalPointCount: 3,
+  });
+
+/** 测风点实时数据列表 */
+export const getPointRealtimeData = (params?: any) =>
+  Promise.resolve({
+    records: [
+      { pointCode: 'FC001', pointName: '主井进风', monitorArea: '主井区', windSpeed: 5.2, windVolume: 2850, status: 'normal' },
+      { pointCode: 'FC002', pointName: '副井进风', monitorArea: '副井区', windSpeed: 4.8, windVolume: 2630, status: 'normal' },
+      { pointCode: 'FC003', pointName: '北翼回风', monitorArea: '北翼采区', windSpeed: 6.8, windVolume: 3720, status: 'abnormal' },
+      { pointCode: 'FC004', pointName: '南翼回风', monitorArea: '南翼采区', windSpeed: 4.1, windVolume: 2250, status: 'normal' },
+      { pointCode: 'FC005', pointName: '1201工作面', monitorArea: '1201采面', windSpeed: 3.5, windVolume: 1920, status: 'normal' },
+      { pointCode: 'FC006', pointName: '1202工作面', monitorArea: '1202采面', windSpeed: 0, windVolume: 0, status: 'missing' },
+    ],
+  });
+
+/** 判识结果 */
+export const getIdentifyResult = (params?: any) =>
+  Promise.resolve({
+    conclusion: '疑似',
+    volumeLowPercent: 15,
+    unclosedDiff: 120,
+    missingCount: 2,
+    relatedAbnormal: 1,
+  });
+
+/** 矿端报警信息列表 */
+export const getMineAlarmList = (params?: any) =>
+  Promise.resolve({
+    records: [
+      { alarmType: '风速超限', pointName: 'FC003', alarmDesc: '实测6.8m/s超阈值6.0m/s', alarmLevel: '二级', alarmTime: '2025-07-28 14:30' },
+      { alarmType: '风量不足', pointName: 'FC005', alarmDesc: '实测1920m³/min低于标准2400m³/min', alarmLevel: '三级', alarmTime: '2025-07-28 13:15' },
+      { alarmType: '测风点缺失', pointName: 'FC006', alarmDesc: '注册测风点在上传数据中缺失', alarmLevel: '一级', alarmTime: '2025-07-28 08:00' },
+      {
+        alarmType: '风量不闭合',
+        pointName: '北翼采区',
+        alarmDesc: '进回风差值3720-2850=870m³/min超阈值10%',
+        alarmLevel: '二级',
+        alarmTime: '2025-07-28 11:45',
+      },
+      { alarmType: '风速超限', pointName: 'FC003', alarmDesc: '实测6.5m/s超阈值6.0m/s', alarmLevel: '三级', alarmTime: '2025-07-27 22:10' },
+    ],
+  });
+
+/** 建议措施 */
+export const getSuggestions = (params?: any) =>
+  Promise.resolve(['请现场核查疑似隐蔽巷道的通风设施状态', '建议对该区域风量不闭合点进行复测', '缺失测风点应尽快补装并接入系统']);

+ 308 - 0
src/views/dashboard/HiddenSurface/index.vue

@@ -0,0 +1,308 @@
+<!-- eslint-disable vue/multi-word-component-names -->
+<template>
+  <div class="hidden-surface-home">
+    <SystemSelect
+      :class="{
+        'custom-system-select': true,
+        'custom-system-select-mine': appStore.getSimpleMapParams.isLeaf,
+      }"
+    />
+
+    <template v-if="appStore.getSimpleMapParams.isLeaf">
+      <div class="company-title">
+        <div class="company-title-content">
+          <span class="title-text">
+            {{ departName }}
+          </span>
+        </div>
+      </div>
+    </template>
+    <template v-else>
+      <div class="button-border"></div>
+    </template>
+    <!-- 渲染所有模块 -->
+    <ModulePrimary
+      v-for="cfg in cfgs"
+      :key="cfg.deviceType"
+      :show-style="cfg.showStyle"
+      :module-data="cfg.moduleData"
+      :module-name="cfg.moduleName"
+      :device-type="cfg.deviceType"
+      :data="data"
+      :visible="true"
+    />
+  </div>
+</template>
+<script lang="ts" setup>
+  import { computed, onUnmounted, ref, watch, shallowRef } from 'vue';
+  import { useInitConfigs, useInitPage } from '@/components/Configurable/hooks/useInit';
+  import { testConfigProvinceMonitor, testConfigVentilationMonitor } from './configurable.data';
+  import {
+    getProvinceOverview,
+    getProvinceAccessCount,
+    getProvinceAlarmNum,
+    getEnforceOfficeList,
+    getVentilationStatus,
+    getPointRealtimeData,
+    getIdentifyResult,
+    getMineAlarmList,
+  } from './hiddenSurface.api';
+  import ModulePrimary from '/@/components/Configurable/ModulePrimary.vue';
+  import { useGlobSetting } from '/@/hooks/setting';
+  import { useAppStore } from '/@/store/modules/app';
+  import { useMineDepartmentStore } from '/@/store/modules/mine';
+  import SystemSelect from '/@/layouts/default/feature/SystemSelect.vue';
+
+  const { title = '隐蔽工作面智能判识分析系统' } = useGlobSetting();
+  const { data, updateData } = useInitPage(title);
+  const appStore = useAppStore();
+  const mineStore = useMineDepartmentStore();
+  const departName = ref('');
+
+  const cfgs = computed(() => configs.value);
+  const { configs } = useInitConfigs();
+
+  // 省域级数据汇总
+  const provinceData = shallowRef({
+    connectedMines: 0, // 接入矿井总数
+    onlinePoints: 0, // 测风点在线数
+    abnormalMines: 0, // 风量异常矿井
+    suspectedWorkface: 0, // 疑似隐蔽工作面
+    offlinePoints: 0, // 数据掉线点
+    accessTotal: {
+      // 联网汇总
+      yjTotal: 0,
+      zxTotal: 0,
+      zdTotal: 0,
+      wjTotal: 0,
+    },
+    accessDetails: null, // 联网详情(各执法处)
+    enforceOffices: null, // 执法处判识情况列表
+    totalMines: 0, // 总计(header slot显示)
+  });
+
+  // 矿端数据汇总
+  const mineData = shallowRef({
+    totalInletVolume: 0,
+    totalOutletVolume: 0,
+    avgWindSpeed: 0,
+    onlinePointCount: 0,
+    abnormalPointCount: 0,
+    pointRecords: null, // 测风点实时数据
+    identifyData: {
+      // 判识结果数据
+      conclusion: '正常',
+      volumeLowPercent: 0,
+      unclosedDiff: 0,
+      missingCount: 0,
+      relatedAbnormal: 0,
+    },
+    alarmRecords: null, // 报警信息列表
+  });
+
+  async function fetchHomeData() {
+    try {
+      const [overview, accessResult, alarmResult, officeList] = await Promise.all([
+        getProvinceOverview(appStore.simpleMapParams)
+          .then((r) => r)
+          .catch((e) => {
+            console.error('省域总览数据获取失败:', e);
+            return null;
+          }),
+
+        getProvinceAccessCount(appStore.simpleMapParams)
+          .then((r) => {
+            let yjTotal = 0,
+              zxTotal = 0,
+              zdTotal = 0,
+              wjTotal = 0;
+            if (Array.isArray(r)) {
+              r.forEach((item) => {
+                yjTotal += Number(item.yjNum) || 0;
+                zxTotal += Number(item.zxNum) || 0;
+                zdTotal += Number(item.zdNum) || 0;
+                wjTotal += Number(item.wjNum) || 0;
+              });
+            }
+            return { accessDetails: r, accessTotal: { yjTotal, zxTotal, zdTotal, wjTotal } };
+          })
+          .catch((e) => {
+            console.error('联网状态数据获取失败:', e);
+            return { accessDetails: null, accessTotal: { yjTotal: 0, zxTotal: 0, zdTotal: 0, wjTotal: 0 } };
+          }),
+
+        getProvinceAlarmNum(appStore.simpleMapParams)
+          .then((r) => r)
+          .catch((e) => {
+            console.error('报警统计获取失败:', e);
+            return null;
+          }),
+
+        getEnforceOfficeList(appStore.simpleMapParams)
+          .then((r) => r)
+          .catch((e) => {
+            console.error('执法处列表获取失败:', e);
+            return null;
+          }),
+      ]);
+
+      provinceData.value = {
+        connectedMines: overview?.connectedMines ?? 0,
+        onlinePoints: overview?.onlinePoints ?? 0,
+        abnormalMines: overview?.abnormalMines ?? 0,
+        suspectedWorkface: overview?.suspectedWorkface ?? 0,
+        offlinePoints: overview?.offlinePoints ?? 0,
+        totalMines: overview?.connectedMines ?? 0,
+        ...(alarmResult ?? {}),
+        ...accessResult,
+        enforceOffices: officeList,
+      };
+      updateData(provinceData.value);
+    } catch (error) {
+      console.error('省域数据获取失败:', error);
+    }
+  }
+
+  async function fetchLeafData() {
+    try {
+      const [ventilationStatus, pointData, identifyResult, alarmList, suggestData] = await Promise.all([
+        getVentilationStatus(appStore.simpleMapParams)
+          .then((r) => r || {})
+          .catch((e) => {
+            console.error('通风系统状态获取失败:', e);
+            return {};
+          }),
+
+        getPointRealtimeData(appStore.simpleMapParams)
+          .then((r) => r)
+          .catch((e) => {
+            console.error('测风点实时数据获取失败:', e);
+            return null;
+          }),
+
+        getIdentifyResult(appStore.simpleMapParams)
+          .then((r) => r || { conclusion: '正常', volumeLowPercent: 0, unclosedDiff: 0, missingCount: 0, relatedAbnormal: 0 })
+          .catch((e) => {
+            console.error('判识结果获取失败:', e);
+            return { conclusion: '正常', volumeLowPercent: 0, unclosedDiff: 0, missingCount: 0, relatedAbnormal: 0 };
+          }),
+
+        getMineAlarmList(appStore.simpleMapParams)
+          .then((r) => r)
+          .catch((e) => {
+            console.error('报警列表获取失败:', e);
+            return null;
+          }),
+      ]);
+
+      mineData.value = {
+        totalInletVolume: ventilationStatus?.totalInletVolume ?? 0,
+        totalOutletVolume: ventilationStatus?.totalOutletVolume ?? 0,
+        avgWindSpeed: ventilationStatus?.avgWindSpeed ?? 0,
+        onlinePointCount: ventilationStatus?.onlinePointCount ?? 0,
+        abnormalPointCount: ventilationStatus?.abnormalPointCount ?? 0,
+        pointRecords: pointData?.records ?? pointData,
+        identifyData: identifyResult || { conclusion: '正常', volumeLowPercent: 0, unclosedDiff: 0, missingCount: 0, relatedAbnormal: 0 },
+        alarmRecords: alarmList?.records ?? alarmList,
+      };
+      updateData(mineData.value);
+    } catch (error) {
+      console.error('矿端数据获取失败:', error);
+    }
+  }
+
+  watch(
+    () => appStore.getSimpleMapParams,
+    (params) => {
+      if (params.isLeaf) {
+        fetchLeafData();
+        configs.value = testConfigVentilationMonitor;
+
+        const depart = mineStore.findDepartById(params.deptId, mineStore.getDepartTree);
+        if (depart) {
+          departName.value = depart.departName;
+        }
+      } else {
+        fetchHomeData();
+        configs.value = testConfigProvinceMonitor;
+      }
+    },
+    {
+      immediate: true,
+    }
+  );
+
+  onUnmounted(() => {});
+</script>
+<style lang="less" scoped>
+  @import '/@/design/theme.less';
+
+  @font-face {
+    font-family: 'douyuFont';
+    src: url('/@/assets/font/douyuFont.otf');
+  }
+
+  .hidden-surface-home {
+    position: fixed;
+    width: 100%;
+    height: 100%;
+    color: @white;
+    background-image: linear-gradient(90deg, transparent 0%, transparent 70%, @map-bg 80%, @map-bg 100%);
+    background-repeat: no-repeat;
+    background-size: 100% 100%;
+    z-index: @layout-basic-z-index;
+    pointer-events: none;
+
+    .custom-system-select {
+      z-index: @layout-header-fixed-z-index;
+      position: absolute;
+      top: 25px;
+      left: 25px;
+      pointer-events: all;
+    }
+  }
+  .custom-system-select-mine {
+    top: 70px !important;
+  }
+  .button-border {
+    position: absolute;
+    width: 405px;
+    height: 60px;
+    left: 22px;
+    top: 15px;
+    border: 2px dashed #0070ff;
+    border-radius: 5px;
+  }
+  .company-title {
+    position: absolute;
+    width: 405px;
+    height: 110px;
+    left: 22px;
+    top: 15px;
+    border: 2px dashed #0070ff;
+    border-radius: 5px;
+
+    .company-title-content {
+      width: 100%;
+      height: 52px;
+      background-image: url(/src/assets/images/sealedGoaf/title-bg.png);
+      background-repeat: no-repeat;
+      background-size: 100% 100%;
+    }
+
+    .title-text {
+      display: block;
+      width: 100%;
+      overflow: hidden;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+      text-align: center;
+      line-height: 52px;
+    }
+  }
+
+  :deep(.ant-select-selection-item) {
+    display: flex;
+    align-items: center;
+  }
+</style>

+ 21 - 0
src/views/monitor/enforceMonitor/enforceMonitor.api.ts

@@ -0,0 +1,21 @@
+import { defHttp } from '/@/utils/http/axios';
+
+enum Api {
+  getBoardNum = '/enforce/monitor/getBoardNum',
+  getRealtimeList = '/enforce/monitor/getRealtimeList',
+  getHistoryList = '/enforce/monitor/getHistoryList',
+  exportRealtime = '/enforce/monitor/exportRealtime',
+  exportHistory = '/enforce/monitor/exportHistory',
+}
+
+/** 获取顶部统计卡片数据 */
+export const getBoardNum = (params?: any) =>
+  defHttp.post({ url: Api.getBoardNum, params }, { joinParamsToUrl: true });
+
+/** 查询实时监测列表 */
+export const getRealtimeList = (params?: any) =>
+  defHttp.post({ url: Api.getRealtimeList, params }, { joinParamsToUrl: true });
+
+/** 查询历史监测列表 */
+export const getHistoryList = (params?: any) =>
+  defHttp.post({ url: Api.getHistoryList, params }, { joinParamsToUrl: true });

+ 172 - 0
src/views/monitor/enforceMonitor/enforceMonitor.data.ts

@@ -0,0 +1,172 @@
+import type { BasicColumn, FormSchema } from '/@/components/Table';
+
+/** 顶部统计卡片 */
+export const boardData = [
+  { label: '辖区矿井数', value: '-' },
+  { label: '测风点在线率', value: '-' },
+  { label: '风量异常矿井', value: '-' },
+  { label: '疑似隐蔽工作面', value: '-' },
+];
+
+/** 生产状态映射 */
+export const productionStatusMap: Record<string, { label: string; color: string }> = {
+  normal: { label: '正常生产', color: '#52c41a' },
+ 停产: { label: '停产', color: '#faad14' },
+ 建设: { label: '建设', color: '#1890ff' },
+};
+
+/** 风量闭合映射 */
+export const windClosedMap: Record<string, { label: string; color: string }> = {
+  closed: { label: '闭合', color: '#52c41a' },
+  deviation: { label: '偏差', color: '#fa8c16' },
+  unclosed: { label: '不闭合', color: '#f5222d' },
+};
+
+/** 判识结果映射 */
+export const judgeResultMap: Record<string, { label: string; color: string }> = {
+  suspected: { label: '疑似', color: '#f5222d' },
+  normal: { label: '正常', color: '#52c41a' },
+  attention: { label: '关注', color: '#fa8c16' },
+  insufficient: { label: '数据不足', color: '#999' },
+};
+
+/** 实时监测数据表格列 —— 对应文档表3-1 */
+export const columns: BasicColumn[] = [
+  {
+    title: '煤矿编号',
+    dataIndex: 'mineCode',
+    fixed: 'left',
+    width: 130,
+  },
+  {
+    title: '煤矿名称/简称',
+    dataIndex: 'mineName',
+    fixed: 'left',
+    width: 180,
+  },
+  {
+    title: '所属区域',
+    dataIndex: 'areaName',
+    width: 120,
+  },
+  {
+    title: '生产状态',
+    dataIndex: 'productionStatus',
+    width: 100,
+    customRender: ({ text }) => {
+      const info = productionStatusMap[text];
+      if (!info) return text;
+      return `<span style="color:${info.color};font-weight:bold;">${info.label}</span>`;
+    },
+  },
+  {
+    title: '测风点数',
+    dataIndex: 'pointCount',
+    width: 90,
+    align: 'center',
+  },
+  {
+    title: '在线/应接',
+    dataIndex: 'onlineRatio',
+    width: 100,
+    align: 'center',
+    customRender: ({ text }) => text ?? '-',
+  },
+  {
+    title: '风量异常',
+    dataIndex: 'windAbnormal',
+    width: 90,
+    align: 'center',
+    customRender: ({ text }) => {
+      if (!text || text === '0') return text ?? '0';
+      return `<span style="color:#f5222d;font-weight:bold;">${text}</span>`;
+    },
+  },
+  {
+    title: '测风点缺失',
+    dataIndex: 'pointMissing',
+    width: 100,
+    align: 'center',
+    customRender: ({ text }) => {
+      if (!text || text === '0') return text ?? '0';
+      return `<span style="color:#f5222d;font-weight:bold;">${text}</span>`;
+    },
+  },
+  {
+    title: '风量闭合',
+    dataIndex: 'windClosed',
+    width: 100,
+    align: 'center',
+    customRender: ({ text }) => {
+      const info = windClosedMap[text];
+      if (!info) return text;
+      return `<span style="color:${info.color};font-weight:bold;">${info.label}</span>`;
+    },
+  },
+  {
+    title: '判识结果',
+    dataIndex: 'judgeResult',
+    width: 100,
+    align: 'center',
+    customRender: ({ text }) => {
+      const info = judgeResultMap[text];
+      if (!info) return text;
+      return `<span style="color:${info.color};font-weight:bold;">${info.label}</span>`;
+    },
+  },
+  {
+    title: '操作',
+    dataIndex: 'action',
+    width: 130,
+    fixed: 'right',
+  },
+];
+
+/** 查询表单 */
+export const schemas: FormSchema[] = [
+  {
+    label: '煤矿名称',
+    field: 'mineName',
+    component: 'Input',
+    colProps: { span: 6 },
+    rules: [],
+  },
+  {
+    label: '所属区域',
+    field: 'areaName',
+    component: 'Input',
+    colProps: { span: 6 },
+    rules: [],
+  },
+  {
+    label: '生产状态',
+    field: 'productionStatus',
+    component: 'Select',
+    colProps: { span: 6 },
+    componentProps: {
+      options: [
+        { label: '全部', value: '' },
+        { label: '正常生产', value: 'normal' },
+        { label: '停产', value: '停产' },
+        { label: '建设', value: '建设' },
+      ],
+    },
+    rules: [],
+  },
+  {
+    label: '判识结果',
+    field: 'judgeResult',
+    component: 'Select',
+    colProps: { span: 6 },
+    componentProps: {
+      options: [
+        { label: '全部', value: '' },
+        { label: '疑似', value: 'suspected' },
+        { label: '正常', value: 'normal' },
+        { label: '关注', value: 'attention' },
+        { label: '数据不足', value: 'insufficient' },
+      ],
+    },
+    rules: [],
+  },
+];

+ 29 - 0
src/views/monitor/enforceMonitor/hooks/form.ts

@@ -0,0 +1,29 @@
+import { get, last } from 'lodash-es';
+import { useRoute } from 'vue-router';
+import { useMineDepartmentStore } from '/@/store/modules/mine';
+
+export function useInitForm() {
+  const mineStore = useMineDepartmentStore();
+  const route = useRoute();
+
+  // 如果generation为true,表示是模板路由直接使用,不会有/:id后缀
+  const departId = route.meta.generation ? undefined : (last(route.path.split('/')) as string);
+  // 获取到路由携带的id后进行过滤
+  const dep = mineStore.findDepartById(departId);
+  // 获取到query携带的矿id,如有就默认选中该矿
+  const rawcode = route.query.deptId as string;
+  // 默认填充的矿码
+  const hiscode = rawcode
+    ? rawcode
+    : get(
+        mineStore.findDepart((e) => e.isLeaf, dep || mineStore.getRoot),
+        'id'
+      );
+
+  return {
+    departId,
+    rawcode,
+    hiscode,
+    route,
+  };
+}

+ 220 - 0
src/views/monitor/enforceMonitor/index.vue

@@ -0,0 +1,220 @@
+<!-- eslint-disable vue/multi-word-component-names -->
+<template>
+  <div class="enforce-monitor">
+    <!-- 顶部统计卡片 -->
+    <div class="board-info">
+      <MiniBoard
+        v-for="(item, index) in boardData"
+        :key="index"
+        type="A"
+        :label="item.label"
+        :value="item.value"
+        layout="label-top"
+        class="board-item"
+      />
+    </div>
+
+    <!-- Tab 切换 -->
+    <Tabs v-model:activeKey="activeTab" type="line" class="common-page-tabs">
+      <TabPane tab="实时监测" key="realtime">
+        <BasicTable @register="registerRealtimeTable" :scroll="{ x: 'max-content' }" :style="{ padding: 0 }">
+          <template #action="{ record }">
+            <div class="action-buttons">
+              <button @click="handleViewDetail(record)" class="action-btn" title="详情">
+                <SvgIcon name="details" />
+              </button>
+              <button @click="handleViewChart(record)" class="action-btn" title="图表">
+                <SvgIcon name="chart" />
+              </button>
+            </div>
+          </template>
+          <template #resetBefore>
+            <a-button type="default" class="ml-8px" preIcon="mdi:download" @click="onExportXlsTime"> 导出 </a-button>
+          </template>
+          <template #empty>
+            <span class="empty-placeholder">-</span>
+          </template>
+        </BasicTable>
+      </TabPane>
+
+      <TabPane tab="历史数据" key="history">
+        <BasicTable @register="registerHistoryTable" :scroll="{ x: 'max-content' }" :style="{ padding: 0 }">
+          <template #action="{ record }">
+            <div class="action-buttons">
+              <button @click="handleViewDetail(record)" class="action-btn" title="详情">
+                <SvgIcon name="details" />
+              </button>
+              <button @click="handleViewChart(record)" class="action-btn" title="图表">
+                <SvgIcon name="chart" />
+              </button>
+            </div>
+          </template>
+          <template #resetBefore>
+            <a-button type="default" class="ml-8px" preIcon="mdi:download" @click="onExportXls"> 导出 </a-button>
+          </template>
+        </BasicTable>
+      </TabPane>
+    </Tabs>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { ref } from 'vue';
+  import { BasicTable } from '/@/components/Table';
+  import { Tabs, TabPane } from 'ant-design-vue';
+  import MiniBoard from '/@/components/Configurable/detail/MiniBoard.vue';
+  import { SvgIcon } from '/@/components/Icon';
+  import { useListPage } from '/@/hooks/system/useListPage';
+  import { advancedRoutePush } from '/@/utils';
+  import { boardData, columns, schemas } from './enforceMonitor.data';
+  import { getBoardNum, getRealtimeList, getHistoryList } from './enforceMonitor.api';
+  import { useInitForm } from './hooks/form';
+
+  // 路由参数初始化(迁移自密闭监测)
+  const { departId, rawcode, hiscode } = useInitForm();
+
+  // 激活的Tab页签
+  const activeTab = ref('realtime');
+
+  // 卡片数据
+  const boardDataRef = ref(boardData.map((item) => ({ ...item })));
+
+  // ====== 加载卡片数据 ======
+  async function loadBoardData(params?: any) {
+    try {
+      const result = await getBoardNum(params);
+      boardDataRef.value[0].value = result.mineTotal ?? '-';
+      boardDataRef.value[1].value = result.onlineRate ?? '-';
+      boardDataRef.value[2].value = result.windAbnormalMine ?? '-';
+      boardDataRef.value[3].value = result.suspectedGoaf ?? '-';
+    } catch {
+      // 使用默认值
+    }
+  }
+
+  // 包装实时列表请求:先加载卡片数据
+  const wrappedGetRealtime = (params) => {
+    return Promise.all([loadBoardData(params), getRealtimeList(params)]).then(([__, res]) => res);
+  };
+
+  // ====== 实时监测表格 ======
+  const { tableContext: ctxRealtime, onExportXls: onExportXlsTime } = useListPage({
+    tableProps: {
+      immediate: false,
+      columns,
+      api: wrappedGetRealtime,
+      formConfig: {
+        autoSearch: true,
+        labelWidth: 120,
+        model: { deptId: rawcode },
+        schemas: [
+          {
+            field: 'deptId',
+            label: '煤矿名称',
+            component: 'MineCascader',
+            colProps: { span: 6 },
+            required: true,
+            componentProps: {
+              initFromStore: false,
+              rootId: departId,
+            },
+          },
+          ...schemas,
+        ],
+        showAdvancedButton: false,
+        schemaGroupNames: ['常规查询'],
+      },
+      pagination: true,
+      striped: true,
+      useSearchForm: true,
+      bordered: true,
+      showIndexColumn: false,
+      actionColumn: {
+        width: 110,
+        title: '操作',
+        dataIndex: 'action',
+        slots: { customRender: 'action' },
+        fixed: 'right',
+      },
+    },
+    exportConfig: {
+      url: '/enforce/monitor/exportRealtime',
+      name: '执法处辖区监测实时数据',
+      params: {},
+    },
+  });
+  const [registerRealtimeTable] = ctxRealtime;
+
+  // ====== 历史数据表格 ======
+  const { tableContext, onExportXls } = useListPage({
+    tableProps: {
+      immediate: false,
+      columns,
+      api: getHistoryList,
+      formConfig: {
+        autoSearch: true,
+        labelWidth: 120,
+        model: { deptId: hiscode },
+        schemas: [
+          {
+            field: 'deptId',
+            label: '煤矿名称',
+            component: 'MineCascader',
+            colProps: { span: 6 },
+            required: true,
+            componentProps: {
+              initFromStore: false,
+              syncToStore: false,
+              rootId: departId,
+            },
+          },
+          ...schemas,
+        ],
+        showAdvancedButton: false,
+        schemaGroupNames: ['常规查询'],
+      },
+      pagination: true,
+      striped: true,
+      useSearchForm: true,
+      bordered: true,
+      showIndexColumn: true,
+    },
+    exportConfig: {
+      url: '/enforce/monitor/exportHistory',
+      name: '执法处辖区监测历史数据',
+      params: {},
+    },
+  });
+  const [registerHistoryTable] = tableContext;
+
+  // ====== 跳转 ======
+  function handleViewDetail(record: any) {
+    advancedRoutePush({
+      path: '/enforce/${id}',
+      query: { deptId: record.deptId },
+    });
+  }
+
+  function handleViewChart(record: any) {
+    advancedRoutePush({
+      path: '/warningAnalysis/connectAnalysis',
+      query: { deptId: record.deptId, filter: 'windSpeed,windVolume' },
+    });
+  }
+</script>
+
+<style lang="less" scoped>
+  @import url(../../analysis/common/board.less);
+
+  .enforce-monitor {
+    height: 100%;
+    overflow-y: auto;
+  }
+
+  .empty-placeholder {
+    display: block;
+    text-align: center;
+    color: #999;
+    padding: 16px 0;
+  }
+</style>