Преглед на файлове

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

lxh преди 5 месеца
родител
ревизия
3cc7134d5a

+ 2 - 4
src/components/AIChat/MiniChat.vue

@@ -96,9 +96,9 @@
                         >思考过程:<RightOutlined v-if="!message.isShowThink" /> <DownOutlined v-if="message.isShowThink"
                       /></span>
                     </div>
-                    <div v-show="message.isShowThink" class="color-gray font-size-12px" v-html="message.parsedContentR1"></div>
+                    <div v-show="message.isShowThink" class="color-gray font-size-12px" v-html="formatMessage(message.parsedContentR1)"></div>
                   </div>
-                  <div v-if="message.parsedContent" v-html="message.parsedContent"> </div>
+                  <div v-if="message.parsedContent" v-html="formatMessage(message.parsedContent)"> </div>
                 </div>
                 <div class="copy-icon-container">
                   <CopyOutlined class="copy-icon" @click="copyToClipboard(message.parsedContent)" title="复制消息" />
@@ -362,8 +362,6 @@ const renderLatexInHtml = (html) => {
   const inlineRegex = /\$(.*?)\$/g;
   // // 替换块级公式(居中显示)
   html = html.replace(blockRegex, (match, formula) => {
-    matchCount++;
-    console.log(`块级公式匹配第 ${matchCount} 次:`, formula); // 查看匹配次数和内容
     return katex.renderToString(formula.trim(), {
       displayMode: true,
       throwOnError: false,

+ 9 - 2
src/views/monitor/quartz/QuartzModal1.vue

@@ -8,11 +8,12 @@ import { ref, computed, unref } from 'vue';
 import { BasicModal, useModalInner } from '/@/components/Modal';
 import { BasicForm, useForm } from '/@/components/Form/index';
 import { formSchema } from './quartz.data';
-import { dataCenterSaveOrUpdateQuartz, getDataCenterQuartzById } from './quartz.api';
+import { dataCenterSaveQuartz, dataCenterUpdateQuartz } from './quartz.api';
 import { isJsonObjectString } from '/@/utils/is';
 // Emits声明
 const emit = defineEmits(['register', 'success']);
 const isUpdate = ref(true);
+const sysType = ref('');
 //表单配置
 const [registerForm, { resetFields, setFieldsValue, validate }] = useForm({
   // labelWidth: 150,
@@ -35,6 +36,8 @@ const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data
     //data.record = await getQuartzById({id: data.record.id});
     try {
       data.record.paramterType = isJsonObjectString(data?.record?.parameter) ? 'json' : 'string';
+      sysType.value = data.record.sysType;
+      console.log(sysType.value, '123123');
     } catch (e) {
       console.log(e);
     }
@@ -52,7 +55,11 @@ async function handleSubmit(v) {
     let values = await validate();
     setModalProps({ confirmLoading: true });
     //提交表单
-    await dataCenterSaveOrUpdateQuartz(values, isUpdate.value);
+    if (isUpdate.value) {
+      await dataCenterUpdateQuartz(values, sysType.value, 'edit');
+    } else {
+      await dataCenterSaveQuartz(values, sysType.value, 'save');
+    }
     //关闭弹窗
     closeModal();
     //刷新列表

+ 2 - 2
src/views/monitor/quartz/index1.vue

@@ -48,7 +48,7 @@ import {
   pauseDataCenterJob,
   excuteDataCenterJob,
 } from './quartz.api';
-import { columns, searchFormSchema } from './quartz.data';
+import { dataCenterColumns, searchFormSchema } from './quartz.data';
 import QuartzModal from './QuartzModal1.vue';
 import { useMessage } from '/@/hooks/web/useMessage';
 
@@ -60,7 +60,7 @@ const { prefixCls, tableContext, onExportXls, onImportXls } = useListPage({
   tableProps: {
     title: '任务列表',
     api: getDataCenterList,
-    columns: columns,
+    columns: dataCenterColumns,
     actionColumn: {
       width: 180,
     },

+ 21 - 12
src/views/monitor/quartz/quartz.api.ts

@@ -119,18 +119,6 @@ export const getDataCenterList = (params) =>
     params,
   });
 
-/**
- * 保存或者更新任务
- * @param params
- */
-export const dataCenterSaveOrUpdateQuartz = (params, isUpdate) => {
-  let url = isUpdate ? Api.dataCenteredit : Api.dataCentersave;
-  return defHttp.post({
-    url: url,
-    params,
-  });
-};
-
 /**
  * 查询任务详情
  * @param params
@@ -230,3 +218,24 @@ export const excuteDataCenterJob = (params: any, sysType: String, operation: Str
       handleSuccess();
     });
 };
+
+/**
+ * 保存或者更新任务
+ * @param params
+ */
+export const dataCenterUpdateQuartz = (params, sysType: String, operation: String) => {
+  const currentModule = sysType;
+  const apiPath = generateApiPath(currentModule, operation);
+  return defHttp.post({
+    url: apiPath,
+    params,
+  });
+};
+export const dataCenterSaveQuartz = (params, sysType: String, operation: String) => {
+  const currentModule = sysType;
+  const apiPath = generateApiPath(currentModule, operation);
+  return defHttp.post({
+    url: apiPath,
+    params,
+  });
+};

+ 39 - 1
src/views/monitor/quartz/quartz.data.ts

@@ -34,6 +34,43 @@ export const columns: BasicColumn[] = [
     },
   },
 ];
+export const dataCenterColumns: BasicColumn[] = [
+  {
+    title: '任务类名',
+    dataIndex: 'jobClassName',
+    width: 200,
+    align: 'left',
+  },
+  {
+    title: 'Cron表达式',
+    dataIndex: 'cronExpression',
+    width: 200,
+  },
+  {
+    title: '参数',
+    dataIndex: 'parameter',
+    width: 200,
+  },
+  {
+    title: '系统类型',
+    dataIndex: 'sysType_dictText',
+    width: 200,
+  },
+  {
+    title: '描述',
+    dataIndex: 'description',
+    width: 200,
+  },
+  {
+    title: '状态',
+    dataIndex: 'status',
+    width: 100,
+    customRender: ({ text }) => {
+      const color = text == '0' ? 'green' : text == '-1' ? 'red' : 'gray';
+      return render.renderTag(render.renderDict(text, 'quartz_status'), color);
+    },
+  },
+];
 
 export const searchFormSchema: FormSchema[] = [
   {
@@ -69,12 +106,13 @@ export const formSchema: FormSchema[] = [
   },
   {
     label: '系统类型',
-    field: 'jobSysType',
+    field: 'sysType',
     component: 'JDictSelectTag',
     componentProps: {
       dictCode: 'jobSysType',
       placeholder: '请选择系统类型',
     },
+    required: true,
   },
   {
     field: 'cronExpression',

+ 299 - 299
src/views/vent/comment/history/HistoryTable.vue

@@ -18,336 +18,336 @@
 </template>
 
 <script lang="ts" setup>
-  // 场景类历史数据公共组件!
-  // 用于服务带子设备的设备历史数据业务,这类数据通常由一条数据返回多个子设备的信息,例如:{ forcFan1Temp, forcFan2Temp };
-  // 而此组件可以将这些数据分类,例如:表格只有 温度 一列,但可以根据所选的子设备展示子设备1的数据:forcFan1Temp;
-  // 综上所述,此组件在基础的历史数据组件上添加了子设备下拉框(由字典驱动),用户选择子设备后表头将动态调整以适配数据展示;
-  //
-  // 使用方法如下:
-  // 设有历史数据2条,[{ name, forcFan1Temp, forcFan2Temp }, { name, forcFan1Temp, forcFan2Temp }]。
-  //
-  // 1、配置设备字段(参考公司端综合设备管理-设备字段管理)
-  // 以压风机为例,设压风机设备的历史数据编码为forcFan_history。
-  // 那么需要把所有字段悉数配置,例子如下:
-  //  显示字段    字段code
-  //   温度     forcFanTemp
-  //  安装位置      name
-  //
-  // 2、配置数据字典(参考系统管理-数据字典),为上述设备配置子设备
-  // 同以压风机为例,设压风机子设备字典编码为forcFan_dict,且已经新增到系统中。
-  // 则字典配置的例子如下:
-  //  名称    数据值
-  // 压风机1 forcFan1
-  // 压风机2 forcFan2
-  //
-  // 3、运维人员应配合前端开发人员,使用指定的编码配置内容。
-  // 同以压风机为例,需使用device-code(forcFan)、dict-code(forcFan_dict)。
-  //
-  // 4、其他内容说明
-  // 同以压风机为例,当子设备没有数据时,不进行过滤,此时展示的数据是:
-  //      温度    安装位置
-  // 取forcFanTemp    取name
-  // 同以压风机为例,当子设备选择压风机1时,过滤压风机1相关的表头,此时展示的数据是:
-  //      温度        安装位置
-  // 取forcFan1Temp    取name
-  // 当设备字段不含数据字典关键词(forcFan)时不做处理,当设备字段包含关键词但已指定编号(即字段包含数字)时不做处理
-  //
-  // 5、历史数据模式选择
-  // 历史数据的请求分为redis分站模式、其他模式,redis分站模式将不支持子设备选择,参考基础的历史数据组件逻辑(本组件已做兼容)
-  // 历史数据查询模式分为多设备查询模式、单设备查询模式,单设备查询模式将不支持设备多选,两种模式本组件都支持
-  import { computed, onMounted, ref, shallowRef } from 'vue';
-  import { BasicColumn, PaginationProps, BasicTable } from '/@/components/Table';
-  import { getTableHeaderColumns } from '/@/hooks/web/useWebColumns';
-  import { getDefaultSchemas } from './history.data';
-  import { adaptFormData, getDeviceList, getExportUrl, list } from './history.api';
-  import { useListPage } from '/@/hooks/system/useListPage';
-  import { initDictOptions } from '/@/utils/dict';
+// 场景类历史数据公共组件!
+// 用于服务带子设备的设备历史数据业务,这类数据通常由一条数据返回多个子设备的信息,例如:{ forcFan1Temp, forcFan2Temp };
+// 而此组件可以将这些数据分类,例如:表格只有 温度 一列,但可以根据所选的子设备展示子设备1的数据:forcFan1Temp;
+// 综上所述,此组件在基础的历史数据组件上添加了子设备下拉框(由字典驱动),用户选择子设备后表头将动态调整以适配数据展示;
+//
+// 使用方法如下:
+// 设有历史数据2条,[{ name, forcFan1Temp, forcFan2Temp }, { name, forcFan1Temp, forcFan2Temp }]。
+//
+// 1、配置设备字段(参考公司端综合设备管理-设备字段管理)
+// 以压风机为例,设压风机设备的历史数据编码为forcFan_history。
+// 那么需要把所有字段悉数配置,例子如下:
+//  显示字段    字段code
+//   温度     forcFanTemp
+//  安装位置      name
+//
+// 2、配置数据字典(参考系统管理-数据字典),为上述设备配置子设备
+// 同以压风机为例,设压风机子设备字典编码为forcFan_dict,且已经新增到系统中。
+// 则字典配置的例子如下:
+//  名称    数据值
+// 压风机1 forcFan1
+// 压风机2 forcFan2
+//
+// 3、运维人员应配合前端开发人员,使用指定的编码配置内容。
+// 同以压风机为例,需使用device-code(forcFan)、dict-code(forcFan_dict)。
+//
+// 4、其他内容说明
+// 同以压风机为例,当子设备没有数据时,不进行过滤,此时展示的数据是:
+//      温度    安装位置
+// 取forcFanTemp    取name
+// 同以压风机为例,当子设备选择压风机1时,过滤压风机1相关的表头,此时展示的数据是:
+//      温度        安装位置
+// 取forcFan1Temp    取name
+// 当设备字段不含数据字典关键词(forcFan)时不做处理,当设备字段包含关键词但已指定编号(即字段包含数字)时不做处理
+//
+// 5、历史数据模式选择
+// 历史数据的请求分为redis分站模式、其他模式,redis分站模式将不支持子设备选择,参考基础的历史数据组件逻辑(本组件已做兼容)
+// 历史数据查询模式分为多设备查询模式、单设备查询模式,单设备查询模式将不支持设备多选,两种模式本组件都支持
+import { computed, onMounted, ref, shallowRef } from 'vue';
+import { BasicColumn, PaginationProps, BasicTable } from '/@/components/Table';
+import { getTableHeaderColumns } from '/@/hooks/web/useWebColumns';
+import { getDefaultSchemas } from './history.data';
+import { adaptFormData, getDeviceList, getExportUrl, list } from './history.api';
+import { useListPage } from '/@/hooks/system/useListPage';
+import { initDictOptions } from '/@/utils/dict';
 
-  const props = withDefaults(
-    defineProps<{
-      /** 表格项配置,默认由deviceCode获取且联动dictCode,可以覆写,覆写后将不支持联动,参考BaiscTable */
-      // columns?: BasicColumn[];
-      /** 表格操作项配置,默认为空,可以覆写 */
-      // actionColumns?: BasicColumn;
-      /** 查询表单项配置,默认联动dictCode,可以覆写,覆写后将不支持联动,提供formProps时此项无效,参考BaiscTable */
-      // schemas?: FormSchema[];
-      /** 表格分页配置,可以覆写,参考BaiscTable */
-      pagination?: PaginationProps;
-      /** 设备编码,该编码用于请求设备信息,示例:forcFan */
-      deviceCode: string;
-      /** 字典编码,该编码用于从字典配置中读出设备项,示例:forcFan_dict */
-      dictCode: string;
-      /** 字段编码,该编码用于从设备字段配置中读取默认表头信息,示例:forcFan_history */
-      columnsCode: string;
+const props = withDefaults(
+  defineProps<{
+    /** 表格项配置,默认由deviceCode获取且联动dictCode,可以覆写,覆写后将不支持联动,参考BaiscTable */
+    // columns?: BasicColumn[];
+    /** 表格操作项配置,默认为空,可以覆写 */
+    // actionColumns?: BasicColumn;
+    /** 查询表单项配置,默认联动dictCode,可以覆写,覆写后将不支持联动,提供formProps时此项无效,参考BaiscTable */
+    // schemas?: FormSchema[];
+    /** 表格分页配置,可以覆写,参考BaiscTable */
+    pagination?: PaginationProps;
+    /** 设备编码,该编码用于请求设备信息,示例:forcFan */
+    deviceCode: string;
+    /** 字典编码,该编码用于从字典配置中读出设备项,示例:forcFan_dict */
+    dictCode: string;
+    /** 字段编码,该编码用于从设备字段配置中读取默认表头信息,示例:forcFan_history */
+    columnsCode: string;
 
-      scroll: { x: number | true; y: number };
-      /** 表格配置,参考BaiscTable,该值会与默认的配置进行浅合并,这里提供的任何配置都是优先的 */
-      // tableProps?: BasicTableProps;
-      /** 查询表单配置,参考BaiscTable */
-      // formProps?: FormProps;
-    }>(),
-    {
-      deviceCode: '',
-      dictCode: '',
-      pagination: (): PaginationProps => ({
-        current: 1,
-        pageSize: 10,
-        pageSizeOptions: ['10', '30', '50', '100'],
-        showQuickJumper: false,
-      }),
-    }
-  );
+    scroll: { x: number | true; y: number };
+    /** 表格配置,参考BaiscTable,该值会与默认的配置进行浅合并,这里提供的任何配置都是优先的 */
+    // tableProps?: BasicTableProps;
+    /** 查询表单配置,参考BaiscTable */
+    // formProps?: FormProps;
+  }>(),
+  {
+    deviceCode: '',
+    dictCode: '',
+    pagination: (): PaginationProps => ({
+      current: 1,
+      pageSize: 10,
+      pageSizeOptions: ['10', '30', '50', '100'],
+      showQuickJumper: false,
+    }),
+  }
+);
 
-  // 未经过处理的原始表头,即项目配置的表头
-  let originColumns: BasicColumn[] = [];
-  // 表格数据
-  const data = shallowRef([]);
+// 未经过处理的原始表头,即项目配置的表头
+let originColumns: BasicColumn[] = [];
+// 表格数据
+const data = shallowRef([]);
 
-  const { tableContext, onExportXls, onExportXlsPost } = useListPage({
-    tableProps: {
-      columns: [
-        {
-          align: 'center',
-          dataIndex: 'strinstallpos',
-          defaultHidden: false,
-          title: '安装位置',
-          width: 80,
-        },
-      ],
-      formConfig: {
-        labelAlign: 'left',
-        labelWidth: 80,
-        showAdvancedButton: false,
-        showSubmitButton: false,
-        showResetButton: false,
-        actionColOptions: {
-          xxl: 4,
-        },
+const { tableContext, onExportXls, onExportXlsPost } = useListPage({
+  tableProps: {
+    columns: [
+      {
+        align: 'center',
+        dataIndex: 'strinstallpos',
+        defaultHidden: false,
+        title: '安装位置',
+        width: 80,
+      },
+    ],
+    formConfig: {
+      labelAlign: 'left',
+      labelWidth: 80,
+      showAdvancedButton: false,
+      showSubmitButton: false,
+      showResetButton: false,
+      actionColOptions: {
+        xxl: 4,
       },
-      canResize: true,
-      showTableSetting: false,
-      showActionColumn: false,
-      bordered: false,
-      size: 'small',
-      showIndexColumn: true,
-      tableLayout: 'auto',
-      scroll: computed(() => {
-        return { ...props.scroll, y: props.scroll.y - 100 };
-      }),
-      pagination: props.pagination,
-    },
-    exportConfig: {
-      name: '设备历史列表',
-      url: () => getExportUrl(deviceInfo.value),
     },
-  });
-  const [register, { getForm, setLoading, getPaginationRef, setPagination, setColumns }] = tableContext;
+    canResize: true,
+    showTableSetting: false,
+    showActionColumn: false,
+    bordered: false,
+    size: 'small',
+    showIndexColumn: true,
+    tableLayout: 'auto',
+    scroll: computed(() => {
+      return { ...props.scroll, y: props.scroll.y - 100 };
+    }),
+    pagination: props.pagination,
+  },
+  exportConfig: {
+    name: '设备历史列表',
+    url: () => getExportUrl(deviceInfo.value),
+  },
+});
+const [register, { getForm, setLoading, getPaginationRef, setPagination, setColumns }] = tableContext;
 
-  // 已选中的设备信息
-  const deviceInfo = ref<Record<string, unknown>>({});
-  // 设备下拉框选项
-  const deviceOptions = ref<Record<string, unknown>[]>([]);
-  // 子设备下拉框选项
-  const dictOptions = ref<Record<string, unknown>[]>([]);
+// 已选中的设备信息
+const deviceInfo = ref<Record<string, unknown>>({});
+// 设备下拉框选项
+const deviceOptions = ref<Record<string, unknown>[]>([]);
+// 子设备下拉框选项
+const dictOptions = ref<Record<string, unknown>[]>([]);
 
-  /**
-   * 获取设备信息列表、子设备字典,初始化设备可选项、子设备可选项并选中首个设备
-   */
-  async function fetchDevice() {
-    const results = await getDeviceList({ devicetype: props.deviceCode, pageSize: 10000 });
-    const dicts = await initDictOptions(props.dictCode);
+/**
+ * 获取设备信息列表、子设备字典,初始化设备可选项、子设备可选项并选中首个设备
+ */
+async function fetchDevice() {
+  const results = await getDeviceList({ devicetype: props.deviceCode, pageSize: 10000 });
+  const dicts = await initDictOptions(props.dictCode);
 
-    const options = results.map((item) => {
-      return {
-        label: item.strinstallpos,
-        value: item.id || item.deviceID,
-        deviceType: item.strtype || item.deviceType,
-        devicekind: item.devicekind,
-        stationtype: item.stationtype,
-      };
-    });
-    deviceOptions.value = options;
-    dictOptions.value = dicts;
-    onDeviceChangeCallback(null, options[0]);
-  }
+  const options = results.map((item) => {
+    return {
+      label: item.strinstallpos,
+      value: item.id || item.deviceID,
+      deviceType: item.strtype || item.deviceType,
+      devicekind: item.devicekind,
+      stationtype: item.stationtype,
+    };
+  });
+  deviceOptions.value = options;
+  dictOptions.value = dicts;
+  onDeviceChangeCallback(null, options[0]);
+}
 
-  /**
-   * 选择任意设备后的回调,根据设备信息刷新表格、表头及其数据
-   */
-  function onDeviceChangeCallback(__, option) {
-    // 生成所有需要查询的表头编码,例如 forcFan_auto 对应 forcFan_auto_history、forcFan_history 这两个
-    const codes: string[] = [];
-    deviceInfo.value = option;
-    if (deviceInfo.value && deviceInfo.value.deviceType) {
-      const arr = (deviceInfo.value.deviceType as string).split('_');
-      while (arr.length) {
-        codes.push(arr.join('_').concat('_history'));
-        arr.pop();
-      }
+/**
+ * 选择任意设备后的回调,根据设备信息刷新表格、表头及其数据
+ */
+function onDeviceChangeCallback(__, option) {
+  // 生成所有需要查询的表头编码,例如 forcFan_auto 对应 forcFan_auto_history、forcFan_history 这两个
+  const codes: string[] = [];
+  deviceInfo.value = option;
+  if (deviceInfo.value && deviceInfo.value.deviceType) {
+    const arr = (deviceInfo.value.deviceType as string).split('_');
+    while (arr.length) {
+      codes.push(arr.join('_').concat('_history'));
+      arr.pop();
     }
-    // 如此,例如deviceType为forcFan_auto, 则查询表头按 forcFan_auto_history、forcFan_history、columnsCode 为顺序
-    initTable(codes.concat(props.columnsCode));
-    search();
   }
+  // 如此,例如deviceType为forcFan_auto, 则查询表头按 forcFan_auto_history、forcFan_history、columnsCode 为顺序
+  initTable(codes.concat(props.columnsCode));
+  search();
+}
 
-  /**
-   * 初始化表格,该方法将根据参数设定新的表头、表单。
-   *
-   * 需要有设备信息之后再初始化表格。
-   *
-   * @param deviceCodes 获取表头所用的编码,从左到右依次尝试,直到找到第一个有表头信息的为止
-   */
-  function initTable(deviceCodes: string[]) {
-    const defaultSchemas = getDefaultSchemas(dictOptions.value, deviceOptions.value, onDeviceChangeCallback);
-    for (const code of deviceCodes) {
-      const cols = getTableHeaderColumns(code);
-      if (cols.length) {
-        originColumns = cols;
-        break;
-      }
+/**
+ * 初始化表格,该方法将根据参数设定新的表头、表单。
+ *
+ * 需要有设备信息之后再初始化表格。
+ *
+ * @param deviceCodes 获取表头所用的编码,从左到右依次尝试,直到找到第一个有表头信息的为止
+ */
+function initTable(deviceCodes: string[]) {
+  const defaultSchemas = getDefaultSchemas(dictOptions.value, deviceOptions.value, onDeviceChangeCallback);
+  for (const code of deviceCodes) {
+    const cols = getTableHeaderColumns(code);
+    if (cols.length) {
+      originColumns = cols;
+      break;
     }
-    getForm().setProps({
-      schemas: defaultSchemas,
-    });
   }
+  getForm().setProps({
+    schemas: defaultSchemas,
+  });
+}
 
-  /**
-   * 搜索,核心方法
-   *
-   * 该方法获取表单、处理表单数据后尝试获取数据并设置表头
-   */
-  async function search() {
-    if (!deviceInfo.value) return;
-
-    const form = getForm();
-    await form.validate();
-    const formData = form.getFieldsValue();
+/**
+ * 搜索,核心方法
+ *
+ * 该方法获取表单、处理表单数据后尝试获取数据并设置表头
+ */
+async function search() {
+  if (!deviceInfo.value) return;
 
-    setLoading(true);
-    const pagination = getPaginationRef() as PaginationProps;
-    const deviceCode = (deviceInfo.value.deviceType || props.deviceCode.concat('*')) as string;
-    const { records, total, current } = await list(deviceCode, deviceInfo.value, formData, pagination).finally(() => {
-      setLoading(false);
-    });
-    setPagination({
-      current,
-      total,
-    });
-    records.forEach((item) => {
-      Object.assign(item, item.readData);
-    });
-    data.value = records;
-    updateColumns(formData.deviceNum);
-  }
+  const form = getForm();
+  await form.validate();
+  const formData = form.getFieldsValue();
 
-  /**
-   * 更新表头,表头默认情况下需要和子设备联动
-   * @param prefix 子设备的值,即为表头取数据时字段的前缀
-   */
-  function updateColumns(prefix?: string) {
-    if (!prefix) return setColumns(originColumns);
-    // 如果有子设备信息,筛选符合规范的表头
-    const cols = originColumns.map((col) => {
-      const dataIndex = col.dataIndex as string;
-      // 获取到子设备编码的前缀及编码,正则例子:forcFan1 => [forcFan1, forcFan, 1]
-      const [_, pfx] = prefix.match(/([A-Za-z]+)([0-9]+)/) || [];
-      // 同时,如若已经在前缀后配置了编号则不要更改
-      const reg = new RegExp(`${pfx}[0-9]`);
-      if (dataIndex.search(reg) !== -1) return col;
-      if (dataIndex.includes(pfx)) {
-        return {
-          ...col,
-          dataIndex: dataIndex.replace(pfx, prefix),
-        };
-      }
-      // 默认直接放行
-      return col;
-    });
-    setColumns(cols);
-  }
+  setLoading(true);
+  const pagination = getPaginationRef() as PaginationProps;
+  const deviceCode = (deviceInfo.value.deviceType || props.deviceCode.concat('*')) as string;
+  const { records, total, current } = await list(deviceCode, deviceInfo.value, formData, pagination).finally(() => {
+    setLoading(false);
+  });
+  setPagination({
+    current,
+    total,
+  });
+  records.forEach((item) => {
+    Object.assign(item, item.readData);
+  });
+  data.value = records;
+  updateColumns(formData.deviceNum);
+}
 
-  /** 导出表格内容为excel */
-  async function exportXls() {
-    const form = getForm();
-    await form.validate();
-    const formData = form.getFieldsValue();
-    const pagination = getPaginationRef() as PaginationProps;
-    const deviceCode = (deviceInfo.value.deviceType || props.deviceCode.concat('*')) as string;
-    const params = adaptFormData(deviceCode, deviceInfo.value, formData, pagination);
-    if (deviceInfo.value.stationtype === 'redis') {
-      return onExportXlsPost(params);
-    } else {
-      return onExportXls(params);
+/**
+ * 更新表头,表头默认情况下需要和子设备联动
+ * @param prefix 子设备的值,即为表头取数据时字段的前缀
+ */
+function updateColumns(prefix?: string) {
+  if (!prefix) return setColumns(originColumns);
+  // 如果有子设备信息,筛选符合规范的表头
+  const cols = originColumns.map((col) => {
+    const dataIndex = col.dataIndex as string;
+    // 获取到子设备编码的前缀及编码,正则例子:forcFan1 => [forcFan1, forcFan, 1]
+    const [_, pfx] = prefix.match(/([A-Za-z]+)([0-9]+)/) || [];
+    // 同时,如若已经在前缀后配置了编号则不要更改
+    const reg = new RegExp(`${pfx}[0-9]`);
+    if (dataIndex.search(reg) !== -1) return col;
+    if (dataIndex.includes(pfx)) {
+      return {
+        ...col,
+        dataIndex: dataIndex.replace(pfx, prefix),
+      };
     }
+    // 默认直接放行
+    return col;
+  });
+  setColumns(cols);
+}
+
+/** 导出表格内容为excel */
+async function exportXls() {
+  const form = getForm();
+  await form.validate();
+  const formData = form.getFieldsValue();
+  const pagination = getPaginationRef() as PaginationProps;
+  const deviceCode = (deviceInfo.value.deviceType || props.deviceCode.concat('*')) as string;
+  const params = adaptFormData(deviceCode, deviceInfo.value, formData, pagination);
+  if (deviceInfo.value.stationtype === 'redis') {
+    return onExportXlsPost(params);
+  } else {
+    return onExportXls(params);
   }
+}
 
-  // watch(
-  //   () => props.deviceCode,
-  //   async () => {
-  //     await fetchDevice();
-  //     onDeviceChangeCallback(null, deviceInfo.value);
-  //   }
-  // );
+// watch(
+//   () => props.deviceCode,
+//   async () => {
+//     await fetchDevice();
+//     onDeviceChangeCallback(null, deviceInfo.value);
+//   }
+// );
 
-  onMounted(async () => {
-    await fetchDevice();
-    onDeviceChangeCallback(null, deviceInfo.value);
-  });
+onMounted(async () => {
+  await fetchDevice();
+  onDeviceChangeCallback(null, deviceInfo.value);
+});
 </script>
 
 <style scoped lang="less">
-  @import '/@/design/theme.less';
+@import '/@/design/theme.less';
 
-  :deep(.@{ventSpace}-table-body) {
-    height: auto !important;
+:deep(.@{ventSpace}-table-body) {
+  height: auto !important;
+}
+:deep(.zxm-picker) {
+  height: 30px !important;
+}
+:deep(.zxm-table) {
+  .zxm-table-title {
+    display: none !important;
   }
-  :deep(.zxm-picker) {
-    height: 30px !important;
-  }
-  :deep(.zxm-table) {
-    .zxm-table-title {
-      display: none !important;
-    }
-  }
-  .history-table {
-    width: 100%;
-    :deep(.jeecg-basic-table-form-container) {
-      .@{ventSpace}-form {
-        padding: 0 !important;
-        border: none !important;
-        margin-bottom: 0 !important;
-        .@{ventSpace}-picker,
-        .@{ventSpace}-select-selector {
-          width: 100% !important;
-          background: #00000017;
-          border: 1px solid #b7b7b7;
-          input,
-          .@{ventSpace}-select-selection-item,
-          .@{ventSpace}-picker-suffix {
-            color: #fff;
-          }
-          .@{ventSpace}-select-selection-placeholder {
-            color: #ffffffaa;
-          }
+}
+.history-table {
+  width: 100%;
+  :deep(.jeecg-basic-table-form-container) {
+    .@{ventSpace}-form {
+      padding: 0 !important;
+      border: none !important;
+      margin-bottom: 0 !important;
+      .@{ventSpace}-picker,
+      .@{ventSpace}-select-selector {
+        width: 100% !important;
+        background: #00000017;
+        border: 1px solid #b7b7b7;
+        input,
+        .@{ventSpace}-select-selection-item,
+        .@{ventSpace}-picker-suffix {
+          color: #fff;
+        }
+        .@{ventSpace}-select-selection-placeholder {
+          color: #ffffffaa;
         }
-      }
-      .@{ventSpace}-table-title {
-        min-height: 0 !important;
       }
     }
-    .pagination-box {
-      display: flex;
-      justify-content: flex-end;
-      align-items: center;
-      .page-num {
-        border: 1px solid #0090d8;
-        padding: 4px 8px;
-        margin-right: 5px;
-        color: #0090d8;
-      }
-      .btn {
-        margin-right: 10px;
-      }
+    .@{ventSpace}-table-title {
+      min-height: 0 !important;
+    }
+  }
+  .pagination-box {
+    display: flex;
+    justify-content: flex-end;
+    align-items: center;
+    .page-num {
+      border: 1px solid #0090d8;
+      padding: 4px 8px;
+      margin-right: 5px;
+      color: #0090d8;
+    }
+    .btn {
+      margin-right: 10px;
     }
   }
+}
 </style>

+ 1 - 1
src/views/vent/dataCenter/APICenter/index.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="safetyList">
-    <customHeader>数据中心-API管理</customHeader>
+    <customHeader>API管理</customHeader>
     <div class="device-manager-box">
       <!-- 分站监测 -->
       <BasicTable @register="registerTable">

+ 496 - 0
src/views/vent/dataCenter/deviceCenter/history/HistoryTable.vue

@@ -0,0 +1,496 @@
+<template>
+  <div class="history-table" v-if="loading">
+    <BasicTable ref="historyTable" @register="registerTable" :data-source="dataSource" :scroll="tableScroll">
+      <template #form-submitBefore>
+        <a-button type="primary" preIcon="ant-design:search-outlined" @click="getDataSource">查询</a-button>
+      </template>
+    </BasicTable>
+  </div>
+</template>
+
+<script lang="ts" setup>
+//ts语法
+import { watchEffect, ref, watch, defineExpose, inject, nextTick, onMounted, computed } from 'vue';
+import { FormSchema } from '/@/components/Form/index';
+import { BasicTable } from '/@/components/Table';
+import { useListPage } from '/@/hooks/system/useListPage';
+import { getTableHeaderColumns } from '/@/hooks/web/useWebColumns';
+import { defHttp } from '/@/utils/http/axios';
+import dayjs from 'dayjs';
+import { getAutoScrollContainer } from '/@/utils/common/compUtils';
+import { render } from '/@/utils/common/renderUtils';
+import { useMethods } from '/@/hooks/system/useMethods';
+import { getDeviceList, getHistoryList } from './history.api';
+import { getDictItemsByCode } from '/@/utils/dict';
+import { get } from 'lodash-es';
+
+const globalConfig = inject('globalConfig');
+const props = defineProps({
+  columnsType: {
+    type: String,
+  },
+  columns: {
+    type: Array,
+    // required: true,
+    default: () => [],
+  },
+  deviceType: {
+    type: String,
+    required: true,
+  },
+  deviceListApi: {
+    type: Function,
+  },
+  deviceArr: {
+    type: Array,
+    // required: true,
+    default: () => [],
+  },
+  designScope: {
+    type: String,
+  },
+  sysId: {
+    type: String,
+  },
+  deviceId: {
+    type: String,
+  },
+  scroll: {
+    type: Object,
+    default: { y: 0 },
+  },
+  formSchemas: {
+    type: Array<FormSchema>,
+    default: () => [],
+  },
+  /** 仅展示已绑定设备,选择是则从系统中获取sysId下已绑定设备。仅能查询到已绑定设备的历史数据 */
+  onlyBounedDevices: {
+    type: Boolean,
+    default: false,
+  },
+  showHistoryCurve: {
+    type: Boolean,
+    default: false,
+  },
+});
+const getDeviceListApi = (params) => defHttp.post({ url: '/monitor/device', params });
+const historyTable = ref();
+const loading = ref(false);
+const stationType = ref('plc1');
+const dataSource = ref([]);
+const emit = defineEmits(['change']);
+
+const historyType = ref('');
+const deviceKide = ref('');
+const columns = ref([]);
+let deviceOptions = ref([]);
+const deviceTypeStr = ref('');
+const deviceTypeName = ref('');
+const deviceType = ref('');
+loading.value = true;
+
+const selectedOption = computed<Record<string, any> | undefined>(() => {
+  let idval: string | undefined = getForm()?.getFieldsValue()?.gdeviceids;
+  if (VENT_PARAM.historyIsMultiple && idval) {
+    const arr = idval.split(',');
+    idval = arr[arr.length - 1];
+  }
+  return deviceOptions.value.find((e: any) => {
+    return e.value === idval;
+  });
+});
+
+watch(
+  () => {
+    return props.columnsType;
+  },
+  async (newVal) => {
+    if (!newVal) return;
+    deviceKide.value = newVal;
+    if (historyTable.value) {
+      getForm().resetFields();
+      // getForm().updateSchema();
+      // getForm();
+    }
+    dataSource.value = [];
+    // const column = getTableHeaderColumns(newVal.includes('_history') ? newVal : newVal + '_history');
+    // if (column && column.length < 1) {
+    //   const arr = newVal.split('_');
+    //   console.log('历史记录列表表头------------>', arr[0] + '_monitor');
+    //   columns.value = getTableHeaderColumns(arr[0] + '_history');
+    //   if (columns.value.length < 1) {
+    //     if (historyType.value) {
+    //       columns.value = getTableHeaderColumns(historyType.value + '_history');
+    //     }
+    //   }
+    // } else {
+    //   columns.value = column;
+    // }
+    await getDeviceList();
+    nextTick(() => {
+      getDataSource();
+    });
+
+    if (historyTable.value) reload();
+  },
+  {
+    immediate: true,
+  }
+);
+
+watch(historyType, (type) => {
+  if (!type) return;
+  // if (historyTable.value) getForm().resetFields()
+
+  const column = getTableHeaderColumns(type.includes('_history') ? type : type + '_history');
+  if (column && column.length < 1) {
+    const arr = type.split('_');
+    columns.value = getTableHeaderColumns(arr[0] + '_history');
+  } else {
+    columns.value = column;
+  }
+  setColumns(columns.value);
+});
+
+// 是否显示历史曲线,在devices_shows_history_curve字典里可以配置哪些设备类型需要显示曲线
+// 字典内的字段可以是前缀,例如fanlocal之于fanlocal_normal
+// 安全监控设备需要更多的配置,除去配置safetymonitor,还需要配置哪些安全监控设备需要曲线
+// 因此可以配置例如A1001的dataTypeName代码(可以查看真实数据参考)
+function calcShowCurveValue() {
+  const historyCurveDicts = getDictItemsByCode('devices_shows_history_curve') || [];
+  const findDict = (str) => historyCurveDicts.some(({ value }) => str.startsWith(value));
+
+  if (!props.showHistoryCurve) return false;
+  const dt = props.deviceType; // 依赖项
+
+  if (!findDict(dt)) return false;
+  if (!dt.startsWith('safetymonitor')) return true;
+
+  // 和字典的设备类型匹配后,如果是安全监控设备,需要额外的匹配安全监控类型
+  const dtns = get(selectedOption.value, 'readData.dataTypeName', ''); // 依赖项
+  return findDict(dtns);
+}
+
+const tableScroll = computed(() => {
+  if (props.scroll.y) return { y: props.scroll.y - 100 };
+  return {};
+});
+
+// watch(stationType, (type) => {
+//   if (type) {
+//     nextTick(() => {
+//       getDataSource();
+//     });
+//   }
+// });
+
+watch(
+  () => props.deviceId,
+  async () => {
+    await getForm().setFieldsValue({});
+    await getDeviceList();
+  }
+);
+
+/** 获取可供查询历史数据的设备列表 */
+async function getDeviceList() {
+  // if (props.deviceType.split('_')[1] && props.deviceType.split('_')[1] === 'history') return;
+  let result;
+  let response;
+  if (props.onlyBounedDevices) {
+    response = await getDeviceListApi({
+      systemID: props.sysId,
+      devicetype: 'sys',
+    }).then(({ msgTxt }) => {
+      return { msgTxt: msgTxt.filter((e) => e.type === props.deviceType) };
+    });
+  } else if (props.sysId) {
+    response = await getDeviceListApi({
+      sysId: props.sysId,
+      devicetype: props.deviceType.startsWith('vehicle') ? 'location_normal' : props.deviceType,
+      pageSize: 10000,
+    });
+  } else if (props.deviceListApi) {
+    response = await props.deviceListApi();
+  } else {
+    response = await getDeviceListApi({ devicetype: props.deviceType, pageSize: 10000 });
+  }
+
+  // 处理不同格式的数据
+  if (response['records'] && response['records'].length > 0) {
+    result = response['records'];
+  } else if (response['msgTxt'] && response['msgTxt'][0] && response['msgTxt'][0]['datalist']) {
+    result = response['msgTxt'][0]['datalist'];
+  }
+  if (response['msgTxt'] && response['msgTxt'][0]) {
+    deviceTypeName.value = response['msgTxt'][0]['typeName'];
+    deviceType.value = response['msgTxt'][0]['type'];
+  }
+
+  if (result) {
+    deviceOptions.value = [];
+    deviceOptions.value = result.map((item, index) => {
+      return {
+        label: item['strinstallpos'],
+        value: item['id'] || item['deviceID'],
+        strtype: item['strtype'] || item['deviceType'],
+        strinstallpos: item['strinstallpos'],
+        devicekind: item['devicekind'],
+        stationtype: item['stationtype'],
+        readData: item['readData'],
+      };
+    });
+
+    stationType.value = deviceOptions.value[0]['stationtype'];
+    if (props.deviceType.startsWith('vehicle')) {
+      historyType.value = 'vehicle';
+    } else {
+      historyType.value = deviceOptions.value[0]['strtype'] || deviceOptions.value[0]['devicekind'];
+    }
+  }
+  if (VENT_PARAM.historyIsMultiple) {
+    await getForm().setFieldsValue({
+      gdeviceids: [props.deviceId ? props.deviceId : deviceOptions.value[0] ? deviceOptions.value[0]['value'] : ''],
+    });
+    await getForm().updateSchema({
+      field: 'gdeviceids',
+      componentProps: {
+        mode: 'multiple',
+        maxTagCount: 'responsive',
+      },
+    });
+  } else {
+    await getForm().setFieldsValue({
+      gdeviceids: props.deviceId ? props.deviceId : deviceOptions.value[0] ? deviceOptions.value[0]['value'] : '',
+    });
+    await getForm().updateSchema({
+      field: 'gdeviceids',
+    });
+  }
+}
+
+function resetFormParam() {
+  const formData = getForm().getFieldsValue();
+  const pagination = getPaginationRef();
+  formData['pageNo'] = pagination['current'];
+  formData['pageSize'] = pagination['pageSize'];
+  formData['column'] = 'createTime';
+  const params = {
+    pageNo: pagination['current'],
+    pageSize: pagination['pageSize'],
+    column: pagination['createTime'],
+    ttime_begin: formData['ttime_begin'],
+    ttime_end: formData['ttime_end'],
+    deviceId: formData['gdeviceids'],
+  };
+  return params;
+}
+
+async function getDataSource() {
+  dataSource.value = [];
+  setLoading(true);
+  const params = await resetFormParam();
+  const result = await getHistoryList(params);
+  setPagination({ total: Math.abs(result['datalist']['total']) || 0 });
+  if (result['datalist']['records'].length > 0) {
+    dataSource.value = result['datalist']['records'].map((item: any) => {
+      return Object.assign(item, item['readData']);
+    });
+  } else {
+    dataSource.value = [];
+  }
+  setLoading(false);
+}
+
+// 列表页面公共参数、方法
+const { tableContext, onExportXls, onExportXlsPost } = useListPage({
+  tableProps: {
+    // api: list,
+    columns: props.columnsType ? columns : (props.columns as any[]),
+    canResize: true,
+    showTableSetting: false,
+    showActionColumn: false,
+    bordered: false,
+    size: 'small',
+    showIndexColumn: true,
+    tableLayout: 'auto',
+    formConfig: {
+      labelAlign: 'left',
+      labelWidth: 80,
+      showAdvancedButton: false,
+      showSubmitButton: false,
+      showResetButton: false,
+      baseColProps: {
+        xs: 24,
+        sm: 24,
+        md: 24,
+        lg: 9,
+        xl: 7,
+        xxl: 4,
+      },
+      schemas:
+        props.formSchemas.length > 0
+          ? props.formSchemas
+          : [
+              {
+                field: 'ttime_begin',
+                label: '开始时间',
+                component: 'DatePicker',
+                defaultValue: dayjs().startOf('date'),
+                required: true,
+                componentProps: {
+                  showTime: true,
+                  valueFormat: 'YYYY-MM-DD HH:mm:ss',
+                  getPopupContainer: getAutoScrollContainer,
+                },
+                colProps: {
+                  span: 5,
+                },
+              },
+              {
+                field: 'ttime_end',
+                label: '结束时间',
+                component: 'DatePicker',
+                defaultValue: dayjs(),
+                required: true,
+                componentProps: {
+                  showTime: true,
+                  valueFormat: 'YYYY-MM-DD HH:mm:ss',
+                  getPopupContainer: getAutoScrollContainer,
+                },
+                colProps: {
+                  span: 5,
+                },
+              },
+              {
+                label: '查询设备',
+                field: 'gdeviceids',
+                component: 'Select',
+                required: true,
+                componentProps: {
+                  showSearch: true,
+                  filterOption: (input: string, option: any) => {
+                    return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0;
+                  },
+                  options: deviceOptions,
+                  onChange: (e, option) => {
+                    if (option && (option['strinstallpos'] || option['strtype'] || option['devicekind'])) {
+                      historyType.value = option['strtype'] || option['devicekind'];
+                    }
+                    if (option['strtype']) {
+                      deviceTypeStr.value = option['strtype'];
+                    }
+                    stationType.value = option['stationtype'];
+                    nextTick(() => {
+                      getDataSource();
+                    });
+                  },
+                },
+                colProps: {
+                  span: 6,
+                },
+              },
+            ],
+      // fieldMapToTime: [['tickectDate', ['ttime_begin', 'ttime_end'], '']],
+    },
+    // fetchSetting: {
+    //   listField: 'datalist',
+    //   totalField: 'datalist.total',
+    // },
+    pagination: {
+      current: 1,
+      pageSize: 20,
+      showQuickJumper: false,
+      showSizeChanger: false,
+    },
+    beforeFetch() {
+      const newParams = { ...resetFormParam() };
+      return newParams;
+    },
+  },
+});
+//注册table数据
+const [registerTable, { reload, setLoading, getForm, setColumns, getPaginationRef, setPagination }] = tableContext;
+watchEffect(() => {
+  if (historyTable.value && dataSource) {
+    const data = dataSource.value || [];
+    emit('change', data);
+  }
+});
+
+onMounted(async () => {
+  await getDeviceList();
+  if (deviceOptions.value[0]) {
+    nextTick(async () => {
+      await getDataSource();
+    });
+  }
+
+  watch([() => getPaginationRef()['current'], () => getPaginationRef()['pageSize']], async () => {
+    if (deviceOptions.value[0]) {
+      if (deviceOptions.value[0]) {
+        await getDataSource();
+      }
+    }
+  });
+});
+defineExpose({ setLoading });
+</script>
+
+<style scoped lang="less">
+@import '/@/design/theme.less';
+
+:deep(.@{ventSpace}-table-body) {
+  height: auto !important;
+}
+:deep(.zxm-picker) {
+  height: 30px !important;
+}
+.history-table {
+  width: 100%;
+  :deep(.jeecg-basic-table-form-container) {
+    .@{ventSpace}-form {
+      padding: 0 !important;
+      border: none !important;
+      margin-bottom: 0 !important;
+      .@{ventSpace}-picker,
+      .@{ventSpace}-select-selector {
+        width: 100% !important;
+        height: 100%;
+        background: #00000017;
+        border: 1px solid #b7b7b7;
+        input,
+        .@{ventSpace}-select-selection-item,
+        .@{ventSpace}-picker-suffix {
+          color: #fff;
+        }
+        .@{ventSpace}-select-selection-placeholder {
+          color: #ffffffaa;
+        }
+      }
+    }
+    .@{ventSpace}-table-title {
+      min-height: 0 !important;
+    }
+  }
+  .pagination-box {
+    display: flex;
+    justify-content: flex-end;
+    align-items: center;
+    .page-num {
+      border: 1px solid #0090d8;
+      padding: 4px 8px;
+      margin-right: 5px;
+      color: #0090d8;
+    }
+    .btn {
+      margin-right: 10px;
+    }
+  }
+}
+
+.history-chart {
+  background-color: #0090d822;
+  margin: 0 10px;
+}
+</style>

+ 289 - 0
src/views/vent/dataCenter/deviceCenter/history/HistoryTableFan.vue

@@ -0,0 +1,289 @@
+<template>
+  <div class="history-table">
+    <BasicTable ref="historyTable" @register="register" :data-source="data" :scroll="scroll" @change="search">
+      <template #bodyCell="{ column, record }">
+        <a-tag v-if="column.dataIndex === 'warnFlag'" :color="record.warnFlag == '0' ? 'green' : 'red'">
+          {{ record.warnFlag == '0' ? '正常' : '报警' }}
+        </a-tag>
+        <a-tag v-if="column.dataIndex === 'netStatus'" :color="record.netStatus == '0' ? '#f00' : 'green'">
+          {{ record.netStatus == '0' ? '断开' : '连接' }}
+        </a-tag>
+      </template>
+      <template #form-submitBefore>
+        <a-button type="primary" preIcon="ant-design:search-outlined" @click="search">查询</a-button>
+      </template>
+    </BasicTable>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import { computed, onMounted, ref, shallowRef } from 'vue';
+import { BasicColumn, PaginationProps, BasicTable } from '/@/components/Table';
+import { getTableHeaderColumns } from '/@/hooks/web/useWebColumns';
+import { getDefaultSchemas } from './history.data';
+import { getDeviceList, getHistoryList } from './history.api';
+import { useListPage } from '/@/hooks/system/useListPage';
+import { initDictOptions } from '/@/utils/dict';
+
+const props = withDefaults(
+  defineProps<{
+    /** 表格项配置,默认由deviceCode获取且联动dictCode,可以覆写,覆写后将不支持联动,参考BaiscTable */
+    // columns?: BasicColumn[];
+    /** 表格操作项配置,默认为空,可以覆写 */
+    // actionColumns?: BasicColumn;
+    /** 查询表单项配置,默认联动dictCode,可以覆写,覆写后将不支持联动,提供formProps时此项无效,参考BaiscTable */
+    // schemas?: FormSchema[];
+    /** 表格分页配置,可以覆写,参考BaiscTable */
+    pagination?: PaginationProps;
+    /** 设备编码,该编码用于请求设备信息,示例:forcFan */
+    deviceCode: string;
+    /** 字典编码,该编码用于从字典配置中读出设备项,示例:forcFan_dict */
+    dictCode: string;
+    /** 字段编码,该编码用于从设备字段配置中读取默认表头信息,示例:forcFan_history */
+    columnsCode: string;
+
+    scroll: { x: number | true; y: number };
+    /** 表格配置,参考BaiscTable,该值会与默认的配置进行浅合并,这里提供的任何配置都是优先的 */
+    // tableProps?: BasicTableProps;
+    /** 查询表单配置,参考BaiscTable */
+    // formProps?: FormProps;
+  }>(),
+  {
+    deviceCode: '',
+    dictCode: '',
+    pagination: (): PaginationProps => ({
+      current: 1,
+      pageSize: 20,
+      showQuickJumper: false,
+      showSizeChanger: false,
+    }),
+  }
+);
+
+// 未经过处理的原始表头,即项目配置的表头
+let originColumns: BasicColumn[] = [];
+// 表格数据
+const data = shallowRef([]);
+
+const { tableContext } = useListPage({
+  tableProps: {
+    columns: [
+      {
+        align: 'center',
+        dataIndex: 'strinstallpos',
+        defaultHidden: false,
+        title: '安装位置',
+        width: 80,
+      },
+    ],
+    formConfig: {
+      labelAlign: 'left',
+      labelWidth: 80,
+      showAdvancedButton: false,
+      showSubmitButton: false,
+      showResetButton: false,
+      actionColOptions: {
+        xxl: 4,
+      },
+    },
+    canResize: true,
+    showTableSetting: false,
+    showActionColumn: false,
+    bordered: false,
+    size: 'small',
+    showIndexColumn: true,
+    tableLayout: 'auto',
+    scroll: computed(() => {
+      return { ...props.scroll, y: props.scroll.y - 100 };
+    }),
+    pagination: props.pagination,
+  },
+});
+const [register, { getForm, setLoading, getPaginationRef, setPagination, setColumns }] = tableContext;
+
+// 已选中的设备信息
+const deviceInfo = ref<Record<string, unknown>>({});
+// 设备下拉框选项
+const deviceOptions = ref<Record<string, unknown>[]>([]);
+// 子设备下拉框选项
+const dictOptions = ref<Record<string, unknown>[]>([]);
+
+/**
+ * 获取设备信息列表、子设备字典,初始化设备可选项、子设备可选项并选中首个设备
+ */
+async function fetchDevice() {
+  const results = await getDeviceList({ devicetype: props.deviceCode, pageSize: 10000 });
+  const dicts = await initDictOptions(props.dictCode);
+
+  const options = results.map((item) => {
+    return {
+      label: item.strinstallpos,
+      value: item.id || item.deviceID,
+      deviceType: item.strtype || item.deviceType,
+      devicekind: item.devicekind,
+      stationtype: item.stationtype,
+    };
+  });
+  deviceOptions.value = options;
+  dictOptions.value = dicts;
+  onDeviceChangeCallback(null, options[0]);
+}
+
+/**
+ * 选择任意设备后的回调,根据设备信息刷新表格、表头及其数据
+ */
+function onDeviceChangeCallback(__, option) {
+  // 生成所有需要查询的表头编码,例如 forcFan_auto 对应 forcFan_auto_history、forcFan_history 这两个
+  const codes: string[] = [];
+  deviceInfo.value = option;
+  if (deviceInfo.value && deviceInfo.value.deviceType) {
+    const arr = (deviceInfo.value.deviceType as string).split('_');
+    while (arr.length) {
+      codes.push(arr.join('_').concat('_history'));
+      arr.pop();
+    }
+  }
+  // 如此,例如deviceType为forcFan_auto, 则查询表头按 forcFan_auto_history、forcFan_history、columnsCode 为顺序
+  initTable(codes.concat(props.columnsCode));
+  search();
+}
+
+/**
+ * 初始化表格,该方法将根据参数设定新的表头、表单。
+ *
+ * 需要有设备信息之后再初始化表格。
+ *
+ * @param deviceCodes 获取表头所用的编码,从左到右依次尝试,直到找到第一个有表头信息的为止
+ */
+function initTable(deviceCodes: string[]) {
+  const defaultSchemas = getDefaultSchemas(dictOptions.value, deviceOptions.value, onDeviceChangeCallback);
+  for (const code of deviceCodes) {
+    const cols = getTableHeaderColumns(code);
+    if (cols.length) {
+      originColumns = cols;
+      break;
+    }
+  }
+  getForm().setProps({
+    schemas: defaultSchemas,
+  });
+}
+
+/**
+ * 搜索,核心方法
+ *
+ * 该方法获取表单、处理表单数据后尝试获取数据并设置表头
+ */
+async function search() {
+  if (!deviceInfo.value) return;
+
+  const form = getForm();
+  await form.validate();
+  const formData = form.getFieldsValue();
+  const pagination = getPaginationRef() as PaginationProps;
+  const params = {
+    ...formData,
+    pageNo: pagination.current,
+    pageSize: pagination.pageSize,
+  };
+  setLoading(true);
+  const result = await getHistoryList(params);
+  if (result) {
+    setLoading(false);
+  }
+  data.value = result.datalist['records'].map((item: any) => {
+    setPagination({ total: Math.abs(result['datalist']['total']) || 0 });
+    return Object.assign(item, item['readData']);
+  });
+
+  updateColumns(formData.deviceNum);
+}
+
+/**
+ * 更新表头,表头默认情况下需要和子设备联动
+ * @param prefix 子设备的值,即为表头取数据时字段的前缀
+ */
+function updateColumns(prefix?: string) {
+  if (!prefix) return setColumns(originColumns);
+  // 如果有子设备信息,筛选符合规范的表头
+  const cols = originColumns.map((col) => {
+    const dataIndex = col.dataIndex as string;
+    // 获取到子设备编码的前缀及编码,正则例子:forcFan1 => [forcFan1, forcFan, 1]
+    const [_, pfx] = prefix.match(/([A-Za-z]+)([0-9]+)/) || [];
+    // 同时,如若已经在前缀后配置了编号则不要更改
+    const reg = new RegExp(`${pfx}[0-9]`);
+    if (dataIndex.search(reg) !== -1) return col;
+    if (dataIndex.includes(pfx)) {
+      return {
+        ...col,
+        dataIndex: dataIndex.replace(pfx, prefix),
+      };
+    }
+    // 默认直接放行
+    return col;
+  });
+  setColumns(cols);
+}
+
+onMounted(async () => {
+  await fetchDevice();
+  onDeviceChangeCallback(null, deviceInfo.value);
+});
+</script>
+
+<style scoped lang="less">
+@import '/@/design/theme.less';
+
+:deep(.@{ventSpace}-table-body) {
+  height: auto !important;
+}
+:deep(.zxm-picker) {
+  height: 30px !important;
+}
+:deep(.zxm-table) {
+  .zxm-table-title {
+    display: none !important;
+  }
+}
+.history-table {
+  width: 100%;
+  :deep(.jeecg-basic-table-form-container) {
+    .@{ventSpace}-form {
+      padding: 0 !important;
+      border: none !important;
+      margin-bottom: 0 !important;
+      .@{ventSpace}-picker,
+      .@{ventSpace}-select-selector {
+        width: 100% !important;
+        background: #00000017;
+        border: 1px solid #b7b7b7;
+        input,
+        .@{ventSpace}-select-selection-item,
+        .@{ventSpace}-picker-suffix {
+          color: #fff;
+        }
+        .@{ventSpace}-select-selection-placeholder {
+          color: #ffffffaa;
+        }
+      }
+    }
+    .@{ventSpace}-table-title {
+      min-height: 0 !important;
+    }
+  }
+  .pagination-box {
+    display: flex;
+    justify-content: flex-end;
+    align-items: center;
+    .page-num {
+      border: 1px solid #0090d8;
+      padding: 4px 8px;
+      margin-right: 5px;
+      color: #0090d8;
+    }
+    .btn {
+      margin-right: 10px;
+    }
+  }
+}
+</style>

+ 78 - 0
src/views/vent/dataCenter/deviceCenter/history/history.api.ts

@@ -0,0 +1,78 @@
+import { PaginationProps } from '/@/components/Table';
+import { defHttp } from '/@/utils/http/axios';
+
+enum Api {
+  getHistory = '/dataCenter/safety/ventanalyMonitorData/getRealHistory',
+  getDeviceList = '/monitor/device',
+}
+/**
+ * 获取列表的接口
+ * @param deviceCode 设备编码,作为 strtype 传参
+ * @param deviceInfo 设备信息,根据分站判别所用的 api
+ * @param formData 表单数据
+ * @param pagination 分页数据
+ * @returns
+ */
+
+const intervalMap = new Map([
+  ['1', '1s'],
+  ['2', '5s'],
+  ['3', '10s'],
+  ['4', '30s'],
+  ['5', '1m'],
+  ['6', '10m'],
+  ['7', '30m'],
+  ['8', '1h'],
+]);
+
+/**
+ * 根据所给设备的分站信息、设备编码等信息生成历史数据/数据导出api所需的请求参数
+ * @param deviceCode
+ * @param deviceInfo
+ * @param formData
+ * @param pagination
+ * @returns
+ */
+export const adaptFormData = (deviceCode: string, deviceInfo: any, formData: any, pagination: PaginationProps) => {
+  if (deviceInfo.stationtype === 'redis') {
+    return {
+      pageNum: pagination.current,
+      pageSize: pagination.pageSize,
+      column: 'createTime',
+      startTime: formData.ttime_begin,
+      endTime: formData.ttime_end,
+      deviceId: formData.gdeviceids,
+      strtype: deviceCode,
+      isEmployee: deviceCode.startsWith('vehicle') ? false : true,
+    };
+  } else {
+    return {
+      pageNo: pagination.current,
+      pageSize: pagination.pageSize,
+      column: 'createTime',
+      strtype: deviceCode,
+      ...formData,
+    };
+  }
+};
+
+// 获取历史数据
+export const getHistoryList = (params) =>
+  defHttp.get({
+    url: Api.getHistory,
+    params,
+  });
+/**
+ * 根据设备编码获取设备列表
+ * @param params
+ */
+export const getDeviceList = (params) =>
+  defHttp.post({ url: Api.getDeviceList, params }).then((r) => {
+    if (r.records && r.records.length) {
+      return r.records;
+    }
+    if (r.msgTxt && r.msgTxt.length) {
+      return r.msgTxt[0].datalist;
+    }
+    return [];
+  });

+ 86 - 0
src/views/vent/dataCenter/deviceCenter/history/history.data.ts

@@ -0,0 +1,86 @@
+import dayjs from 'dayjs';
+import { FormSchema } from '/@/components/Table';
+import { getAutoScrollContainer } from '/@/utils/common/compUtils';
+import { get } from 'lodash-es';
+
+/**
+ * 默认的查询表单项props
+ *
+ * @param dictOptions 用于初始化子设备下拉框
+ * @param deviceOptions 用于初始化设备下拉框
+ * @param onDeviceChange 设备下拉框选择内容后的回调函数
+ * @returns
+ */
+export const getDefaultSchemas: (dictOptions: any[], deviceOptions: any[], onDeviceChange?: Function) => FormSchema[] = (
+  dictOptions: any[],
+  deviceOptions: any[],
+  onDeviceChange?: Function
+) => {
+  const device = get(deviceOptions, '[0].value', '');
+  const isRedis = get(deviceOptions, '[0].stationtype', 'redis') === 'redis';
+  const dictcode = get(dictOptions, '[0].value', '');
+  return [
+    {
+      field: 'ttime_begin',
+      label: '开始时间',
+      component: 'DatePicker',
+      defaultValue: dayjs().startOf('date'),
+      required: true,
+      componentProps: {
+        showTime: true,
+        valueFormat: 'YYYY-MM-DD HH:mm:ss',
+        getPopupContainer: getAutoScrollContainer,
+      },
+      colProps: {
+        span: 5,
+      },
+    },
+    {
+      field: 'ttime_end',
+      label: '结束时间',
+      component: 'DatePicker',
+      defaultValue: dayjs(),
+      required: true,
+      componentProps: {
+        showTime: true,
+        valueFormat: 'YYYY-MM-DD HH:mm:ss',
+        getPopupContainer: getAutoScrollContainer,
+      },
+      colProps: {
+        span: 5,
+      },
+    },
+    {
+      label: '查询设备',
+      field: 'deviceId',
+      component: 'Select',
+      required: true,
+      defaultValue: VENT_PARAM.historyIsMultiple ? [device] : device,
+      componentProps: {
+        options: deviceOptions,
+        mode: VENT_PARAM.historyIsMultiple ? 'multiple' : undefined,
+        maxTagCount: 'responsive',
+        onChange: onDeviceChange,
+      },
+      colProps: {
+        span: 5,
+      },
+    },
+    {
+      label: '子设备',
+      field: 'deviceNum',
+      component: 'Select',
+      required: isRedis ? false : Boolean(dictOptions.length),
+      show: isRedis ? false : Boolean(dictOptions.length),
+      defaultValue: isRedis ? '' : dictcode,
+      componentProps: {
+        options: dictOptions,
+        // onChange: (e, option) => {
+        // },
+      },
+      colProps: {
+        span: 4,
+      },
+    },
+  ];
+};

+ 26 - 78
src/views/vent/dataCenter/deviceCenter/index.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="safetyList">
-    <customHeader>数据中心-设备监测</customHeader>
+    <customHeader>设备监测</customHeader>
     <div class="content">
       <a-tabs class="tab-box" v-model:activeKey="activeKey" @change="onChangeTab">
         <a-tab-pane tab="设备监测" key="device" />
@@ -92,8 +92,22 @@
             </a-table>
           </div>
           <div class="right-box" v-else-if="activeKey == 'history'">
-            <div class="right-title">历史数据:</div>
-            <a-table></a-table>
+            <template v-if="deviceType.startsWith('fanmain')">
+              <HistoryTableFan class="w-100% h-100%" :device-code="`${deviceType}`" :scroll="scroll" dict-code="fan_dict" />
+            </template>
+            <template v-else-if="deviceType.startsWith('fanlocal')">
+              <HistoryTableFan class="w-100% h-100%" :device-code="`${deviceType}`" :scroll="scroll" dict-code="fanlocal_dict" />s
+            </template>
+            <template v-else>
+              <HistoryTable
+                ref="historyTable"
+                :sysId="systemID"
+                :columns-type="`${deviceType}`"
+                :device-type="deviceType"
+                designScope="device-history"
+                :scroll="scroll"
+              />
+            </template>
           </div>
         </div>
       </div>
@@ -105,15 +119,16 @@
 import { ref, nextTick, reactive, onMounted, onUnmounted, watch, shallowRef, computed } from 'vue';
 import { usePermission } from '/@/hooks/web/usePermission';
 import customHeader from '/@/components/vent/customHeader.vue';
-import { AesEncryption } from '/@/utils/cipher';
-import { loginCipher } from '/@/settings/encryptionSetting';
 import { message, TreeProps } from 'ant-design-vue';
 import { getDeviceTypeList, getDeviceListByType, getDevMonitorListById } from './device.api';
+import HistoryTableFan from './history/HistoryTableFan.vue';
+import HistoryTable from './history/HistoryTable.vue';
 import { RightCircleTwoTone, DownCircleTwoTone } from '@ant-design/icons-vue';
-import { alignElement } from 'dom-align';
-import { active } from 'sortablejs';
-import { stubTrue } from 'lodash';
 import { useRoute } from 'vue-router';
+import { useListPage } from '/@/hooks/system/useListPage';
+import { BasicTable } from '/@/components/Table';
+import { FormSchema } from '/@/components/Form/index';
+
 let route = useRoute();
 const { hasPermission } = usePermission();
 let activeKey = ref('device');
@@ -133,73 +148,11 @@ const deviceList = ref<any[]>([]); // 设备列表
 const monitorList = ref<any[]>([]); // 监测数据列表
 // 当前展开的行key数组
 const expandedRowKeys = ref([]);
-
+const scroll = reactive({
+  y: 680,
+});
 // 加载状态映射
 const loadingMap = reactive({});
-// 分页参数
-const paginationState = ref({
-  current: 1,
-  pageSize: 10,
-  total: 0,
-});
-const paginationState2 = ref({
-  current: 1,
-  pageSize: 10,
-  total: 0,
-});
-// 计算分页后的数据
-const paginatedData = computed(() => {
-  const start = (paginationState.value.current - 1) * paginationState.value.pageSize;
-  const end = start + paginationState.value.pageSize;
-  return monitorList.value.slice(start, end);
-});
-// 计算分页后的数据
-const paginatedData2 = computed(() => {
-  const start = (paginationState2.value.current - 1) * paginationState2.value.pageSize;
-  const end = start + paginationState2.value.pageSize;
-  return deviceList.value.slice(start, end);
-});
-// 分页器配置 - 修复响应式问题
-// const paginationConfig = computed(() => {
-//   return {
-//     current: paginationState.value.current,
-//     pageSize: paginationState.value.pageSize,
-//     total: monitorList.value.length,
-//     showSizeChanger: true,
-//     showQuickJumper: true,
-//     showTotal: (total) => `共 ${total} 条`,
-//     pageSizeOptions: ['10', '20', '50', '100'],
-//     size: 'small',
-//     onChange: (page, pageSize) => {
-//       paginationState.value.current = page;
-//       paginationState.value.pageSize = pageSize;
-//     },
-//     onShowSizeChange: (current, size) => {
-//       paginationState.value.current = 1;
-//       paginationState.value.pageSize = size;
-//     },
-//   };
-// });
-// const paginationConfig2 = computed(() => {
-//   return {
-//     current: paginationState2.value.current,
-//     pageSize: paginationState2.value.pageSize,
-//     total: deviceList.value.length,
-//     showSizeChanger: true,
-//     showQuickJumper: true,
-//     showTotal: (total) => `共 ${total} 条`,
-//     pageSizeOptions: ['10', '20', '50', '100'],
-//     size: 'small',
-//     onChange: (page, pageSize) => {
-//       paginationState2.value.current = page;
-//       paginationState2.value.pageSize = pageSize;
-//     },
-//     onShowSizeChange: (current, size) => {
-//       paginationState2.value.current = 1;
-//       paginationState2.value.pageSize = size;
-//     },
-//   };
-// });
 // 切换tab页面
 async function onChangeTab(tab) {
   activeKey.value = tab;
@@ -267,7 +220,6 @@ async function getDeviceType(type?) {
     treeData.value = getData(result, dataSource, key);
     // 数据就绪后设置展开key数组
     expandedKeys.value = getAllNodeKeys(treeData.value, type);
-    console.log(expandedKeys.value, '22222');
   }
 }
 // 获取当前选择节点
@@ -417,10 +369,6 @@ onUnmounted(() => {
   }
 });
 // 监听分页变化
-watch(
-  () => [paginationState.value.current, paginationState.value.pageSize],
-  () => {}
-);
 </script>
 
 <style lang="less" scoped>

+ 28 - 45
src/views/vent/dataCenter/stationCenter/index.vue

@@ -1,6 +1,6 @@
 <template>
   <div class="safetyList">
-    <customHeader>数据中心-分站监测</customHeader>
+    <customHeader>分站监测</customHeader>
     <div class="content">
       <a-tabs class="tab-box" v-model:activeKey="activeKey" @change="onChangeTab">
         <a-tab-pane tab="分站监测" key="device" />
@@ -11,7 +11,7 @@
           <div class="left-box">
             <div class="left-content">
               <div class="card-box" v-for="(item, index) in cardList" :key="index">
-                <div :class="activeIndex1 === index ? 'card-itemD' : 'card-itemL'" @click="cardClick(item, index)">
+                <div class="card-itemL" :class="{ active: selectedIndex === index }" @click="cardClick(item, index)">
                   <div class="card-item-label">{{ item.strname }}</div>
                 </div>
               </div>
@@ -46,7 +46,21 @@
                   <RightCircleTwoTone v-else />
                 </a-button>
               </template>
-
+              <template #bodyCell="{ column, record }">
+                <template v-if="column.key === 'netStatus'">
+                  <span
+                    :style="{
+                      color: record.netStatus ? '#52c41a' : '#ddd',
+                      fontWeight: '500',
+                    }"
+                  >
+                    {{ record.netStatus ? '在线' : '断开' }}
+                  </span>
+                </template>
+                <template v-else>
+                  {{ record[column.dataIndex] }}
+                </template>
+              </template>
               <!-- 嵌套表格 -->
               <template #expandedRowRender="{ record }">
                 <a-table
@@ -94,6 +108,7 @@ const openNum = ref(0);
 const clsoeNum = ref(0);
 const monitorList = ref<any[]>([]); // 监测数据列表
 const expandedRowKeys = ref([]);
+const selectedIndex = ref(0);
 // // 分页参数
 // const paginationState = ref({
 //   current: 1,
@@ -172,6 +187,7 @@ async function getSubStationList() {
 }
 //菜单选项切换
 function cardClick(item, ind) {
+  selectedIndex.value = selectedIndex.value === ind ? -1 : ind;
   if (timer) {
     clearInterval(timer);
     timer = undefined;
@@ -215,10 +231,6 @@ const outerColumns = [
     dataIndex: 'netStatus',
     key: 'netStatus',
     align: 'center',
-    customRender: ({ text }) => {
-      text = '在线';
-      return `${text}`;
-    },
   },
 ];
 
@@ -395,30 +407,13 @@ onUnmounted(() => {
 
             .card-box {
               position: relative;
-              width: 182px;
+              width: 178px;
               height: 120px;
+              margin-top: 15px;
               margin-bottom: 30px;
               display: flex;
               justify-content: center;
 
-              .card-itemN {
-                position: relative;
-                width: 85px;
-                height: 120px;
-                background: url('/@/assets/images/zd-2.png') no-repeat center;
-                background-size: 100% 100%;
-                cursor: pointer;
-
-                .card-item-label {
-                  width: 100%;
-                  position: absolute;
-                  bottom: 5px;
-                  font-size: 12px;
-                  color: #fff;
-                  text-align: center;
-                }
-              }
-
               .card-itemL {
                 position: relative;
                 width: 85px;
@@ -426,7 +421,8 @@ onUnmounted(() => {
                 background: url('/@/assets/images/zd-3.png') no-repeat center;
                 background-size: 100% 100%;
                 cursor: pointer;
-
+                border: 1px solid transparent;
+                transition: border-color 0.2s ease;
                 .card-item-label {
                   width: 100%;
                   position: absolute;
@@ -436,25 +432,12 @@ onUnmounted(() => {
                   text-align: center;
                 }
               }
-
-              .card-itemD {
-                position: relative;
-                width: 85px;
-                height: 120px;
-                background: url('/@/assets/images/zd-1.png') no-repeat center;
-                background-size: 100% 100%;
-                cursor: pointer;
-
-                .card-item-label {
-                  width: 100%;
-                  position: absolute;
-                  bottom: 5px;
-                  font-size: 12px;
-                  color: #fff;
-                  text-align: center;
-                }
+              /* 选中状态的高亮边框 */
+              .card-itemL.active {
+                border-color: #0b69b6;
+                border-radius: 5px;
+                box-shadow: 0 0 3px 3px rgb(0, 128, 255);
               }
-
               .card-modal {
                 width: 86px;
                 position: absolute;

+ 227 - 0
src/views/vent/dataCenter/statsCenter/index.vue

@@ -0,0 +1,227 @@
+<template>
+  <div class="safetyList">
+    <customHeader>统计监测</customHeader>
+    <div class="content">
+      <div class="box-content">
+        <a-table size="small" :columns="outerColumns" :data-source="deviceList" :pagination="true"> </a-table>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, nextTick, reactive, onMounted, onUnmounted, watch, shallowRef, computed } from 'vue';
+import { getInvokeList, getInvokeDetailList } from './stats.api.ts';
+import customHeader from '/@/components/vent/customHeader.vue';
+const deviceList = ref<any[]>([]); // 列表数据
+const monitorList = ref<any[]>([]); // 详情数据
+// 分页参数
+const paginationState = ref({
+  current: 1,
+  pageSize: 10,
+  total: 0,
+});
+const paginationState2 = ref({
+  current: 1,
+  pageSize: 10,
+  total: 0,
+});
+// 计算分页后的数据
+const paginatedData = computed(() => {
+  const start = (paginationState.value.current - 1) * paginationState.value.pageSize;
+  const end = start + paginationState.value.pageSize;
+  return monitorList.value.slice(start, end);
+});
+// 计算分页后的数据
+const paginatedData2 = computed(() => {
+  const start = (paginationState2.value.current - 1) * paginationState2.value.pageSize;
+  const end = start + paginationState2.value.pageSize;
+  return deviceList.value.slice(start, end);
+});
+// 分页器配置 - 修复响应式问题
+const paginationConfig = computed(() => {
+  return {
+    current: paginationState.value.current,
+    pageSize: paginationState.value.pageSize,
+    total: monitorList.value.length,
+    showSizeChanger: true,
+    showQuickJumper: true,
+    showTotal: (total) => `共 ${total} 条`,
+    pageSizeOptions: ['10', '20', '50', '100'],
+    size: 'small',
+    onChange: (page, pageSize) => {
+      paginationState.value.current = page;
+      paginationState.value.pageSize = pageSize;
+    },
+    onShowSizeChange: (current, size) => {
+      paginationState.value.current = 1;
+      paginationState.value.pageSize = size;
+    },
+  };
+});
+const paginationConfig2 = computed(() => {
+  return {
+    current: paginationState2.value.current,
+    pageSize: paginationState2.value.pageSize,
+    total: deviceList.value.length,
+    showSizeChanger: true,
+    showQuickJumper: true,
+    showTotal: (total) => `共 ${total} 条`,
+    pageSizeOptions: ['10', '20', '50', '100'],
+    size: 'small',
+    onChange: (page, pageSize) => {
+      paginationState2.value.current = page;
+      paginationState2.value.pageSize = pageSize;
+    },
+    onShowSizeChange: (current, size) => {
+      paginationState2.value.current = 1;
+      paginationState2.value.pageSize = size;
+    },
+  };
+});
+// 外层表格列配置
+const outerColumns = [
+  {
+    title: '账户',
+    dataIndex: 'userid',
+    key: 'userid',
+    align: 'center',
+  },
+  {
+    title: '账号名称',
+    dataIndex: 'username',
+    key: 'username',
+    align: 'center',
+  },
+  {
+    title: '访问次数',
+    dataIndex: 'invoke_count',
+    key: 'invoke_count',
+    align: 'center',
+  },
+  {
+    title: '最新访问时间',
+    dataIndex: 'lastTime',
+    key: 'lastTime',
+    align: 'center',
+  },
+];
+
+// 内层表格列配置
+const innerColumns = [
+  {
+    title: '账号',
+    dataIndex: 'userid',
+    key: 'userid',
+    align: 'center',
+  },
+  {
+    title: '账号名称',
+    dataIndex: 'username',
+    key: 'username',
+    align: 'center',
+  },
+  {
+    title: '请求路径',
+    dataIndex: 'requestUrl',
+    key: 'requestUrl',
+    align: 'center',
+  },
+  {
+    title: '访问ip',
+    dataIndex: 'ip',
+    key: 'ip',
+    align: 'center',
+  },
+  {
+    title: '请求时间',
+    dataIndex: 'createTime',
+    key: 'createTime',
+    align: 'center',
+  },
+];
+
+async function getTableData(params?) {
+  if (!params) {
+    const params = {
+      pageNo: 1,
+      pageSize: 20,
+      logType: 9,
+    };
+    const result = await getInvokeList(params);
+    deviceList.value = result.records;
+  } else {
+    const result = await getInvokeList(params);
+    deviceList.value = result.records;
+  }
+}
+onMounted(() => {
+  getTableData();
+});
+onUnmounted(() => {});
+// 监听分页变化
+watch(
+  () => [paginationState.value.current, paginationState.value.pageSize],
+  () => {}
+);
+</script>
+
+<style lang="less" scoped>
+.safetyList {
+  width: calc(100% - 20px);
+  height: calc(100% - 80px);
+  position: relative;
+  margin: 50px 10px 10px 10px;
+
+  .content {
+    position: relative;
+    width: 100%;
+    height: 100%;
+
+    .box-content {
+      height: calc(100% - 50px);
+      padding-top: 10px;
+      box-sizing: border-box;
+    }
+  }
+}
+::v-deep(.zxm-radio-wrapper) {
+  font-size: 12px;
+}
+
+::v-deep(.zxm-input) {
+  font-size: 12px;
+}
+
+::v-deep(.zxm-select:not(.zxm-select-customize-input) .zxm-select-selector) {
+  border: 1px solid #3ad8ff77 !important;
+}
+
+// ::v-deep(.zxm-select-selection-item) {
+//   color: #fff ;
+// }
+
+// ::v-deep(.zxm-form-item-label > label) {
+//   color: #fff !important;
+// }
+
+/* 嵌套表格样式 */
+:deep(.ant-table-expanded-row) > td {
+  background-color: #f9f9f9 !important;
+  padding: 0 !important;
+}
+
+:deep(.ant-table-expanded-row .ant-table) {
+  margin: -10px -8px;
+  background: #f9f9f9;
+}
+/* 自定义展开按钮 */
+:deep(.ant-table-row-expand-icon) {
+  margin-right: 8px;
+}
+</style>
+<style>
+div[aria-hidden='true'] {
+  display: none !important;
+}
+</style>

+ 18 - 0
src/views/vent/dataCenter/statsCenter/stats.api.ts

@@ -0,0 +1,18 @@
+import { defHttp } from '/@/utils/http/axios';
+
+enum Api {
+  invokeList = '/dataCenter/sys/log/invokeList',
+  invokeDetailList = '/dataCenter/sys/log/invokeDetailList',
+}
+//统计列表接口
+export const getInvokeList = (params) =>
+  defHttp.get({
+    url: Api.invokeList,
+    params,
+  });
+//统计详情接口
+export const getInvokeDetailList = (params) =>
+  defHttp.get({
+    url: Api.invokeDetailList,
+    params,
+  });