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

[Feat 0000]注气详情监测历史数据处理及表格内容显示

wangkeyi 2 месяцев назад
Родитель
Сommit
eb1920e926

+ 1 - 1
src/views/vent/home/configurable/components/gasInject/LineAndBarDetail.vue

@@ -33,7 +33,7 @@
       </div>
     </template>
     <template v-else>
-      <zqHistoryExport :tableColumn="tableColumn" @handleBack="handleBack"></zqHistoryExport>
+      <zqHistoryExport :tableColumn="tableColumn" @handleBack="handleBack" :deviceID="deviceID"></zqHistoryExport>
     </template>
   </div>
 </template>

+ 6 - 0
src/views/vent/home/configurable/components/gasInject/gasInject.api.ts

@@ -9,6 +9,7 @@ enum Api {
 getlist = '/safety/ventanalyDevicesetLog/list',
 devicecontrol='/safety/ventanalyMonitorData/devicecontrol',
 listdays = '/safety/ventanalyMonitorData/listdays',
+historydata = '/safety/ventanalyMonitorData/export/historydata',
 
 getCameraUrl = '/monitor/camera/queryByCameraCode',
 //insertSyncRule = '/ventanaly-device/synccontrol/upcoming/saveOrUpdateRule',
@@ -49,6 +50,11 @@ return defHttp.put({ url: Api.devicecontrol, params });
 * @param params
 */
 export const listdays = (params) => defHttp.get({ url: Api.listdays, params },{ joinParamsToUrl: true });
+/**
+ * 导出详情历史
+ * @param params
+ */
+export const historydata = (params) => defHttp.get({ url: Api.historydata, params }, { joinParamsToUrl: true });
 
 export const cameraAddr = (params) => defHttp.get({ url: Api.getCameraUrl, params });
 

+ 433 - 169
src/views/vent/home/configurable/components/gasInject/zqHistoryExport.vue

@@ -1,25 +1,45 @@
 <template>
   <div class="history-export">
-    <SvgIcon v-if="!isShowIcon" class="icon-style-back" size="32" name="modal-back" @click="handleBack"></SvgIcon>
+    <SvgIcon v-if="!isShowIcon" class="icon-style-back" size="32" name="modal-back" @click="handleBack" />
     <div class="nav-box">
       <div class="nav-search">
         <div class="search-item">
           <span>时间:</span>
-          <a-range-picker style="width:340px" :value="[searchData.startTime, searchData.endTime]"
-            :show-time="{ format: 'HH:mm' }" valueFormat="YYYY-MM-DD HH:mm" :placeholder="['开始时间', '结束时间']"
-            @change="onRangeChange" />
+          <a-range-picker
+            style="width: 340px"
+            :value="[searchData.startTime, searchData.endTime]"
+            :show-time="{ format: 'HH:mm' }"
+            valueFormat="YYYY-MM-DD HH:mm"
+            :placeholder="['开始时间', '结束时间']"
+            @change="onRangeChange"
+          />
+        </div>
+        <div class="search-item">
+          <span>时间间隔:</span>
+          <a-select v-model:value="searchData.skip" placeholder="请选择" style="width: 120px; margin-left: 10px" @change="onskipChange">
+            <a-select-option v-for="item in skipOptions" :key="item.value" :value="item.value">
+              {{ item.label }}
+            </a-select-option>
+          </a-select>
+        </div>
+
+        <!-- 新增:查询按钮 -->
+        <div class="search-btn-item" @click="handleSearch">
+          <div class="btn-text">
+            <!-- <SvgIcon class="icon-style" size="16" name="search" /> -->
+            <span>查询</span>
+          </div>
         </div>
       </div>
       <div class="nav-btn" @click="handleHisExport">
         <div class="nav-text">
-          <SvgIcon class="icon-style" size="18" name="gas-export"></SvgIcon>
+          <SvgIcon class="icon-style" size="18" name="gas-export" />
           <span>数据导出</span>
         </div>
       </div>
     </div>
     <div class="main-content">
-      <a-table size="small" :dataSource="dataSource" :columns="tableColumn" :scroll="{ y: 455 }"
-        :pagination="pagination">
+      <a-table size="small" :dataSource="dataSource" :columns="tableColumns" :scroll="{ y: 455 }" :pagination="pagination">
         <template #action="{ record }">
           <!-- <a class="table-action-link" @click="handleSpDetail(record)">审批详情</a> -->
         </template>
@@ -29,194 +49,438 @@
 </template>
 
 <script setup lang="ts">
-import { reactive, ref, onMounted } from 'vue'
-import { SvgIcon } from '/@/components/Icon';
-import { getList } from './gasInject.api'
-import dayjs from 'dayjs';
-
-let props = defineProps({
-  //是否显示返回按钮
-  isShowIcon: {
-    type: Boolean
-  },
-  tableColumn: {
-    type: Array
-  }
-})
-
-let searchData = reactive({
-  startTime: dayjs().subtract(1, 'day').format('YYYY-MM-DD HH:mm'),
-  endTime: dayjs().format('YYYY-MM-DD HH:mm'),
-
-})
-let dataSource = ref<any[]>([
-  // { fileName: '测试' },
-])
-//分页参数配置
-let pagination = reactive({
-  current: 1, // 当前页码
-  pageSize: 15, // 每页显示条数
-  total: 0, // 总条目数,后端返回
-  // showTotal: (total, range) => `${range[0]}-${range[1]} 条,总共 ${total} 条`, // 分页右下角显示信息
-  showSizeChanger: true, // 是否可改变每页显示条数
-  pageSizeOptions: ['15', '20', '25', '30', '50'], // 可选的每页显示条数
-});
-let $emit = defineEmits(['handleBack'])
-
-//返回上一级
-function handleBack() {
-  $emit('handleBack')
-}
-
-//注气详情table数据
-async function getHistoryTableData() {
-  let res = await getList({ pageNo: pagination.current, pageSize: pagination.pageSize, createTime_begin: searchData.startTime, createTime_end: searchData.endTime, devicetype: 'injection_standard' })
-  console.log(res, '装备运行状态与控制')
-  dataSource.value = res.records || []
-  pagination.total = res.total
-}
-//导出历史数据
-function handleHisExport() { }
-//日期切换
-function onRangeChange(__, time) {
-  searchData.startTime = time[0];
-  searchData.endTime = time[1];
-}
-
-onMounted(() => {
-  getHistoryTableData()
-})
-</script>
+  import { reactive, ref, onMounted } from 'vue';
+  import { SvgIcon } from '/@/components/Icon';
+  import { getList, listdays, historydata } from './gasInject.api';
+  import dayjs from 'dayjs';
+  import { BasicColumn } from '/@/components/Table';
 
-<style lang="less" scoped>
-@import '/@/design/theme.less';
+  let props = defineProps({
+    //是否显示返回按钮
+    isShowIcon: {
+      type: Boolean,
+    },
+    tableColumn: {
+      type: Array,
+    },
+    deviceID: {
+      type: String,
+    },
+  });
+  // 设备列表显示字段
+  const DEFAULT_TOTAL_DEVICE_FIELDS = [
+    { key: 'injectionPressure', title: '注入压力' },
+    { key: 'flowWorkInstant', title: '工作瞬时流量' },
+    { key: 'flowStdAccum', title: '标准累计流量' },
+    { key: 'flowStdInstant', title: '标准瞬时流量' },
+    { key: 'flowWorkAccum', title: '工作累计流量' },
+  ];
+  // 字段名 -> 中文标题 映射表
+  const fieldTitleMap = Object.fromEntries(DEFAULT_TOTAL_DEVICE_FIELDS.map((item) => [item.key, item.title]));
+  // 定义时间间隔选项
+  const skipOptions = [
+    { label: '1秒', value: 1 },
+    { label: '5秒', value: 2 },
+    { label: '10秒', value: 3 },
+    { label: '30秒', value: 4 },
+    { label: '1分钟', value: 5 },
+    { label: '10分钟', value: 6 },
+    { label: '30分钟', value: 7 },
+  ];
 
-@{theme-deepblue} {
-  .history-export {
-    --image-inject_zq_monitor: url('@/assets/images/themify/deepblue/home-container/configurable/gasInjection/9-3.png');
+  let searchData = reactive({
+    startTime: dayjs().subtract(1, 'day').format('YYYY-MM-DD HH:mm:ss'),
+    endTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
+    skip: 5, // 存储选中的间隔值
+  });
+  let dataSource = ref<any[]>([
+    // { fileName: '测试' },
+  ]);
+  let tableColumns = ref<BasicColumn[]>([]);
+  //分页参数配置
+  let pagination = reactive({
+    current: 1, // 当前页码
+    pageSize: 15, // 每页显示条数
+    total: 0, // 总条目数,后端返回
+    // showTotal: (total, range) => `${range[0]}-${range[1]} 条,总共 ${total} 条`, // 分页右下角显示信息
+    showSizeChanger: true, // 是否可改变每页显示条数
+    pageSizeOptions: ['15', '20', '25', '30', '50'], // 可选的每页显示条数
+  });
+  let $emit = defineEmits(['handleBack']);
+
+  //返回上一级
+  function handleBack() {
+    $emit('handleBack');
+  }
+
+  //注气详情table数据
+  async function getHistoryTableData() {
+    let res = await listdays({
+      ttime_begin: searchData.startTime,
+      ttime_end: searchData.endTime,
+      pageNo: pagination.current,
+      pageSize: pagination.pageSize,
+      skip: searchData.skip,
+      gdeviceids: props.deviceID,
+      strtype: 'injection_standard',
+    });
+    dataSource.value = transformTableData(res.datalist.records);
+    tableColumns.value = generateTableColumns(dataSource.value);
   }
-}
+  //导出历史数据
+  async function handleHisExport() {
+    let res = await historydata({
+      ttime_begin: searchData.startTime,
+      ttime_end: searchData.endTime,
+      pageNo: pagination.current,
+      pageSize: pagination.pageSize,
+      skip: searchData.skip,
+      gdeviceids: props.deviceID,
+      strtype: 'injection_standard',
+    });
+  }
+  //日期切换
+  function onRangeChange(__, time) {
+    searchData.startTime = time[0];
+    searchData.endTime = time[1];
+  }
+  // 时间间隔切换
+  function onskipChange(value: number) {
+    // 可以在此调用 handleSearch()
+  }
+  // 查询操作
+  function handleSearch() {
+    getHistoryTableData();
+  }
+
+  /**
+   * 将原始 records 转换为表格所需的扁平化数据,按设备分组
+   * @param records 原始数据列表
+   */
+  function transformTableData(records: any[]) {
+    if (!records || records.length === 0) return [];
+
+    // 1. 收集所有子设备号(从readData字段末尾的数字提取)和对应的基础字段
+    const allDevices = new Set<number>(); // 所有子设备号(字段末尾的数字)
+    const deviceFieldMap = new Map<number, Set<string>>(); // 设备号 -> 该设备对应的字段名集合
+    const baseFields = new Set<string>(); // 无数字后缀的基础字段
+
+    // 遍历所有记录,提取设备号和字段映射
+    records.forEach((record) => {
+      const readData = record.readData;
+      if (!readData) return;
+
+      Object.keys(readData).forEach((fieldKey) => {
+        // 匹配字段末尾的数字(支持下划线/直接数字结尾,如 flowStdAccum_1 或 flowStdAccum1)
+        const numMatch = fieldKey.match(/(\d+)$/); // 匹配末尾的数字
+        // const separatorMatch = fieldKey.match(/(.+?)[_-](\d+)$/); // 匹配 "字段_数字" 格式(兼容下划线/短横线分隔)
+
+        if (numMatch) {
+          // 字段末尾有数字 → 归为子设备字段
+          const deviceNo = parseInt(numMatch[1], 10);
+          allDevices.add(deviceNo);
+
+          // 提取「纯字段名」(去掉末尾的数字/分隔符)
+          // const pureField = separatorMatch ? separatorMatch[1] : fieldKey.replace(numMatch[0], '');
 
-.history-export {
-  --image-inject_zq_monitor: url('@/assets/images/gasInjection/9-3.png');
-  position: relative;
-  width: 100%;
-  height: 100%;
+          // 记录该设备对应的字段
+          if (!deviceFieldMap.has(deviceNo)) {
+            deviceFieldMap.set(deviceNo, new Set<string>());
+          }
+          deviceFieldMap.get(deviceNo)!.add(fieldKey); // 直接存原始字段名(如 flowStdAccum_1)
+        } else {
+          // 字段末尾无数字 → 归为基础字段
+          baseFields.add(fieldKey);
+        }
+      });
+    });
 
-  .icon-style-back {
-    position: absolute;
-    left: 6px;
-    top: -44px;
-    cursor: pointer;
+    // 格式转换:Set → 排序后的数组
+    const deviceList = Array.from(allDevices).sort((a, b) => a - b);
+
+    // 2. 构建最终的表格数据
+    const result: any[] = [];
+
+    records.forEach((record) => {
+      const { gcreatetime, gdevicename, ginstallpos, gdeviceid } = record;
+      const readData = record.readData || {};
+
+      // 基础行数据(固定字段)
+      const rowItem: any = {
+        key: record.id,
+        time: gcreatetime,
+        deviceName: gdevicename,
+        installPos: ginstallpos,
+        deviceId: gdeviceid,
+      };
+
+      // 3. 处理子设备数据(仅字段末尾带数字的字段)
+      deviceList.forEach((deviceNo) => {
+        const deviceKey = `subDevice${deviceNo}`;
+        rowItem[deviceKey] = {};
+
+        // 获取该设备对应的所有字段,赋值
+        const deviceFields = deviceFieldMap.get(deviceNo) || new Set<string>();
+        deviceFields.forEach((fieldKey) => {
+          const value = readData[fieldKey];
+          rowItem[deviceKey][fieldKey] = value !== null && value !== undefined ? value : '-';
+        });
+      });
+      // 合并:基础字段 + 核心字段(确保核心字段必存在)
+      const allBaseFields = new Set([...baseFields, ...DEFAULT_TOTAL_DEVICE_FIELDS.map((item) => item.key)]);
+      allBaseFields.forEach((fieldKey) => {
+        const value = readData[fieldKey];
+        rowItem[fieldKey] = value !== null && value !== undefined ? value : '-';
+      });
+
+      result.push(rowItem);
+    });
+
+    return result;
   }
+  function generateTableColumns(data): BasicColumn[] {
+    // 1. 固定列
+    const fixedColumns: BasicColumn[] = [
+      {
+        title: '序号',
+        width: 60,
+        align: 'center',
+        fixed: 'left',
+        customRender: ({ index }: { index: number }) => `${index + 1}`,
+      },
+      {
+        title: '时间',
+        dataIndex: 'time',
+        key: 'time',
+        align: 'center',
+        fixed: 'left',
+        width: 120,
+      },
+    ];
 
-  .nav-box {
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-    width: 100%;
-    height: 39px;
-    padding: 0px 20px;
-    margin-bottom: 15px;
-    background: var(--image-inject_zq_monitor) no-repeat;
-    background-size: 100% 100%;
+    if (!data || data.length === 0) return fixedColumns;
+
+    // 2. 动态生成子设备分组列
+    // 取第一行数据作为样本分析结构
+    const sampleRow = data[0];
+
+    // 提取所有以 subDevice 开头的 key (如 ['subDevice1', 'subDevice2'])
+    const subDeviceKeys = Object.keys(sampleRow).filter((key) => key.startsWith('subDevice'));
+    // 如果没有以 subDevice 开头的 key,则生成总设备列
+    if (subDeviceKeys.length === 0) {
+      const totalDeviceColumns = DEFAULT_TOTAL_DEVICE_FIELDS.map((item) => ({
+        title: item.title,
+        dataIndex: item.key,
+        key: item.key,
+        align: 'center',
+        width: 140,
+        ellipsis: true,
+      }));
+
+      return [...fixedColumns, ...totalDeviceColumns];
+    }
+
+    // 遍历每个 subDevice 生成分组
+    const dynamicColumns = subDeviceKeys
+      .map((deviceKey) => {
+        // 获取该子设备下的所有数据字段 (如 { flowStdInstant_1: "118.40" ... })
+        const deviceDataSample = sampleRow[deviceKey];
+        if (!deviceDataSample) return null;
+
+        // 获取该子设备下的所有字段名 (如 ['flowStdInstant_1', 'injectionPressure_1' ...])
+        const fieldKeys = Object.keys(deviceDataSample);
+
+        // 生成 children 子列
+        const childrenColumns = fieldKeys.map((fieldKey) => {
+          // 处理显示标题:去掉末尾的 "_1", "_2" 后缀,让表头好看点
+          // 比如 "flowStdInstant_1" -> "flowStdInstant"
+          const title = fieldKey.replace(/_\d+$/, '');
+          // 优化:提取纯字段名(兼容 flowStdInstant_1 / flowStdInstant1 两种格式)
+          let pureFieldKey = '';
+          // 匹配 "字段_数字" 格式(如 flowStdInstant_1)
+          const separatorMatch = fieldKey.match(/(.+?)[_-](\d+)$/);
+          // 匹配 "字段数字" 格式(如 flowStdInstant1)
+          const directNumMatch = fieldKey.match(/(.+?)(\d+)$/);
+
+          if (separatorMatch) {
+            pureFieldKey = separatorMatch[1]; // 提取 flowStdInstant
+          } else if (directNumMatch) {
+            pureFieldKey = directNumMatch[1]; // 提取 flowStdInstant
+          } else {
+            pureFieldKey = fieldKey; // 兜底(无数字后缀)
+          }
+
+          // 核心:通过映射表获取中文标题,兜底用原字段名
+          const columnTitle = fieldTitleMap[pureFieldKey] || pureFieldKey;
+
+          return {
+            title: columnTitle,
+            dataIndex: [deviceKey, fieldKey], //对应嵌套结构:row[deviceKey][fieldKey]
+            key: `${deviceKey}-${fieldKey}`,
+            align: 'center',
+            width: 140,
+            ellipsis: true,
+          };
+        });
+
+        return {
+          title: `注气子设备${deviceKey.replace('subDevice', '')}`, // 分组标题
+          key: deviceKey,
+          align: 'center',
+          children: childrenColumns,
+        };
+      })
+      .filter(Boolean); // 过滤掉空值
+
+    return [...fixedColumns, ...dynamicColumns];
+  }
+
+  onMounted(() => {
+    getHistoryTableData();
+  });
+</script>
+
+<style lang="less" scoped>
+  @import '/@/design/theme.less';
+
+  @{theme-deepblue} {
+    .history-export {
+      --image-inject_zq_monitor: url('@/assets/images/themify/deepblue/home-container/configurable/gasInjection/9-3.png');
+    }
   }
 
-  .nav-search {
-    display: flex;
-    align-items: center;
+  .history-export {
+    --image-inject_zq_monitor: url('@/assets/images/gasInjection/9-3.png');
+    position: relative;
+    width: 100%;
     height: 100%;
-    color: #fff;
+
+    .icon-style-back {
+      position: absolute;
+      left: 6px;
+      top: -44px;
+      cursor: pointer;
+    }
+
+    .nav-box {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      width: 100%;
+      height: 39px;
+      padding: 0px 20px;
+      margin-bottom: 15px;
+      background: var(--image-inject_zq_monitor) no-repeat;
+      background-size: 100% 100%;
+    }
+
+    .nav-search {
+      display: flex;
+      align-items: center;
+      height: 100%;
+      color: #fff;
+    }
+
+    .search-item {
+      margin-right: 40px;
+    }
+    .search-btn-item {
+      display: flex;
+      width: 100px;
+      height: 78%;
+      padding: 2px;
+      cursor: pointer;
+      justify-content: center;
+      align-items: center;
+      background-color: #1081d7;
+      border-radius: 4px;
+    }
+
+    .nav-btn {
+      width: 100px;
+      height: 78%;
+      padding: 2px;
+      border: 1px solid #1081d7;
+      cursor: pointer;
+      color: #fff;
+    }
+
+    .nav-text {
+      width: 100%;
+      height: 100%;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      background-color: #1081d7;
+    }
+
+    .icon-style {
+      margin-right: 5px;
+    }
+
+    .main-content {
+      display: flex;
+      justify-content: space-between;
+      width: 100%;
+      height: calc(100% - 54px);
+      min-height: 500px;
+    }
   }
 
-  .search-item {
-    margin-right: 40px;
+  :deep(.zxm-picker) {
+    background: #0b223f !important;
+    border: 1px solid #1081d7 !important;
+    border-radius: 4px;
+    margin-left: 10px;
   }
 
-  .nav-btn {
-    width: 100px;
-    height: 78%;
-    padding: 2px;
-    border: 1px solid #1081d7;
-    cursor: pointer;
+  :deep(.zxm-picker-input > input) {
     color: #fff;
   }
 
-  .nav-text {
-    width: 100%;
-    height: 100%;
-    display: flex;
-    justify-content: center;
-    align-items: center;
-    background-color: #1081d7;
+  :deep(.zxm-picker-separator) {
+    color: #fff;
   }
 
-  .icon-style {
-    margin-right: 5px;
+  :deep(.zxm-select) {
+    color: #fff !important;
   }
 
-  .main-content {
-    display: flex;
-    justify-content: space-between;
-    width: 100%;
-    height: calc(100% - 54px);
-  }
-}
-
-:deep(.zxm-picker) {
-  background: #0b223f !important;
-  border: 1px solid #1081d7 !important;
-  border-radius: 4px;
-  margin-left: 10px;
-}
-
-:deep(.zxm-picker-input > input) {
-  color: #fff;
-}
-
-:deep(.zxm-picker-separator) {
-  color: #fff;
-}
-
-:deep(.zxm-select) {
-  color: #fff !important;
-}
-
-:deep(.zxm-select-selector) {
-  background: #0b223f !important;
-  border: 1px solid #1081d7 !important;
-  border-radius: 4px;
-  margin-left: 10px;
-}
-
-
-:deep(.zxm-table-header) {
-  border-left: 1px solid #2896ca !important;
-  border-right: 1px solid #2896ca !important;
-}
-
-:deep(.zxm-table-thead > tr > th:last-child) {
-  border-right: 1px solid #2896ca !important;
-}
-
-:deep(.zxm-table-tbody > tr) {
-  &:nth-child(odd) {
-    td {
-      background-color: #0d2c50 !important;
-    }
+  :deep(.zxm-select-selector) {
+    background: #0b223f !important;
+    border: 1px solid #1081d7 !important;
+    border-radius: 4px;
+    margin-left: 10px;
+  }
 
+  :deep(.zxm-table-header) {
+    border-left: 1px solid #2896ca !important;
+    border-right: 1px solid #2896ca !important;
   }
 
-  &:nth-child(even) {
-    td {
-      background-color: #0d233f !important;
+  :deep(.zxm-table-thead > tr > th:last-child) {
+    border-right: 1px solid #2896ca !important;
+  }
+  :deep(.zxm-table-cell-fix-left) {
+    background-color: var(--vent-table-thead) !important;
+  }
+
+  :deep(.zxm-table-tbody > tr) {
+    &:nth-child(odd) {
+      td {
+        background-color: #0d2c50 !important;
+      }
     }
 
+    &:nth-child(even) {
+      td {
+        background-color: #0d233f !important;
+      }
+    }
   }
-}
 
-:deep(.zxm-table-tbody > tr > td) {
-  border-color: transparent !important;
-  background: #0a2140 !important;
-}
+  :deep(.zxm-table-tbody > tr > td) {
+    border-color: transparent !important;
+    background: #0a2140 !important;
+  }
 </style>