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

[Feat 0000] 瓦斯传感器相关功能及页面开发

houzekong 1 день назад
Родитель
Сommit
1379b9aaf6

+ 31 - 0
public/js/global.js

@@ -424,6 +424,37 @@ const __STATIC_ROUTES__ = [
     },
     name: 'sensor',
   },
+  {
+    redirect: undefined,
+    path: '/gas',
+    component: '/monitor/gasMonitor/index',
+    children: [
+      {
+        path: '/gas/${id}',
+        component: '/monitor/gasMonitor/index',
+        meta: {
+          keepAlive: true,
+          internalOrExternal: false,
+          icon: '',
+          componentName: 'index',
+          title: '${departName}',
+          hideMenu: true,
+          generation: false,
+          // orderNo: '${departOrder}',
+        },
+        name: 'gas-${id}',
+      }
+    ],
+    meta: {
+      keepAlive: false,
+      internalOrExternal: false,
+      icon: 'menu-analysis|svg',
+      componentName: 'index',
+      title: '瓦斯监测',
+      generation: true,
+    },
+    name: 'gas',
+  },
   // {
   //   redirect: undefined,
   //   path: '/smart-analysis',

+ 162 - 17
src/components/Configurable/preset/IdentifyResult.vue

@@ -6,13 +6,27 @@
       <div class="conclusion-value font-bold" :class="conclusionClass">{{ conclusionText }}</div>
     </div>
 
-    <!-- 判定依据列表 -->
-    <div v-if="basisItems.length > 0" class="basis-section">
+    <!-- 判定依据:按四个步骤分组(配风计划 / 测风报表 / 传感器风量 / 瓦斯校验情况) -->
+    <div v-if="groups.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">{{ item.value }}</span>
+      <div class="step-list flex flex-col gap-1.5">
+        <div v-for="group in groups" :key="group.key" class="step-item">
+          <!-- 步骤头:序号 + 名称 + 状态徽标,整行点击折叠/展开 -->
+          <div class="step-header flex items-center" @click="toggleGroup(group.key)">
+            <span class="step-no">{{ group.stepNo }}</span>
+            <span class="step-title">{{ group.title }}</span>
+            <span v-if="group.statusText" class="step-status" :class="group.statusClass">{{ group.statusText }}</span>
+            <RightOutlined v-if="isCollapsed(group.key)" class="step-arrow" />
+            <DownOutlined v-else class="step-arrow" />
+          </div>
+
+          <!-- 步骤明细:未取到值时展示占位符 '-'(不拼接单位) -->
+          <div v-show="!isCollapsed(group.key)" class="step-items flex flex-col">
+            <div v-for="(item, idx) in group.items" :key="group.key + '-item-' + idx" class="basis-item flex justify-between items-center">
+              <span class="basis-label" :title="item.label">{{ item.label }}</span>
+              <span class="basis-value font-semibold" :class="item.levelClass">{{ item.text }}</span>
+            </div>
+          </div>
         </div>
       </div>
     </div>
@@ -31,7 +45,8 @@
 </template>
 
 <script setup lang="ts">
-  import { computed } from 'vue';
+  import { computed, reactive } from 'vue';
+  import { DownOutlined, RightOutlined } from '@ant-design/icons-vue';
   import { getFormattedText } from '../hooks/helper';
 
   const props = withDefaults(
@@ -56,18 +71,67 @@
     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 ?? '-';
+  /**
+   * 解析配置中的取值占位(如 '${airPlan.missAreaCount}')。
+   * 数据未就绪(后端字段待定)时统一返回空串,由调用方决定展示 '-',避免出现 'undefined'。
+   */
+  function resolveText(formatter?: string, trans?: Record<string, any>): string {
+    if (!formatter) return '';
+    const raw = getFormattedText(props.data, formatter, trans);
+    if (raw === undefined || raw === null || raw === '' || raw === '-') return '';
+    return String(raw);
+  }
+
+  /**
+   * 按文案关键词给出强调样式类(组状态徽标与指标行共用):
+   * 异常类 → 红;不足/缺失/无数据类 → 灰;正常/达标类 → 绿;其余(数值等)保持默认色。
+   */
+  function levelClassOf(text?: string): string {
+    const val = String(text ?? '');
+    if (!val || val === '-') return '';
+    if (/异常|报警|超限|不达标|不符|不自洽|故障|负值/.test(val)) return 'level-danger';
+    if (/不足|缺失|无数据|未获取|暂无/.test(val)) return 'level-muted';
+    if (/正常|达标|合格/.test(val)) return 'level-success';
+    return '';
+  }
+
+  const groups = computed(() => {
+    const raw = props.config?.groups;
+    if (!Array.isArray(raw)) return [];
+    return raw.map((group: any, index: number) => {
+      const statusText = resolveText(group?.status, group?.trans);
+      const items = Array.isArray(group?.items) ? group.items : [];
       return {
-        label: item.label,
-        value: rawValue + (item.unit || ''),
-        colorClass: '',
+        key: group?.key || `group-${index}`,
+        // 步骤序号按配置顺序生成(四个研判步骤 1..4)
+        stepNo: index + 1,
+        title: group?.title || '',
+        statusText,
+        statusClass: levelClassOf(statusText),
+        items: items.map((item: any) => {
+          const value = resolveText(item?.value, item?.trans);
+          return {
+            label: item?.label || '',
+            // 取到值时拼接单位,未取到值只显示占位 '-'
+            text: value ? `${value}${item?.unit || ''}` : '-',
+            levelClass: levelClassOf(resolveText(item?.level)),
+          };
+        }),
       };
     });
   });
 
+  /** 步骤折叠状态(默认全部展开) */
+  const collapsed = reactive<Record<string, boolean>>({});
+
+  function isCollapsed(key: string) {
+    return !!collapsed[key];
+  }
+
+  function toggleGroup(key: string) {
+    collapsed[key] = !collapsed[key];
+  }
+
   const suggestionItems = computed(() => {
     const raw = props.data?.suggestions ?? props.config?.suggestions ?? [];
     if (Array.isArray(raw)) return raw;
@@ -113,17 +177,98 @@
       padding-left: 4px;
     }
 
-    .basis-item {
+    .step-item {
       background: #f9fafb;
+      border-radius: 8px;
+      padding: 4px 8px 6px;
+
+      .step-header {
+        gap: 6px;
+        padding: 2px 2px;
+        cursor: pointer;
+
+        .step-no {
+          flex-shrink: 0;
+          width: 18px;
+          height: 18px;
+          line-height: 18px;
+          text-align: center;
+          border-radius: 50%;
+          background: #60a5fa;
+          color: #fff;
+          font-size: 12px;
+        }
+
+        .step-title {
+          flex: 1;
+          font-size: 14px;
+          font-weight: 600;
+          color: @text-color-base;
+          overflow: hidden;
+          white-space: nowrap;
+          text-overflow: ellipsis;
+        }
+
+        .step-status {
+          flex-shrink: 0;
+          font-size: 12px;
+          padding: 0 6px;
+          border-radius: 8px;
+          line-height: 16px;
+
+          &.level-danger {
+            color: #dc2626;
+            background: #fee2e2;
+          }
+          &.level-muted {
+            color: #6b7280;
+            background: #e5e7eb;
+          }
+          &.level-success {
+            color: #16a34a;
+            background: #dcfce7;
+          }
+        }
+
+        .step-arrow {
+          flex-shrink: 0;
+          font-size: 12px;
+          color: @text-color-call-out;
+        }
+      }
+
+      .step-items {
+        margin-top: 4px;
+      }
+    }
+
+    .basis-item {
       border-radius: 6px;
+      padding: 3px 6px;
 
       .basis-label {
-        font-size: 16px;
+        flex: 1;
+        max-width: 62%;
+        font-size: 13px;
         color: @text-color;
+        overflow: hidden;
+        white-space: nowrap;
+        text-overflow: ellipsis;
       }
       .basis-value {
-        font-size: 24px;
+        flex-shrink: 0;
+        font-size: 15px;
         color: @text-color;
+
+        &.level-danger {
+          color: #dc2626;
+        }
+        &.level-muted {
+          color: #6b7280;
+        }
+        &.level-success {
+          color: #16a34a;
+        }
       }
     }
 

+ 57 - 7
src/views/analysis/warningAnalysis/windPointManage/index.vue

@@ -24,7 +24,15 @@
             </BasicTable>
           </a-col>
           <a-col :span="10">
-            <BasicTable @register="registerWindrectTable" :scroll="{ x: 'max-content', y: 420 }" :pagination="false" :style="{ padding: 0 }" />
+            <!-- 右列:本矿设备一览(测风装置 / 瓦斯装置),供左侧注册点位绑定设备时对照 -->
+            <Tabs size="small" class="device-tabs" :destroyInactiveTabPane="true">
+              <TabPane tab="测风装置" key="windrect">
+                <BasicTable @register="registerWindrectTable" :scroll="{ x: 'max-content', y: 420 }" :pagination="false" :style="{ padding: 0 }" />
+              </TabPane>
+              <TabPane tab="瓦斯装置" key="gas">
+                <BasicTable @register="registerGasTable" :scroll="{ x: 'max-content', y: 420 }" :style="{ padding: 0 }" />
+              </TabPane>
+            </Tabs>
           </a-col>
         </a-row>
       </TabPane>
@@ -84,6 +92,7 @@
   import {
     pointColumns,
     windrectColumns,
+    gasColumns,
     /* modelConfigColumns, defaultModelConfigs, */ formSchema /*, modelConfigFormSchema */,
   } from './windPointManage.data';
   import {
@@ -91,6 +100,7 @@
     savePoint,
     deletePoint,
     getWindrectList,
+    getGasList,
     // getMineAreaRelation,
     // addMineAreaRelation,
     // deleteMineAreaRelation,
@@ -126,11 +136,11 @@
             componentProps: {
               options: [
                 { label: '进风井', value: 0 },
-                { label: '矿井进风', value: 1 },
-                { label: '采区进风', value: 2 },
-                { label: '采区用风', value: 3 },
-                { label: '采区回风', value: 4 },
-                { label: '矿井回风', value: 5 },
+                { label: '总进风巷', value: 1 },
+                { label: '采区进风巷', value: 2 },
+                { label: '工作面巷道', value: 3 },
+                { label: '采区回风巷', value: 4 },
+                { label: '总回风巷', value: 5 },
                 { label: '回风井', value: 6 },
               ],
             },
@@ -168,7 +178,6 @@
   // ====== 上传测风装置表格(getWindrectList)======
   const { tableContext: windrectCtx } = useListPage({
     tableProps: {
-      title: '测风装置',
       formConfig: {
         autoSearch: true,
         labelWidth: 120,
@@ -203,6 +212,42 @@
   });
   const [registerWindrectTable] = windrectCtx;
 
+  // ====== 瓦斯装置表格(getGasList,GasController)======
+  const { tableContext: gasCtx } = useListPage({
+    tableProps: {
+      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: gasColumns,
+      api: getGasList,
+      // 与测风装置表格一致:设备列表作为左侧点位绑定的对照,不分页
+      pagination: false,
+      striped: true,
+      bordered: true,
+      showIndexColumn: true,
+      showActionColumn: false,
+    },
+  });
+  const [registerGasTable] = gasCtx;
+
   // ====== 判识模型参数表格(暂注释,待接口就绪后恢复)======
   /*
   const modelConfigs = ref<any[]>([]);
@@ -414,6 +459,11 @@
     padding: 0 4px;
   }
 
+  // 右列设备表格所属的嵌套 Tab(测风装置 / 瓦斯装置)
+  .device-tabs :deep(.ant-tabs-nav) {
+    margin-bottom: 4px;
+  }
+
   /*
   .config-note {
     margin-top: 12px;

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

@@ -11,6 +11,7 @@ enum Api {
   updateGoaf = '/workingface/mineArea/updateGoaf',
   deleteGoaf = '/workingface/mineArea/deleteGoaf',
   getWindrectList = '/workingface/windrect/getWindrectList',
+  getGasList = '/workingface/gas/getGasList',
   getWindrectRegulation = '/workingface/mineData/getWindrectRegulation',
   getMineAreaRelation = '/workingface/mineArea/getMineAreaRelation',
   addMineAreaRelation = '/workingface/mineArea/addMineAreaRelation',
@@ -66,6 +67,9 @@ export const deleteGoaf = (params?: any) => defHttp.post({ url: Api.deleteGoaf,
 /** 根据矿ID查询下面所有测风装置信息(WindrectController.getWindrectList) */
 export const getWindrectList = (params?: any) => defHttp.post({ url: Api.getWindrectList, params }, { joinParamsToUrl: true });
 
+/** 根据矿ID查询下面所有瓦斯装置信息(GasController.getGasList,deptId/mineCode 均可选) */
+export const getGasList = (params?: any) => defHttp.post({ url: Api.getGasList, params }, { joinParamsToUrl: true });
+
 /** 查询规程值列表(MineDataController.getWindrectRegulation,无参数) */
 export const getWindrectRegulation = (params?: any) => defHttp.get({ url: Api.getWindrectRegulation, params }, { joinParamsToUrl: true });
 

+ 62 - 13
src/views/analysis/warningAnalysis/windPointManage/windPointManage.data.ts

@@ -3,7 +3,7 @@ import type { FormSchema } from '/@/components/Table';
 import { h } from 'vue';
 import { StatusColorEnum } from '/@/enums/jeecgEnum';
 import { useMineDepartmentStore } from '/@/store/modules/mine';
-import { getWindrectList, getWindrectRegulation, getGoafList } from './windPointManage.api';
+import { getWindrectList, getGasList, getWindrectRegulation, getGoafList } from './windPointManage.api';
 
 /** 按矿编码(fax)在组织树中匹配部门名称,未匹配时回退显示原编码 */
 export function renderMineNameByFax(mineCode: string): string {
@@ -51,11 +51,11 @@ export const pointColumns: BasicColumn[] = [
     customRender: ({ text }) => {
       const map: Record<number, string> = {
         0: '进风井',
-        1: '矿井进风',
-        2: '采区进风',
-        3: '采区用风',
-        4: '采区回风',
-        5: '矿井回风',
+        1: '总进风巷',
+        2: '采区进风巷',
+        3: '工作面巷道',
+        4: '采区回风巷',
+        5: '总回风巷',
         6: '回风井',
       };
       return map[text] || text;
@@ -84,6 +84,14 @@ export const pointColumns: BasicColumn[] = [
       return h('span', record.windrectId ? '已绑定' : '未绑定');
     },
   },
+  {
+    title: '瓦斯设备',
+    dataIndex: 'gasId',
+    width: 100,
+    customRender({ record }) {
+      return h('span', record.gasId ? '已绑定' : '未绑定');
+    },
+  },
   {
     title: '创建时间',
     dataIndex: 'createTime',
@@ -138,6 +146,32 @@ export const windrectColumns: BasicColumn[] = [
   // },
 ];
 
+/** 瓦斯装置表格列(Gas,字段与测风装置一致) */
+export const gasColumns: BasicColumn[] = [
+  {
+    title: '设备位置',
+    dataIndex: 'devicePos',
+    width: 200,
+  },
+  {
+    title: '矿井名称',
+    dataIndex: 'mineName',
+    width: 160,
+  },
+  {
+    title: '状态',
+    dataIndex: 'status',
+    width: 100,
+    customRender: ({ text }) => {
+      // Gas.status:1 在线、0 断线
+      const normal = String(text) === '1';
+      const color = normal ? StatusColorEnum.green : StatusColorEnum.red;
+      const label = normal ? '在线' : '离线';
+      return h('span', { style: { color, fontWeight: 'bold' } }, label);
+    },
+  },
+];
+
 /** 新增/编辑 MineArea 表单 */
 export const formSchema: FormSchema[] = [
   {
@@ -154,14 +188,14 @@ export const formSchema: FormSchema[] = [
     dynamicRules: mineLeafDynamicRules,
     // 编辑态(已有 id)禁用矿编码,避免误改归属矿井;新增态保持可选
     ifShow: ({ values }) => !values.id,
-    // 切换煤矿时清空已选测风设备,避免绑定到其他矿的设备
+    // 切换煤矿时清空已选测风设备/瓦斯设备,避免绑定到其他矿的设备
     componentProps: ({ formActionType }) => ({
       placeholder: '请输入煤矿编号',
       changeOnSelect: true,
       initFromStore: false,
       syncFromStore: false,
       onChange: () => {
-        formActionType.setFieldsValue({ windrectId: undefined });
+        formActionType.setFieldsValue({ windrectId: undefined, gasId: undefined });
       },
     }),
   },
@@ -174,11 +208,11 @@ export const formSchema: FormSchema[] = [
       placeholder: '请选择层级',
       options: [
         { label: '进风井', value: 0 },
-        { label: '矿井进风', value: 1 },
-        { label: '采区进风', value: 2 },
-        { label: '采区用风', value: 3 },
-        { label: '采区回风', value: 4 },
-        { label: '矿井回风', value: 5 },
+        { label: '总进风巷', value: 1 },
+        { label: '采区进风巷', value: 2 },
+        { label: '工作面巷道', value: 3 },
+        { label: '采区回风巷', value: 4 },
+        { label: '总回风巷', value: 5 },
         { label: '回风井', value: 6 },
       ],
     },
@@ -210,6 +244,21 @@ export const formSchema: FormSchema[] = [
       params: formModel?.mineCode ? { deptId: formModel.mineCode } : {},
     }),
   },
+  {
+    label: '瓦斯设备',
+    field: 'gasId',
+    component: 'ApiSelect',
+    // 与「煤矿编号」联动:按选中矿(MineCascader 值为部门 id)过滤本矿瓦斯设备(GasController.getGasList);
+    // 非必填——一个测风地点可只绑测风设备,也可再绑一个瓦斯设备
+    componentProps: ({ formModel }) => ({
+      api: getGasList,
+      labelField: 'devicePos',
+      valueField: 'id',
+      placeholder: '请输入瓦斯设备(可不选)',
+      searchable: true,
+      params: formModel?.mineCode ? { deptId: formModel.mineCode } : {},
+    }),
+  },
   {
     label: '供风区域',
     field: 'goafList',

+ 74 - 27
src/views/analysis/warningAnalysis/windPointManage/windTopology/hooks/useTopology.ts

@@ -28,6 +28,8 @@ export function useTopology() {
   /** 测风装置显示名映射(rawId → devicePos/deviceCode/id,取自全量 windrectList):
    *  已绑定设备不再绘制节点,其名称由该映射写入巷道连线 edgeLabel 及解绑确认文案 */
   let deviceNameMap: Record<string, string> = {};
+  /** 瓦斯装置显示名映射(rawId → devicePos/deviceCode/id,取自全量 gasList),用途同上 */
+  let gasNameMap: Record<string, string> = {};
   let resizeObserver: ResizeObserver | null = null;
 
   // —— 模式状态 ——
@@ -35,10 +37,12 @@ export function useTopology() {
   const mode = ref<TopoMode>('relation');
   /** 绑定模式选中的关联源节点(双击节点选中,再单击另一节点建立关系;红色描边高亮) */
   const armedSource = ref<TopoNodeData | null>(null);
-  /** 布点模式绑定弹窗:双击未绑定布点的连线后弹出选择测风装置 */
+  /** 布点模式绑定弹窗:双击巷道连线后弹出,同时处理测风装置与瓦斯装置的绑定/解绑 */
   const bindVisible = ref(false);
   const bindPoint = ref<TopoNodeData | null>(null);
-  const bindDeviceId = ref<string | undefined>(undefined);
+  /** 本次新选的测风装置 / 瓦斯装置 id(已绑定的设备只读展示,不在此处回填) */
+  const bindWindrectId = ref<string | undefined>(undefined);
+  const bindGasId = ref<string | undefined>(undefined);
   /** 操作栏部门选择:选中任意层级部门后过滤部门树(该部门及其后代)并按该部门查询数据;空=全局 */
   const selectedDeptId = ref('');
 
@@ -96,6 +100,11 @@ export function useTopology() {
       for (const d of bundle.windrectList) {
         if (d.id) deviceNameMap[String(d.id)] = d.devicePos || d.deviceCode || String(d.id);
       }
+      // 瓦斯装置显示名映射(同上)
+      gasNameMap = {};
+      for (const g of bundle.gasList) {
+        if (g.id) gasNameMap[String(g.id)] = g.devicePos || g.deviceCode || String(g.id);
+      }
       renderTopology(bundle.topology);
       // 数据为空时给出诊断信息(总进地面节点仍会渲染)
       if (!bundle.topology.nodes.some((n) => n.category !== 0)) {
@@ -214,15 +223,19 @@ export function useTopology() {
         if (l.kind === 'roadway') {
           // 巷道连线:直线(无曲率)+ 按主要数据点位 level 用 levelColorMap 着色 + 风流方向箭头;
           // 疑似隐蔽工作面巷道(主要数据点位 judgeAreaList 字段有内容 → 节点 suspected)标红加粗;
-          // 中点标注(edgeLabel rich 富文本):第一行主要数据点位名称、第二行风量、第三行圆点 + 已绑定设备名;
-          // 未绑定时第三行输出浅灰占位圆点(dotEmpty),保证标签高度恒定
+          // 中点标注(edgeLabel rich 富文本):第一行主要数据点位名称、第二行为测风装置(绿点)、第三行为瓦斯装置(蓝点);
+          // 未绑定的设备行输出浅灰占位圆点(dotEmpty),保证标签恒为 3 行、高度稳定
           const point = l.mainPointId ? pointById.get(l.mainPointId) : undefined;
           const lv = point ? Number(point.level) : NaN;
           const deviceName = point?.windrectId ? deviceNameMap[point.windrectId] : '';
+          const gasName = point?.gasId ? gasNameMap[point.gasId] : '';
           const suspected = point?.suspected;
-          // 常态标注:仅名称;高亮标注:名称 + 圆点与已绑定布点名(未绑定输出浅灰占位圆点)
+          // 设备行:已绑定输出设备名,未绑定输出浅灰占位圆点
+          const windrectLine = deviceName ? `{dot|● }{device|${deviceName}}` : '{dotEmpty|● }';
+          const gasLine = gasName ? `{dotGas|● }{device|${gasName}}` : '{dotEmpty|● }';
+          // 常态标注:仅名称;高亮标注:名称 + 测风装置行 + 瓦斯装置行
           link.midLabel = point ? `{name|${point.name}}` : '';
-          link.midLabelFull = point ? `{name|${point.name}}\n${deviceName ? `{dot|● }{device|${deviceName}}` : '{dotEmpty|● }'}` : '';
+          link.midLabelFull = point ? `{name|${point.name}}\n${windrectLine}\n${gasLine}` : '';
           link.lineStyle = {
             color: suspected ? JUDGE_COLOR : levelColorMap[lv] || POINT_COLOR,
             width: suspected ? JUDGE_WIDTH : 3,
@@ -287,7 +300,7 @@ export function useTopology() {
           // 绑定模式:双击连线 → 解除巷道关系(传关系 id)
           handleRemoveRelation(params.data);
         } else {
-          // 布点模式:双击连线 → 对其主要数据点位解绑/绑定测风装置
+          // 布点模式:双击连线 → 打开其主数据点位的设备绑定弹窗(测风装置 / 瓦斯装置)
           const mainId = params.data.mainPointId || params.data.pointId;
           const point = mainId ? topologyData.nodes.find((n) => n.id === mainId) : undefined;
           if (point) handleDeviceAction(point);
@@ -394,15 +407,21 @@ export function useTopology() {
     return (rawId && deviceNameMap[rawId]) || rawId || '';
   }
 
-  /** 布点模式:双击连线 → 已有测风装置则解绑,否则弹窗选择绑定 */
+  /** 按瓦斯装置原始 id 解析其显示名(同上) */
+  function gasNameOf(rawId?: string) {
+    return (rawId && gasNameMap[rawId]) || rawId || '';
+  }
+
+  /**
+   * 布点模式:双击连线 → 打开设备绑定弹窗。
+   * 弹窗内已绑定的设备只读展示(附解绑按钮),未绑定的设备用下拉(NoUsed 列表)新选,
+   * 因此「未绑定→绑定」「已绑定→解绑」两种场景共用同一个弹窗。
+   */
   function handleDeviceAction(point: TopoNodeData) {
-    if (point.windrectId) {
-      confirmUnbindDevice(point);
-    } else {
-      bindPoint.value = point;
-      bindDeviceId.value = undefined;
-      bindVisible.value = true;
-    }
+    bindPoint.value = point;
+    bindWindrectId.value = undefined;
+    bindGasId.value = undefined;
+    bindVisible.value = true;
   }
 
   /** 关闭布点绑定弹窗(index.vue 取消/关闭时同步,成功关闭由 confirmBind 内部调用) */
@@ -410,19 +429,24 @@ export function useTopology() {
     bindVisible.value = false;
   }
 
-  /** 布点模式:弹窗选择测风装置后调用(updateMineArea 更新 windrectId 字段) */
-  async function confirmBind(deviceId?: string) {
+  /**
+   * 布点模式:弹窗确认 → 仅提交「本次新选」的设备字段(updateMineArea 更新 windrectId / gasId),
+   * 避免把另一设备的既有绑定覆盖为空。
+   */
+  async function confirmBind() {
     const point = bindPoint.value;
     if (!point?.rawId) {
       message.warning('缺少测风地点原始 id,无法绑定');
       return;
     }
-    if (!deviceId) {
-      message.warning('请选择数据点位');
+    const windrectId = bindWindrectId.value;
+    const gasId = bindGasId.value;
+    if (!windrectId && !gasId) {
+      message.warning('请选择要绑定的测风装置或瓦斯装置');
       return;
     }
     try {
-      await updateArea({ id: point.rawId, windrectId: deviceId });
+      await updateArea({ id: point.rawId, ...(windrectId ? { windrectId } : {}), ...(gasId ? { gasId } : {}) });
       closeBindDialog();
       await loadTopology();
       message.success('绑定成功');
@@ -432,20 +456,31 @@ export function useTopology() {
   }
 
   /** 布点模式:解绑测风装置(清空 windrectId 字段,updateMineArea) */
-  function confirmUnbindDevice(point: TopoNodeData) {
-    const deviceName = deviceNameOf(point.windrectId);
+  function unbindWindrect(point: TopoNodeData) {
+    confirmUnbind('测风装置', deviceNameOf(point.windrectId), { id: point.rawId, windrectId: '' }, point.name);
+  }
+
+  /** 布点模式:解绑瓦斯装置(清空 gasId 字段,updateMineArea) */
+  function unbindGas(point: TopoNodeData) {
+    confirmUnbind('瓦斯装置', gasNameOf(point.gasId), { id: point.rawId, gasId: '' }, point.name);
+  }
+
+  /** 解绑确认弹窗(测风装置 / 瓦斯装置共用;field 为提交的字段负载,空串表示解绑) */
+  function confirmUnbind(deviceLabel: string, deviceName: string, payload: Record<string, any>, pointName: string) {
     Modal.confirm({
       title: '确认解绑',
-      content: `确定解除"${point.name}"与测风装置"${deviceName}"的绑定关系?`,
+      content: `确定解除"${pointName}"与${deviceLabel}"${deviceName}"的绑定关系?`,
       okText: '确认解绑',
       cancelText: '取消',
       onOk: async () => {
-        if (!point.rawId) {
+        if (!payload.id) {
           message.warning('缺少测风地点原始 id,无法解绑');
           return;
         }
         try {
-          await updateArea({ id: point.rawId, windrectId: '' });
+          await updateArea(payload);
+          // 解绑后关闭绑定弹窗:bindPoint 引用的是重载前的节点快照,避免弹窗仍显示旧的绑定态
+          closeBindDialog();
           await loadTopology();
           message.success('解绑成功');
         } catch {
@@ -549,6 +584,13 @@ export function useTopology() {
       label: f.label,
       value: (node as any)[f.key] ?? '-',
     }));
+    // 测风地点:追加已绑定的测风装置 / 瓦斯装置显示名(原始 id → 设备位置名,未绑定显示「未绑定」)
+    if (node.category === 1) {
+      detailFields.value.push(
+        { label: '测风装置', value: deviceNameOf(node.windrectId) || '未绑定' },
+        { label: '瓦斯装置', value: gasNameOf(node.gasId) || '未绑定' }
+      );
+    }
   }
 
   function clearSelection() {
@@ -587,7 +629,7 @@ export function useTopology() {
     if (m === 'relation') {
       message.info('双击连线解除关系;双击节点并选中另一节点以建立关系');
     } else {
-      message.info('双击连线解绑或绑定测风装置');
+      message.info('双击连线绑定或解绑测风装置、瓦斯装置');
     }
   }
 
@@ -611,8 +653,13 @@ export function useTopology() {
     setMode,
     bindVisible,
     bindPoint,
-    bindDeviceId,
+    bindWindrectId,
+    bindGasId,
     confirmBind,
+    unbindWindrect,
+    unbindGas,
+    deviceNameOf,
+    gasNameOf,
     closeBindDialog,
     selectedNode,
     detailFields,

+ 1 - 1
src/views/analysis/warningAnalysis/windPointManage/windTopology/hooks/useTopologyLayout.ts

@@ -19,7 +19,7 @@ const rowGapFor = (lv?: number | null) => LAYOUT.rowGap[levelKey(lv) as any] ??
 
 /**
  * 列式布局(从左到右,固定间距):
- *   固定列序 总进 → lv0..lv6 → 未分级 → 总回;
+ *   固定列序 总进 → lv0..lv6(进风井 → 总进风巷 → 采区进风巷 → 工作面巷道 → 采区回风巷 → 总回风巷 → 回风井)→ 未分级 → 总回;
  *   列间距(x 轴间隔)固定:x = margin + colGap × 列序;
  *   行间距(y 轴间隔)固定且按层级分别配置:每列取该层级 rowGap[level](未配置回退 rowGapDefault);
  *   每列围绕画布垂直中心独立居中:colStart = centerY − spacing·(n−1)/2,

+ 2 - 2
src/views/analysis/warningAnalysis/windPointManage/windTopology/hooks/useTopologyOrdering.ts

@@ -3,7 +3,7 @@ import type { TopologyData, TopoNodeData } from '../windTopology.data';
 /**
  * 通风网络拓扑层内排序:median / barycenter 多轮扫描法(Sugiyama 框架的 crossing reduction 阶段)。
  *
- * 背景:层级已由 level 字段固定为语义列(1=矿井进风 … 5=矿井回风),列内排序只影响相邻层
+ * 背景:层级已由 level 字段固定为语义列(1=总进风巷 … 5=总回风巷),列内排序只影响相邻层
  * 巷道连线的交叉数。二部交叉最小化(bipartite crossing minimization)为 NP-hard,
  * 本模块采用工业图布局库(dagre/ELK)同款启发式:
  *   1) 每层节点按"其相邻层邻居位置的 median(中位数)/ barycenter(均值)"计算关键值;
@@ -26,7 +26,7 @@ export interface OrderingOptions {
   sweepRounds?: number;
 }
 
-/** 参与排序的层级 key(0=进风井 1..5 现有层级 6=回风井),与 levelTextMap / computeLayout 分组一致 */
+/** 参与排序的层级 key(0=进风井 1..5 总进风巷..总回风巷 6=回风井),与 levelTextMap / computeLayout 分组一致 */
 const ORDER_KEYS = ['0', '1', '2', '3', '4', '5', '6'];
 
 /** 层级 key:0..6 有效 → 数字字符串;否则 'ungraded' */

+ 66 - 12
src/views/analysis/warningAnalysis/windPointManage/windTopology/index.vue

@@ -40,7 +40,7 @@
         </a-button-group>
         <a-divider type="vertical" />
         <span v-if="mode === 'relation'" class="select-text">双击连线解除关系;双击节点并选中另一节点以建立关系</span>
-        <span v-else class="select-text">双击连线解绑或绑定测风装置</span>
+        <span v-else class="select-text">双击连线可绑定/解绑测风装置与瓦斯装置</span>
       </div>
 
       <!-- 图例 -->
@@ -83,25 +83,53 @@
       </div>
     </div>
 
-    <!-- 布点模式绑定弹窗(双击未绑定布点的连线弹出;destroyOnClose 保证每次打开重建表单刷新下拉数据) -->
+    <!-- 布点模式设备绑定弹窗(双击连线弹出;destroyOnClose 保证每次打开重建表单、刷新未使用设备下拉) -->
     <BasicModal
       @register="registerBindModal"
-      :title="`绑定测风装置至:${bindPoint?.name}`"
+      :title="`设备绑定至:${bindPoint?.name}`"
       ok-text="确认"
       cancel-text="取消"
+      :width="520"
       destroyOnClose
       @ok="handleBindOk"
       @cancel="handleBindCancel"
     >
       <a-form layout="vertical" class="m-5">
-        <a-form-item label="数据点位" required>
+        <!-- 测风装置:已绑定 → 只读展示 + 解绑;未绑定 → 从未使用列表中下拉选择 -->
+        <a-form-item label="测风装置">
+          <template v-if="bindPoint?.windrectId">
+            <div class="bound-device">
+              <span class="bound-device-name">{{ deviceNameOf(bindPoint.windrectId) }}</span>
+              <a-button type="link" danger size="small" @click="unbindWindrect(bindPoint)">解绑</a-button>
+            </div>
+          </template>
           <ApiSelect
-            v-model:value="bindDeviceId"
+            v-else
+            v-model:value="bindWindrectId"
             :api="getWindrectListNoUsed"
             :params="bindDeviceParams"
             label-field="devicePos"
             value-field="id"
-            placeholder="请选择数据点位"
+            placeholder="请选择测风装置"
+          />
+        </a-form-item>
+
+        <!-- 瓦斯装置:已绑定 → 只读展示 + 解绑;未绑定 → 从未使用列表中下拉选择 -->
+        <a-form-item label="瓦斯装置">
+          <template v-if="bindPoint?.gasId">
+            <div class="bound-device">
+              <span class="bound-device-name">{{ gasNameOf(bindPoint.gasId) }}</span>
+              <a-button type="link" danger size="small" @click="unbindGas(bindPoint)">解绑</a-button>
+            </div>
+          </template>
+          <ApiSelect
+            v-else
+            v-model:value="bindGasId"
+            :api="getGasListNoUsed"
+            :params="bindDeviceParams"
+            label-field="devicePos"
+            value-field="id"
+            placeholder="请选择瓦斯装置"
           />
         </a-form-item>
       </a-form>
@@ -117,11 +145,11 @@
   import MineCascader from '/@/components/Form/src/jeecg/components/MineCascader/MineCascader.vue';
   import { categories, levelTextMap, levelColorMap, POINT_COLOR, JUDGE_COLOR } from './windTopology.data';
   import { useTopology } from './hooks/useTopology';
-  import { getWindrectListNoUsed } from './windTopology.api';
+  import { getWindrectListNoUsed, getGasListNoUsed } from './windTopology.api';
   import { h } from 'vue';
   import { PlusOutlined, MinusOutlined, SaveOutlined } from '@ant-design/icons-vue';
 
-  /** 图例层级项:按 levelTextMap/levelColorMap 顺序生成(矿井进风 → 矿井回风) */
+  /** 图例层级项:按 levelTextMap/levelColorMap 顺序生成(进风井 → 回风井,井筒层级用于展示石门、辅运大巷等连接关系) */
   const levelList = Object.keys(levelTextMap).map((k) => {
     const level = Number(k);
     return { level, text: levelTextMap[level], color: levelColorMap[level] };
@@ -139,8 +167,13 @@
     setMode,
     bindVisible,
     bindPoint,
-    bindDeviceId,
+    bindWindrectId,
+    bindGasId,
     confirmBind,
+    unbindWindrect,
+    unbindGas,
+    deviceNameOf,
+    gasNameOf,
     closeBindDialog,
     selectedNode,
     detailFields,
@@ -172,7 +205,7 @@
   /** 布点模式绑定弹窗(useModal 钩子调用;destroyOnClose 每次打开重建表单刷新下拉数据) */
   const [registerBindModal, { openModal, closeModal }] = useModal();
 
-  // 双击未绑定布点的连线置 bindVisible=true → 打开模态框;确认/取消置 false → 关闭
+  // 双击连线置 bindVisible=true → 打开模态框;确认/取消置 false → 关闭
   watch(bindVisible, (v) => {
     if (v) {
       openModal(true);
@@ -181,11 +214,11 @@
     }
   });
 
-  /** 绑定弹窗下拉参数:按当前选中矿(deptId)查询未使用测风装置 */
+  /** 绑定弹窗下拉参数:按当前选中矿(deptId)查询未使用的测风装置 / 瓦斯装置 */
   const bindDeviceParams = computed(() => ({ deptId: selectedDeptId.value }));
 
   function handleBindOk() {
-    confirmBind(bindDeviceId.value);
+    confirmBind();
   }
 
   function handleBindCancel() {
@@ -279,6 +312,27 @@
     overflow: hidden;
   }
 
+  // 绑定弹窗中已绑定设备的只读展示行(设备名 + 解绑按钮)
+  .bound-device {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    gap: 8px;
+    padding: 0 11px;
+    height: 32px;
+    border: 1px solid #d9d9d9;
+    border-radius: 6px;
+    background: #fafafa;
+
+    .bound-device-name {
+      flex: 1;
+      overflow: hidden;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+      color: #333;
+    }
+  }
+
   .topo-canvas {
     width: 100%;
     height: 100%;

+ 14 - 6
src/views/analysis/warningAnalysis/windPointManage/windTopology/windTopology.api.ts

@@ -1,6 +1,6 @@
 import { defHttp } from '/@/utils/http/axios';
 import { useMineDepartmentStore } from '/@/store/modules/mine';
-import type { MineAreaNode, MineAreaRelationNode, TopologyBundle, WindrectNode } from './windTopology.data';
+import type { MineAreaNode, MineAreaRelationNode, TopologyBundle, WindrectNode, GasNode } from './windTopology.data';
 import { buildTopologyBundle } from './windTopology.data';
 import { RequestOptions } from '/#/axios';
 
@@ -10,6 +10,8 @@ enum Api {
   deleteMineAreaRelation = '/workingface/mineArea/deleteMineAreaRelation',
   getWindrectList = '/workingface/windrect/getWindrectList',
   getWindrectListNoUsed = '/workingface/windrect/getWindrectListNoUsed',
+  getGasList = '/workingface/gas/getGasList',
+  getGasListNoUsed = '/workingface/gas/getGasListNoUsed',
   updateMineArea = '/workingface/mineArea/updateMineArea',
   updateMineAreaTopology = '/workingface/mineArea/updateMineAreaTopology',
 }
@@ -17,7 +19,8 @@ enum Api {
 /**
  * 获取测风网络拓扑全量数据(接口获取 → 代码处理 → 返回完整数据供消费):
  *   getMineAreaRelation -> 测风地点列表 + 巷道关系列表(按矿编码 mineCode=fax 查询)
- *   getWindrectList     -> 测风装置全量列表(按 deptId 查询,用于解析已绑定设备显示名)
+ *   getWindrectList     -> 测风装置全量列表(按 deptId 查询,用于解析已绑定测风装置显示名)
+ *   getGasList          -> 瓦斯装置全量列表(按 deptId 查询,用于解析已绑定瓦斯装置显示名)
  * 所有接口并行请求(allSettled),单个失败不影响整体;整体异常时返回备用空数据
  * (总进地面节点 + 空列表),保证拓扑图总能绘制。
  */
@@ -30,9 +33,10 @@ export const getTopologyData = async (params?: any): Promise<TopologyBundle> =>
     // 兼容两种响应结构:分页 { records } 或裸数组
     const toList = (res: any): any[] => (Array.isArray(res) ? res : Array.isArray(res?.records) ? res.records : []);
 
-    const [relRes, windrectRes] = await Promise.allSettled([
+    const [relRes, windrectRes, gasRes] = await Promise.allSettled([
       defHttp.post({ url: Api.getMineAreaRelation, params: { mineCode } }, { joinParamsToUrl: true }),
       defHttp.post({ url: Api.getWindrectList, params: { deptId, column: 'createTime', order: 'desc' } }, { joinParamsToUrl: true }),
+      defHttp.post({ url: Api.getGasList, params: { deptId, column: 'createTime', order: 'desc' } }, { joinParamsToUrl: true }),
     ]);
     // 单个接口失败不影响另一个(避免整体空白);失败时记录原因便于定位
     const relData = relRes.status === 'fulfilled' ? (relRes.value as any) || {} : (console.error('获取巷道关系失败:', relRes.reason), {});
@@ -40,16 +44,17 @@ export const getTopologyData = async (params?: any): Promise<TopologyBundle> =>
     const relationList = Array.isArray(relData.mineAreaRelationList) ? (relData.mineAreaRelationList as MineAreaRelationNode[]) : [];
     const windrectList =
       windrectRes.status === 'fulfilled' ? toList(windrectRes.value) : (console.error('获取测风装置失败:', windrectRes.reason), []);
+    const gasList = gasRes.status === 'fulfilled' ? toList(gasRes.value) : (console.error('获取瓦斯装置失败:', gasRes.reason), []);
 
-    return buildTopologyBundle(areaList, relationList, windrectList as WindrectNode[]);
+    return buildTopologyBundle(areaList, relationList, windrectList as WindrectNode[], gasList as GasNode[]);
   } catch (e) {
     // 备用空数据兜底:保证拓扑图(总进地面节点)在接口异常时仍可绘制
     console.error('拓扑数据组装失败,使用空数据兜底:', e);
-    return buildTopologyBundle([], [], []);
+    return buildTopologyBundle([], [], [], []);
   }
 };
 
-/** 编辑测风地点(绑定/解绑测风装置通过字段:id=测风地点主键、windrectId=绑定测风装置 id) */
+/** 编辑测风地点(绑定/解绑测风装置、瓦斯装置通过字段:id=测风地点主键、windrectId=测风装置 id、gasId=瓦斯装置 id) */
 export const updateArea = (params?: any, opts?: RequestOptions) => defHttp.post({ url: Api.updateMineArea, params }, opts);
 
 export const updateMineAreaTopology = (params?: any) => defHttp.post({ url: Api.updateMineAreaTopology, params });
@@ -57,6 +62,9 @@ export const updateMineAreaTopology = (params?: any) => defHttp.post({ url: Api.
 /** 查询未使用测风装置列表(按 deptId 查询,供布点模式绑定弹窗选择) */
 export const getWindrectListNoUsed = (params?: any) => defHttp.post({ url: Api.getWindrectListNoUsed, params }, { joinParamsToUrl: true });
 
+/** 查询未使用瓦斯装置列表(未被任何 mine_area 绑定,供布点模式绑定弹窗选择) */
+export const getGasListNoUsed = (params?: any) => defHttp.post({ url: Api.getGasListNoUsed, params }, { joinParamsToUrl: true });
+
 /** 绑定 MineArea 关联关系(RequestBody:mineCode/parentId/childId;双击节点+单击节点关联时调用) */
 export const addMineAreaRelation = (params?: any) => defHttp.post({ url: Api.addMineAreaRelation, params });
 

+ 44 - 16
src/views/analysis/warningAnalysis/windPointManage/windTopology/windTopology.data.ts

@@ -25,25 +25,29 @@ export const JUDGE_WIDTH = 6;
 
 // ==================== 测风地点层级映射 ====================
 
-/** 测风地点通风类型层级(level 字段语义),用于按层级分列绘制拓扑 */
+/**
+ * 巷道分类层级(level 字段语义),用于按层级分列绘制拓扑、区分连线颜色:
+ *   进风侧:进风井(井筒,便于展示石门、辅运大巷等总进风巷的连接关系)→ 总进风巷 → 采区进风巷 → 工作面巷道
+ *   回风侧:采区回风巷 → 总回风巷 → 回风井(井筒)
+ */
 export const levelTextMap: Record<number, string> = {
   0: '进风井',
-  1: '矿井进风',
-  2: '采区进风',
-  3: '采区用风',
-  4: '采区回风',
-  5: '矿井回风',
+  1: '总进风巷',
+  2: '采区进风巷',
+  3: '工作面巷道',
+  4: '采区回风巷',
+  5: '总回风巷',
   6: '回风井',
 };
 
 /** 测风地点层级配色(7 级互不混淆,避开地面紫 #722ed1),用于按层级区分巷道连线颜色 */
 export const levelColorMap: Record<number, string> = {
   0: '#52c41a', // 进风井
-  1: '#f5222d', // 矿井进风
-  2: '#fa8c16', // 采区进风
-  3: '#fadb14', // 采区用风
-  4: '#13c2c2', // 采区回风
-  5: '#eb2f96', // 矿井回风
+  1: '#f5222d', // 总进风巷
+  2: '#fa8c16', // 采区进风巷
+  3: '#fadb14', // 工作面巷道
+  4: '#13c2c2', // 采区回风巷
+  5: '#eb2f96', // 总回风巷
   6: '#1677ff', // 回风井
 };
 
@@ -85,7 +89,7 @@ export function createGraphOption(): EChartsOption {
         edgeSymbol: ['none', 'arrow'],
         edgeSymbolSize: [0, 10],
         // 巷道连线中点标注:常态仅显示名称(midLabel),高亮(emphasis)显示名称及布点信息(midLabelFull);
-        // 名称行 + 圆点与已绑定布点名;未绑定时第三行输出浅灰占位圆点
+        // 名称行 + 圆点与已绑定布点名(测风装置、瓦斯装置各一行);未绑定时对应行输出浅灰占位圆点
         edgeLabel: {
           show: true,
           position: 'middle',
@@ -93,11 +97,13 @@ export function createGraphOption(): EChartsOption {
           opacity: 0.4,
           width: 100,
           overflow: 'truncate',
-          // midLabel/midLabelFull 由渲染层按 rich 标记生成:常态 {name|...};高亮 {name|...}\n{dot|● }{device|...} 或 {dotEmpty|● }
+          // midLabel/midLabelFull 由渲染层按 rich 标记生成:常态 {name|...};
+          // 高亮 {name|...}\n{dot|● }{device|测风装置名 or dotEmpty} \n{dotGas|● }{device|瓦斯装置名 or dotEmpty}
           formatter: (p: any) => p?.data?.midLabel || '',
           rich: {
             name: { fontSize: 14, color: '#333', fontWeight: 'bold' },
             dot: { fontSize: 12, color: '#52c41a', lineHeight: 18 },
+            dotGas: { fontSize: 12, color: '#1677ff', lineHeight: 18 },
             device: { fontSize: 12, color: '#333', lineHeight: 18 },
             dotEmpty: { fontSize: 12, color: '#d9d9d9', lineHeight: 18 },
           },
@@ -157,12 +163,14 @@ export interface TopoNodeData {
   isLeafText?: string;
   /** 矿井编码(部门叶子节点(矿点)的 fax) */
   fax?: string;
-  /** 测风地点通风类型层级(1=矿井进风 2=采区进风 3=采区用风 4=采区回风 5=矿井回风),用于按层级分列 */
+  /** 巷道分类层级(1=总进风巷 2=采区进风巷 3=工作面巷道 4=采区回风巷 5=总回风巷),用于按层级分列 */
   level?: number;
   /** 层级显示名(levelTextMap 映射) */
   levelText?: string;
   airVolume?: number;
   windrectId?: string;
+  /** 绑定的瓦斯装置 id(mine_area.gasId,与 windrectId 并列) */
+  gasId?: string;
   mineCode?: string;
   /** 布局保存坐标(用户拖动后保存的 ECharts 画布像素坐标,加载时优先使用) */
   topologyX?: number | string;
@@ -203,10 +211,12 @@ export interface MineAreaNode {
   id: string;
   mineCode: string;
   name: string;
-  level: number; // 0=进风井 1=矿井进风 2=采区进风 3=采区用风 4=采区回风 5=矿井回风 6=回风井
+  level: number; // 0=进风井 1=总进风巷 2=采区进风巷 3=工作面巷道 4=采区回风巷 5=总回风巷 6=回风井
   airVolume: number;
   regulationId?: string;
   windrectId?: string;
+  /** 绑定的瓦斯装置 id(随 updateMineArea 写入 mine_area.gasId) */
+  gasId?: string;
   /** 布局保存坐标(用户拖动后保存的 ECharts 画布像素坐标) */
   topologyX?: number | string;
   topologyY?: number | string;
@@ -229,6 +239,18 @@ export interface WindrectNode {
   [key: string]: any;
 }
 
+/** API -> Gas(瓦斯装置,字段结构同 Windrect) */
+export interface GasNode {
+  id: string;
+  mineCode: string;
+  mineName?: string;
+  deviceCode?: string;
+  devicePos?: string;
+  status?: number;
+  createTime?: string;
+  [key: string]: any;
+}
+
 /** API -> MineAreaRelation(巷道关系:一条巷道 = 父巷道 → 子巷道) */
 export interface MineAreaRelationNode {
   id: string;
@@ -260,6 +282,8 @@ export interface TopologyBundle {
   topology: TopologyData;
   /** 测风装置全量列表(getWindrectList,用于解析已绑定设备显示名写入连线标注) */
   windrectList: WindrectNode[];
+  /** 瓦斯装置全量列表(getGasList,用于解析已绑定瓦斯装置显示名) */
+  gasList: GasNode[];
 }
 
 /**
@@ -356,6 +380,7 @@ export function transformToTopologyData(mineAreas: MineAreaNode[], relations: Ar
       mineName: a.mineCode,
       airVolume: a.airVolume,
       windrectId: a.windrectId,
+      gasId: a.gasId,
       level: a.level,
       levelText: levelTextMap[a.level] || '',
       topologyX: a.topologyX,
@@ -371,15 +396,18 @@ export function transformToTopologyData(mineAreas: MineAreaNode[], relations: Ar
  * 由各接口原始数据组装拓扑全量数据(供 getTopologyData 调用):
  * 生成巷道关系数组 → 拓扑数据(含总进地面节点与疑似标记)。
  * 任何列表为空都返回完整结构(总进地面节点 + 空列表),保证消费方总能绘制拓扑图。
+ * gasList 为可选参数(默认空数组):接口未就绪/未传时瓦斯装置名称与下拉为空,不影响既有行为。
  */
 export function buildTopologyBundle(
   mineAreaList: MineAreaNode[],
   mineAreaRelationList: MineAreaRelationNode[],
-  windrectList: WindrectNode[]
+  windrectList: WindrectNode[],
+  gasList: GasNode[] = []
 ): TopologyBundle {
   const relations = buildRelationArray(mineAreaList, mineAreaRelationList);
   return {
     topology: transformToTopologyData(mineAreaList, relations),
     windrectList,
+    gasList,
   };
 }

+ 60 - 4
src/views/dashboard/HiddenSurface/configurable.data.ts

@@ -294,10 +294,66 @@ export const testConfigVentilationMonitor: Config[] = [
         {
           readFrom: 'identifyData',
           conclusion: '${conclusion}',
-          items: [
-            { label: '风量异常数', prop: 'm3Exception', unit: '个' },
-            { label: '测风地点数', prop: 'mineAreaCount', unit: '个' },
-            { label: '测风装置数', prop: 'windrectCount', unit: '个' },
+          /**
+           * 判定依据:按四个步骤分组研判(需求 2.1.1)。
+           * 取值路径为占位约定(后端判识明细接口未定稿):数据挂在 identifyData 下的组 key 上,
+           * 组状态约定 0 正常 / 1 异常 / 2 数据不足;后端定稿后仅调整本处字段路径。
+           * 未取到数值时前端统一展示 '-'。
+           */
+          groups: [
+            {
+              key: 'airPlan',
+              title: '配风计划',
+              status: '${airPlan.status}',
+              trans: { 0: '正常', 1: '异常', 2: '数据不足' },
+              items: [
+                { label: '用风地点覆盖率未达标缺失地点', value: '${airPlan.missAreaCount}', unit: '个' },
+                { label: '供风满足率不足地点', value: '${airPlan.lackAreaCount}', unit: '个' },
+                { label: '风量偏差率≤0 地点', value: '${airPlan.zeroDeviationCount}', unit: '个' },
+                { label: '明细/小计/汇总不符类别', value: '${airPlan.inconsistentTypeCount}', unit: '个' },
+              ],
+            },
+            {
+              key: 'report',
+              title: '测风报表',
+              status: '${report.status}',
+              trans: { 0: '正常', 1: '异常', 2: '数据不足' },
+              items: [
+                { label: '断面/风速/风量不自洽测点', value: '${report.inconsistentPointCount}', unit: '个' },
+                { label: '明细与汇总不符类别', value: '${report.inconsistentTypeCount}', unit: '个' },
+                { label: '配风与测风偏差率≤0 地点', value: '${report.zeroDeviationCount}', unit: '个' },
+              ],
+            },
+            {
+              key: 'windVolume',
+              title: '传感器风量',
+              status: '${windVolume.status}',
+              trans: { 0: '正常', 1: '异常', 2: '数据不足' },
+              items: [
+                { label: '横向闭合差', value: '${windVolume.horizontalClosure}', unit: 'm³/min' },
+                { label: '纵向闭合差', value: '${windVolume.verticalClosure}', unit: 'm³/min' },
+                { label: '闭合差报警', value: '${windVolume.closureAlarm}', trans: { 0: '正常', 1: '报警' } },
+                { label: '班次偏移率异常测点', value: '${windVolume.shiftAbnormalCount}', unit: '个' },
+                { label: '突发型偏差率异常测点', value: '${windVolume.suddenAbnormalCount}', unit: '个' },
+                { label: '趋势型偏差率异常测点', value: '${windVolume.trendAbnormalCount}', unit: '个' },
+                { label: '未解释风量·用风地点', value: '${windVolume.unexplainedAreaVolume}', unit: 'm³/min' },
+                { label: '未解释风量·采(盘)区', value: '${windVolume.unexplainedDistrictVolume}', unit: 'm³/min' },
+                { label: '未解释风量·矿井', value: '${windVolume.unexplainedMineVolume}', unit: 'm³/min' },
+              ],
+            },
+            {
+              key: 'gasCheck',
+              title: '瓦斯校验情况',
+              status: '${gasCheck.status}',
+              trans: { 0: '正常', 1: '异常', 2: '数据不足' },
+              items: [
+                { label: '实测绝对瓦斯涌出量', value: '${gasCheck.gasVolume}', unit: 'm³/min' },
+                { label: '分源预测理论基准值', value: '${gasCheck.theoryVolume}', unit: 'm³/min' },
+                { label: '偏差率', value: '${gasCheck.deviationRate}', unit: '%' },
+                { label: '偏差持续时间', value: '${gasCheck.duration}', unit: 'min' },
+                { label: '与风量异常时间窗叠合测点', value: '${gasCheck.overlapCount}', unit: '个' },
+              ],
+            },
           ],
           suggestions: ['请现场核查疑似隐蔽巷道的通风设施状态', '缺失测风点应尽快补装并接入系统'],
           // suggestions: ['暂无建议'],

+ 16 - 0
src/views/monitor/gasMonitor/gasMonitor.api.ts

@@ -0,0 +1,16 @@
+import { defHttp } from '/@/utils/http/axios';
+
+enum Api {
+  getGasData = '/workingface/gas/getGasData',
+  getGasHistory = '/workingface/gas/getGasHistory',
+  getGasList = '/workingface/gas/getGasList',
+}
+
+/** 查询瓦斯装置实时监测数据(GasController.getGasData,deptId 必填,分页) */
+export const getGasData = (params?: any) => defHttp.post({ url: Api.getGasData, params }, { joinParamsToUrl: true });
+
+/** 查询瓦斯历史跨天分页列表(GasController.getGasHistory,startTime/endTime/gasId 必填) */
+export const getGasHistory = (params?: any) => defHttp.get({ url: Api.getGasHistory, params }, { joinParamsToUrl: true });
+
+/** 根据矿ID查询下面所有瓦斯装置信息(GasController.getGasList,供历史查询选择瓦斯装置) */
+export const getGasList = (params?: any) => defHttp.post({ url: Api.getGasList, params }, { joinParamsToUrl: true });

+ 164 - 0
src/views/monitor/gasMonitor/gasMonitor.data.ts

@@ -0,0 +1,164 @@
+import type { BasicColumn, FormSchema } from '/@/components/Table';
+import { h } from 'vue';
+import { StatusColorEnum } from '/@/enums/jeecgEnum';
+import dayjs from 'dayjs';
+import { getGasList } from './gasMonitor.api';
+
+/** 瓦斯数据状态映射(GasData.status:0正常、128传感器故障)——与风速风量的 statusMap 语义不同,单独定义 */
+export const gasStatusMap: Record<string, string> = {
+  '0': '正常',
+  '128': '传感器故障',
+};
+
+/** 瓦斯实时监测表格列(GasData,GasController.getGasData) */
+export const columns: BasicColumn[] = [
+  {
+    title: '矿井名称',
+    dataIndex: 'mineName',
+    fixed: 'left',
+    width: 180,
+  },
+  {
+    title: '测风地点',
+    dataIndex: 'mineAreaName',
+    width: 140,
+  },
+  {
+    title: '设备位置',
+    dataIndex: 'devicePos',
+    width: 200,
+  },
+  {
+    title: '瓦斯浓度(%)',
+    dataIndex: 'gas',
+    width: 120,
+  },
+  {
+    title: '数据状态',
+    dataIndex: 'status',
+    width: 120,
+    customRender: ({ text }) => {
+      const label = gasStatusMap[String(text)] ?? '-';
+      const color = String(text) === '0' ? StatusColorEnum.green : StatusColorEnum.red;
+      return h('span', { style: { color, fontWeight: 'bold' } }, label);
+    },
+  },
+  {
+    title: '数据读取时间',
+    dataIndex: 'readTime',
+    width: 170,
+  },
+];
+
+/**
+ * 瓦斯历史数据表格列。
+ * 注意:getGasHistory 后端内部调用 WindrectHistoryService,返回结构为 WindrectHistory(va 风速 / m3 风量),
+ * 因此此处按接口返回字段原样展示,与风速风量历史表格列保持一致;待后端补齐瓦斯历史表后再调整。
+ */
+export const historyColumns: BasicColumn[] = [
+  {
+    title: '矿井名称',
+    dataIndex: 'mineName',
+    fixed: 'left',
+    width: 180,
+  },
+  {
+    title: '测风地点',
+    dataIndex: 'mineAreaName',
+    width: 140,
+  },
+  {
+    title: '设备位置',
+    dataIndex: 'devicePos',
+    width: 200,
+  },
+  {
+    title: '瓦斯浓度(%)',
+    dataIndex: 'gas',
+    width: 120,
+  },
+  {
+    title: '数据状态',
+    dataIndex: 'status',
+    width: 110,
+    customRender: ({ text }) => {
+      const label = gasStatusMap[String(text)] ?? '-';
+      const color = String(text) === '0' ? StatusColorEnum.green : StatusColorEnum.red;
+      return h('span', { style: { color, fontWeight: 'bold' } }, label);
+    },
+  },
+  {
+    title: '创建时间',
+    dataIndex: 'createTime',
+    width: 170,
+  },
+];
+
+/** 实时监测查询表单(getGasData:deptId 必填) */
+export const schemas: FormSchema[] = [
+  // {
+  //   label: '瓦斯装置',
+  //   field: 'gasId',
+  //   component: 'ApiSelect',
+  //   colProps: { span: 6 },
+  //   componentProps: {
+  //     api: getGasList,
+  //     labelField: 'devicePos',
+  //     valueField: 'id',
+  //     placeholder: '请选择瓦斯装置',
+  //     params: {},
+  //   },
+  // },
+];
+
+/** 历史数据查询表单(getGasHistory:startTime/endTime/gasId 必填) */
+export const historySchemas: FormSchema[] = [
+  {
+    label: '瓦斯装置',
+    field: 'gasId',
+    component: 'ApiSelect',
+    required: true,
+    colProps: { span: 6 },
+    // 与「煤矿名称」联动:按选中矿(MineCascader 值为部门 id)过滤本矿瓦斯装置;
+    // ApiSelect 深度监听 params 变化,切矿后自动重新请求
+    componentProps: ({ formModel, formActionType }) => ({
+      api: getGasList,
+      labelField: 'devicePos',
+      valueField: 'id',
+      placeholder: '请选择瓦斯装置',
+      params: formModel?.deptId ? { deptId: formModel.deptId } : {},
+      // 筛选(挂载加载/切矿重查)后默认选中第一项,无需手动选择
+      onOptionsChange: (options) => {
+        if (options && options.length) {
+          formActionType.setFieldsValue({ gasId: options[0].value });
+        }
+      },
+    }),
+  },
+  {
+    field: 'startTime',
+    label: '开始时间',
+    component: 'DatePicker',
+    required: true,
+    componentProps: {
+      showTime: true,
+      format: 'YYYY-MM-DD HH:mm:ss',
+      valueFormat: 'YYYY-MM-DD HH:mm:ss',
+    },
+    defaultValue: dayjs().add(-30, 'day').format('YYYY-MM-DD 00:00:00'),
+    colProps: { span: 6 },
+  },
+  {
+    field: 'endTime',
+    label: '结束时间',
+    component: 'DatePicker',
+    required: true,
+    componentProps: {
+      showTime: true,
+      format: 'YYYY-MM-DD HH:mm:ss',
+      valueFormat: 'YYYY-MM-DD HH:mm:ss',
+    },
+    defaultValue: dayjs().format('YYYY-MM-DD 23:59:59'),
+    colProps: { span: 6 },
+  },
+];

+ 133 - 0
src/views/monitor/gasMonitor/index.vue

@@ -0,0 +1,133 @@
+<!-- eslint-disable vue/multi-word-component-names -->
+<template>
+  <div class="gas-monitor">
+    <!-- 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 #empty>
+            <span class="empty-placeholder">-</span>
+          </template>
+        </BasicTable>
+      </TabPane>
+
+      <TabPane tab="历史数据" key="history">
+        <!-- 说明:后端当前返回 WindrectHistory(风速/风量)字段,表格按接口返回原样展示 -->
+        <BasicTable @register="registerHistoryTable" :scroll="{ x: 'max-content' }" :style="{ padding: 0 }" />
+      </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 { useListPage } from '/@/hooks/system/useListPage';
+  import { columns, historyColumns, historySchemas, schemas } from './gasMonitor.data';
+  import { getGasData, getGasHistory } from './gasMonitor.api';
+  import { useInitForm } from '../sensorMonitor/hooks/form';
+
+  // 路由参数初始化(与风速风量监测一致:/gas/${id} 携带部门 id,query.deptId 为默认选中矿)
+  const { departId, rawcode, hiscode } = useInitForm();
+
+  // 激活的Tab页签
+  const activeTab = ref('realtime');
+
+  // ====== 实时监测表格(getGasData:deptId 必填)======
+  const { tableContext: ctxRealtime } = useListPage({
+    tableProps: {
+      immediate: false,
+      columns,
+      api: getGasData,
+      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,
+      showActionColumn: false,
+    },
+    // 导出接口后端暂未提供(无 exportGas* 接口),如需导出再补 exportConfig
+  });
+  const [registerRealtimeTable] = ctxRealtime;
+
+  // ====== 历史数据表格(getGasHistory:startTime/endTime/gasId 必填)======
+  const { tableContext } = useListPage({
+    tableProps: {
+      immediate: false,
+      columns: historyColumns,
+      api: getGasHistory,
+      formConfig: {
+        autoSearch: true,
+        labelWidth: 120,
+        model: { deptId: hiscode },
+        schemas: [
+          {
+            field: 'deptId',
+            label: '煤矿名称',
+            component: 'MineCascader',
+            colProps: { span: 6 },
+            required: true,
+            // 切换煤矿时清空已选瓦斯装置,避免提交其他矿的设备
+            componentProps: ({ formActionType }) => ({
+              initFromStore: false,
+              rootId: departId,
+              onChange: () => {
+                formActionType.setFieldsValue({ gasId: undefined });
+              },
+            }),
+          },
+          ...historySchemas,
+        ],
+        showAdvancedButton: false,
+        schemaGroupNames: ['常规查询'],
+      },
+      pagination: true,
+      striped: true,
+      useSearchForm: true,
+      bordered: true,
+      showIndexColumn: true,
+    },
+    // 导出接口后端暂未提供(无 exportGas* 接口),如需导出再补 exportConfig
+  });
+  const [registerHistoryTable] = tableContext;
+</script>
+
+<style lang="less" scoped>
+  .gas-monitor {
+    height: 100%;
+    overflow-y: auto;
+  }
+
+  .history-note {
+    margin: 4px 8px;
+  }
+
+  .empty-placeholder {
+    display: block;
+    text-align: center;
+    color: #999;
+    padding: 16px 0;
+  }
+</style>

+ 56 - 0
tests/identifyBasisGroups.spec.ts

@@ -0,0 +1,56 @@
+import { testConfigVentilationMonitor } from '../src/views/dashboard/HiddenSurface/configurable.data';
+
+/**
+ * 矿端首页「判识结果」模块 · 判定依据四步分组回归。
+ *
+ * 需求 2.1.1:判定依据按四个步骤研判(配风计划 / 测风报表 / 传感器风量 / 瓦斯校验情况),
+ * 各组指标以后端判识明细接口字段为准,当前以 ${} 取值占位(未取到值展示 '-')。
+ * 本用例锁定分组顺序、标题、占位写法与「三项概况已从首页移除」这三件事,
+ * 防止后续调整配置时误改结构。渲染逻辑(折叠、状态徽标、单位拼接)由人工走查验证。
+ */
+describe('矿端判识结果 · 判定依据四步分组配置', () => {
+  const identifyConfig = testConfigVentilationMonitor.find((c) => c.deviceType === 'identify_result');
+  const preset: any = identifyConfig?.moduleData?.preset?.[0] || {};
+
+  test('存在 identify_result 模块且数据来源仍为 identifyData', () => {
+    expect(identifyConfig).toBeTruthy();
+    expect(preset.readFrom).toBe('identifyData');
+  });
+
+  test('判定依据为四个分组,标题与顺序固定', () => {
+    const groups = preset.groups;
+    expect(Array.isArray(groups)).toBe(true);
+    expect(groups.map((g: any) => g.title)).toEqual(['配风计划', '测风报表', '传感器风量', '瓦斯校验情况']);
+    expect(groups.map((g: any) => g.key)).toEqual(['airPlan', 'report', 'windVolume', 'gasCheck']);
+  });
+
+  test('每个分组都有状态约定与指标占位项(值为 ${} 取值,不写死)', () => {
+    for (const group of preset.groups) {
+      // 组状态:0 正常 / 1 异常 / 2 数据不足
+      expect(group.status).toContain('${');
+      expect(group.trans).toMatchObject({ 0: '正常', 1: '异常', 2: '数据不足' });
+
+      expect(Array.isArray(group.items)).toBe(true);
+      expect(group.items.length).toBeGreaterThan(0);
+      for (const item of group.items) {
+        expect(typeof item.label).toBe('string');
+        expect(item.label.length).toBeGreaterThan(0);
+        expect(item.value).toContain('${');
+      }
+    }
+  });
+
+  test('原判定依据中的三项概况(风量异常数/测风地点数/测风装置数)已从首页移除', () => {
+    expect(preset.items).toBeUndefined();
+    const labels = preset.groups.flatMap((g: any) => g.items.map((i: any) => i.label));
+    expect(labels).not.toContain('风量异常数');
+    expect(labels).not.toContain('测风地点数');
+    expect(labels).not.toContain('测风装置数');
+  });
+
+  test('判识结论与建议措施未被误删', () => {
+    expect(preset.conclusion).toBe('${conclusion}');
+    expect(Array.isArray(preset.suggestions)).toBe(true);
+    expect(preset.suggestions.length).toBeGreaterThan(0);
+  });
+});

+ 36 - 0
tests/roadwayLevelNames.spec.ts

@@ -0,0 +1,36 @@
+import { levelTextMap, levelColorMap } from '../src/views/analysis/warningAnalysis/windPointManage/windTopology/windTopology.data';
+
+/**
+ * 巷道分类层级名称回归。
+ *
+ * 名称按需求文档统一:进风井 / 总进风巷 / 采区进风巷 / 工作面巷道 / 采区回风巷 / 总回风巷 / 回风井
+ * (「采区用风」已改名为「工作面巷道」,level 编码 0..6 保持不变)。
+ * 名称/颜色在拓扑 data、地点表格列、层级下拉三处各自定义,本用例锁定拓扑侧定义,
+ * 防止后续无意改名或改色导致三处展示不一致。
+ */
+describe('巷道分类层级名称与配色', () => {
+  test('level 0..6 的层级名称与新分类一致', () => {
+    expect(levelTextMap).toEqual({
+      0: '进风井',
+      1: '总进风巷',
+      2: '采区进风巷',
+      3: '工作面巷道',
+      4: '采区回风巷',
+      5: '总回风巷',
+      6: '回风井',
+    });
+  });
+
+  test('每个层级都有配色,且颜色互不重复(统一色标体系)', () => {
+    const colors = Object.values(levelColorMap);
+    expect(colors).toHaveLength(7);
+    colors.forEach((c) => expect(c).toMatch(/^#[0-9a-f]{6}$/i));
+    expect(new Set(colors).size).toBe(colors.length);
+  });
+
+  test('层级名称与配色一一对应(无缺项)', () => {
+    for (const key of Object.keys(levelTextMap)) {
+      expect(levelColorMap[Number(key)]).toBeTruthy();
+    }
+  });
+});

+ 54 - 0
tests/topologyGas.spec.ts

@@ -0,0 +1,54 @@
+import {
+  buildTopologyBundle,
+  buildRelationArray,
+  transformToTopologyData,
+} from '../src/views/analysis/warningAnalysis/windPointManage/windTopology/windTopology.data';
+import type { GasNode, WindrectNode } from '../src/views/analysis/warningAnalysis/windPointManage/windTopology/windTopology.data';
+
+/** 构造测风地点(MineArea) */
+const area = (id: string, level: number, extra: Record<string, any> = {}) => ({
+  id,
+  name: id,
+  level,
+  airVolume: 100,
+  mineCode: 'M1',
+  ...extra,
+});
+
+/** 构造巷道关系(MineAreaRelation) */
+const rel = (id: string, parentId: string, childId: string) => ({ id, mineCode: 'M1', parentId, childId });
+
+/** 构造测风装置(Windrect) */
+const windrect = (id: string): WindrectNode => ({ id, mineCode: 'M1', devicePos: `测风-${id}`, status: 1 });
+
+/** 构造瓦斯装置(Gas) */
+const gas = (id: string): GasNode => ({ id, mineCode: 'M1', devicePos: `瓦斯-${id}`, status: 1 });
+
+describe('测风地点绑定瓦斯装置(gasId)', () => {
+  test('transformToTopologyData 将 mineArea.gasId 透传到测风地点节点', () => {
+    const areas = [area('a1', 1, { windrectId: '501', gasId: '601' }), area('b2', 2)];
+    const data = transformToTopologyData(areas, buildRelationArray(areas, [rel('r1', 'a1', 'b2')]));
+
+    const a1 = data.nodes.find((n) => n.rawId === 'a1');
+    const b2 = data.nodes.find((n) => n.rawId === 'b2');
+    // 一个测风地点同时绑定测风装置与瓦斯装置;未绑定的点位 gasId 为空
+    expect(a1?.windrectId).toBe('501');
+    expect(a1?.gasId).toBe('601');
+    expect(b2?.gasId).toBeUndefined();
+  });
+
+  test('buildTopologyBundle 透传瓦斯装置全量列表(供连线标注解析设备名)', () => {
+    const areas = [area('a1', 1, { gasId: '601' })];
+    const bundle = buildTopologyBundle(areas, [], [windrect('501')], [gas('601'), gas('602')]);
+
+    expect(bundle.windrectList).toHaveLength(1);
+    expect(bundle.gasList.map((g) => g.id)).toEqual(['601', '602']);
+  });
+
+  test('buildTopologyBundle 省略瓦斯列表参数时 gasList 为空数组(兼容旧调用)', () => {
+    const bundle = buildTopologyBundle([area('a1', 1)], [], [windrect('501')]);
+
+    expect(bundle.windrectList).toHaveLength(1);
+    expect(bundle.gasList).toEqual([]);
+  });
+});