Ver Fonte

feat(系统模块): 添加登录日志、接口日志功能

shizhongming há 2 anos atrás
pai
commit
241eaac86a

+ 12 - 0
src/modules/system/views/log/InterfaceLogListView.vue

@@ -0,0 +1,12 @@
+<template>
+  <SystemLogComponent ident="INTERFACE_LOG" id="smart-system-tool-interferLog" />
+</template>
+
+<script lang="ts" setup>
+  /**
+   * 登录日志
+   */
+  import SystemLogComponent from './SystemLogComponent.vue';
+</script>
+
+<style scoped></style>

+ 12 - 0
src/modules/system/views/log/LoginLogListView.vue

@@ -0,0 +1,12 @@
+<template>
+  <SystemLogComponent ident="LOGIN_LOG" id="smart-system-tool-loginLog" />
+</template>
+
+<script lang="ts" setup>
+  /**
+   * 登录日志
+   */
+  import SystemLogComponent from './SystemLogComponent.vue';
+</script>
+
+<style scoped></style>

+ 26 - 0
src/modules/system/views/log/SystemLogComponent.api.ts

@@ -0,0 +1,26 @@
+import { ApiServiceEnum, defHttp } from '@/utils/http/axios';
+
+enum Api {
+  list = 'sys/log/list',
+  getById = 'sys/log/getById',
+}
+
+export const listApi = (parameter) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.list,
+    data: {
+      sortName: 'createTime',
+      sortOrder: 'desc',
+      ...parameter,
+    },
+  });
+};
+
+export const getByIdApi = (id: number) => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_SYSTEM,
+    url: Api.getById,
+    data: id,
+  });
+};

+ 231 - 0
src/modules/system/views/log/SystemLogComponent.config.ts

@@ -0,0 +1,231 @@
+import type { SmartColumn, SmartSearchFormSchema } from '@/components/SmartTable';
+import dayjs from 'dayjs';
+
+export type LoginIdent = 'LOGIN_LOG' | 'INTERFACE_LOG';
+
+/**
+ * 获取表格列表
+ * @param ident 标识位
+ */
+export const getTableColumns = (ident: LoginIdent): SmartColumn[] => {
+  return tableColumns.filter((item) => {
+    return item.ident === undefined || item.ident.includes(ident);
+  }) as SmartColumn[];
+};
+
+const tableColumns: Array<SmartColumn & { ident?: LoginIdent[] }> = [
+  {
+    type: 'seq',
+    width: 80,
+  },
+  {
+    title: '{system.views.log.title.operation}',
+    field: 'operation',
+    minWidth: 200,
+  },
+  {
+    title: '{system.views.log.title.logSource}',
+    field: 'logSource',
+    width: 180,
+  },
+  {
+    title: '{system.views.log.title.createUserId}',
+    field: 'createBy',
+    width: 120,
+  },
+  {
+    title: '{system.views.log.title.ip}',
+    field: 'ip',
+    width: 160,
+  },
+  {
+    title: '{system.views.log.title.operationType}',
+    field: 'operationType',
+    headerAlign: 'left',
+    align: 'center',
+    width: 120,
+    ident: ['INTERFACE_LOG'],
+  },
+  {
+    title: '{system.views.log.title.requestPath}',
+    field: 'requestPath',
+    width: 200,
+    ident: ['INTERFACE_LOG'],
+  },
+  {
+    title: '{system.views.log.title.statusCode}',
+    field: 'statusCode',
+    width: 120,
+    headerAlign: 'left',
+    align: 'center',
+    slots: {
+      default: 'table-statusCode',
+    },
+    sortable: true,
+  },
+  {
+    title: '{system.views.log.title.method}',
+    field: 'method',
+    width: 200,
+    ident: ['INTERFACE_LOG'],
+  },
+  {
+    title: '{system.views.log.title.useTime}',
+    field: 'useTime',
+    width: 140,
+    headerAlign: 'left',
+    align: 'center',
+    sortable: true,
+    slots: {
+      default: 'table-useTime',
+    },
+    ident: ['INTERFACE_LOG'],
+  },
+  {
+    title: '{system.views.log.title.createTime}',
+    field: 'createTime',
+    width: 180,
+    sortable: true,
+  },
+  {
+    title: '{common.table.operation}',
+    field: 'table-operation',
+    width: 100,
+    fixed: 'right',
+    slots: {
+      default: 'table-operation',
+    },
+  },
+];
+
+const logSourceEnum = [
+  {
+    value: '10',
+    enumName: 'AUTO_POINTCUT',
+    label: 'system.views.log.title.logSourceAuto',
+    ident: ['INTERFACE_LOG'],
+  },
+  {
+    value: '20',
+    enumName: 'MANUAL',
+    label: 'system.views.log.title.logSourceManual',
+    ident: ['INTERFACE_LOG'],
+  },
+  {
+    value: '30',
+    enumName: 'LOGIN',
+    label: 'system.views.log.title.logSourceLoginSuccess',
+    ident: ['LOGIN_LOG'],
+  },
+  {
+    value: '40',
+    enumName: 'LOGOUT',
+    label: 'system.views.log.title.logSourceLogout',
+    ident: ['LOGIN_LOG'],
+  },
+  {
+    value: '50',
+    enumName: 'LOGIN_FAIL',
+    label: 'system.views.log.title.logSourceLoginFail',
+    ident: ['LOGIN_LOG'],
+  },
+];
+
+export const getLogSourceEnum = (ident: LoginIdent, t: Function) => {
+  return logSourceEnum
+    .filter((item) => item.ident === undefined || item.ident.includes(ident))
+    .map((item) => {
+      return {
+        value: item.value,
+        label: t(item.label),
+      };
+    });
+};
+
+const operationTypeEnum = [
+  {
+    value: 'ADD',
+    label: 'system.views.log.title.operationTypeAdd',
+  },
+  {
+    value: 'DELETE',
+    label: 'system.views.log.title.operationTypeDelete',
+  },
+  {
+    value: 'UPDATE',
+    label: 'system.views.log.title.operationTypeUpdate',
+  },
+  {
+    value: 'QUERY',
+    label: 'system.views.log.title.operationTypeQuery',
+  },
+];
+export const getOperationTypeEnum = (t: Function) => {
+  return operationTypeEnum.map((item) => {
+    return {
+      label: t(item.label),
+      value: item.value,
+    };
+  });
+};
+/**
+ * 获取搜索表单
+ * @param t
+ * @param ident
+ */
+export const getSearchFormSchemas = (t: Function, ident: LoginIdent) => {
+  const schemas: Array<SmartSearchFormSchema & { ident?: LoginIdent[] }> = [
+    {
+      label: t('system.views.log.title.operation'),
+      field: 'operation',
+      component: 'Input',
+      searchSymbol: 'like',
+    },
+    {
+      label: t('system.views.log.title.logSource'),
+      field: 'logSource',
+      component: 'Select',
+      componentProps: {
+        mode: 'multiple',
+        style: { width: '200px' },
+        options: getLogSourceEnum(ident, t),
+      },
+      searchSymbol: 'in',
+    },
+    {
+      label: t('system.views.log.title.statusCode'),
+      field: 'statusCode',
+      component: 'Input',
+      searchSymbol: '=',
+      componentProps: {
+        style: { width: '120px' },
+      },
+    },
+    {
+      label: t('system.views.log.title.createTime'),
+      field: 'createTime',
+      component: 'RangePicker',
+      searchSymbol: 'between',
+      componentProps: {
+        style: { width: '340px' },
+        showTime: {
+          defaultValue: [dayjs('00:00:00', 'HH:mm:ss')],
+        },
+      },
+    },
+    {
+      label: t('system.views.log.title.operationType'),
+      field: 'operationType',
+      component: 'Select',
+      searchSymbol: '=',
+      ident: ['INTERFACE_LOG'],
+      componentProps: {
+        optionLabelProp: 'children',
+        mode: 'multiple',
+        style: { width: '120px' },
+        options: getOperationTypeEnum(t),
+      },
+    },
+  ];
+  return schemas.filter((item) => item.ident === undefined || item.ident.includes(ident));
+};

+ 151 - 0
src/modules/system/views/log/SystemLogComponent.vue

@@ -0,0 +1,151 @@
+<template>
+  <div class="full-height page-container" :class="getClass">
+    <SmartTable @register="registerTable" v-bind="$attrs" :size="getTableSize">
+      <template #table-operation="{ row }">
+        <SmartVxeTableAction :actions="getTableActions(row)" />
+      </template>
+      <template #table-statusCode="{ row }">
+        <a-tag :color="row.statusCode >= 200 && row.statusCode < 300 ? '#2db7f5' : '#f50'">
+          {{ row.statusCode }}
+        </a-tag>
+      </template>
+      <template #table-useTime="{ row }">
+        <a-tag v-if="row.useTime !== null" :color="getUseTimeTagColor(row.useTime)">
+          {{ row.useTime }}
+        </a-tag>
+      </template>
+    </SmartTable>
+    <LogDetailModal @register="registerModal" />
+  </div>
+</template>
+
+<script lang="ts" setup>
+  /**
+   * 日志组件
+   */
+  import type { LoginIdent } from './SystemLogComponent.config';
+
+  import { computed } from 'vue';
+  import {
+    useSmartTable,
+    SmartTable,
+    SmartVxeTableAction,
+    ActionItem,
+  } from '@/components/SmartTable';
+  import { useSizeSetting } from '@/hooks/setting/UseSizeSetting';
+  import { useI18n } from '@/hooks/web/useI18n';
+  import { useModal } from '@/components/Modal';
+
+  import { listApi } from './SystemLogComponent.api';
+  import { getTableColumns, getSearchFormSchemas } from './SystemLogComponent.config';
+  import LogDetailModal from './components/LogDetailModal.vue';
+
+  const props = defineProps({
+    ident: {
+      type: String as PropType<LoginIdent>,
+      required: true,
+    },
+  });
+
+  const { t } = useI18n();
+  const { getTableSize } = useSizeSetting();
+
+  const [registerModal, { openModal }] = useModal();
+
+  const getTableActions = (row): ActionItem[] => {
+    return [
+      {
+        label: t('common.title.details'),
+        preIcon: 'ant-design:bars-outlined',
+        onClick: () => {
+          openModal(true, row.logId);
+        },
+      },
+    ];
+  };
+
+  const getClass = computed(() => {
+    if (props.ident === 'INTERFACE_LOG') {
+      return ['two-search'];
+    }
+    return [];
+  });
+
+  /**
+   * 获取使用时间tag颜色
+   * @param useTime 时长
+   */
+  const getUseTimeTagColor = (useTime: number) => {
+    if (useTime <= 300) {
+      return 'blue';
+    }
+    if (useTime <= 500) {
+      return 'green';
+    }
+    if (useTime <= 1000) {
+      return 'orange';
+    }
+    return 'pink';
+  };
+
+  const [registerTable] = useSmartTable({
+    customConfig: { storage: true },
+    columns: getTableColumns(props.ident),
+    height: 'auto',
+    border: true,
+    rowConfig: {
+      isHover: true,
+    },
+    stripe: true,
+    showOverflow: 'tooltip',
+    pagerConfig: true,
+    useSearchForm: true,
+    searchFormConfig: {
+      layout: 'inline',
+      actionColOptions: {
+        span: undefined,
+      },
+      compact: true,
+      colon: true,
+      searchWithSymbol: true,
+      schemas: getSearchFormSchemas(t, props.ident),
+    },
+    sortConfig: {
+      remote: true,
+      defaultSort: {
+        field: 'createTime',
+        order: 'desc',
+      },
+    },
+    columnConfig: {
+      resizable: true,
+    },
+    toolbarConfig: {
+      refresh: true,
+      column: { columnOrder: true },
+      zoom: true,
+    },
+    proxyConfig: {
+      ajax: {
+        query: ({ ajaxParameter }) => {
+          const params = {
+            ...ajaxParameter,
+            parameter: {
+              ...ajaxParameter?.parameter,
+              'ident@=': props.ident,
+            },
+          };
+          return listApi(params);
+        },
+      },
+    },
+  });
+</script>
+
+<style lang="less" scoped>
+  .two-search {
+    :deep(.smart-table-container) {
+      height: calc(100% - 120px) !important;
+    }
+  }
+</style>

+ 86 - 0
src/modules/system/views/log/components/LogDetailModal.vue

@@ -0,0 +1,86 @@
+<template>
+  <BasicModal @register="registerModal" :title="$t('common.title.details')" width="1000px">
+    <template #footer>
+      <a-button type="primary" @click="closeModal">
+        {{ $t('common.title.close') }}
+      </a-button>
+    </template>
+    <Descriptions bordered>
+      <DescriptionsItem
+        :labelStyle="firstLabelStyle"
+        :label="$t('system.views.log.title.operationType')"
+      >
+        {{ detailsData.operationType }}
+      </DescriptionsItem>
+      <DescriptionsItem :label="$t('system.views.log.title.requestPath')">
+        {{ detailsData.requestPath }}
+      </DescriptionsItem>
+      <DescriptionsItem :label="$t('system.views.log.title.statusCode')">
+        <a-tag
+          :color="
+            detailsData.statusCode >= 200 && detailsData.statusCode < 300 ? '#2db7f5' : '#f50'
+          "
+        >
+          {{ detailsData.statusCode }}
+        </a-tag>
+      </DescriptionsItem>
+
+      <DescriptionsItem :label="$t('system.views.log.title.operation')" :span="2">
+        {{ detailsData.operation }}
+      </DescriptionsItem>
+      <DescriptionsItem :label="$t('system.views.log.title.logSource')">
+        {{ detailsData.logSource }}
+      </DescriptionsItem>
+
+      <DescriptionsItem :label="$t('system.views.log.title.createTime')" :span="2">
+        {{ detailsData.createTime }}
+      </DescriptionsItem>
+      <DescriptionsItem :label="$t('system.views.log.title.ip')">
+        {{ detailsData.ip }}
+      </DescriptionsItem>
+
+      <DescriptionsItem :label="$t('system.views.log.title.method')" :span="2">
+        {{ detailsData.method }}
+      </DescriptionsItem>
+      <DescriptionsItem :label="$t('system.views.log.title.useTime')">
+        {{ detailsData.useTime }}
+      </DescriptionsItem>
+
+      <DescriptionsItem :label="$t('system.views.log.title.params')" :span="3">
+        {{ detailsData.params }}
+      </DescriptionsItem>
+
+      <DescriptionsItem :label="$t('system.views.log.title.result')" :span="3">
+        {{ detailsData.result }}
+      </DescriptionsItem>
+      <DescriptionsItem :label="$t('system.views.log.title.errorMessage')" :span="3">
+        {{ detailsData.errorMessage }}
+      </DescriptionsItem>
+    </Descriptions>
+  </BasicModal>
+</template>
+
+<script lang="ts" setup>
+  import { ref } from 'vue';
+  import { useModalInner, BasicModal } from '@/components/Modal';
+  import { Descriptions, DescriptionsItem } from 'ant-design-vue';
+
+  import { getByIdApi } from '../SystemLogComponent.api';
+
+  const firstLabelStyle = {
+    width: '120px',
+  };
+
+  const detailsData = ref<Recordable>({});
+  const [registerModal, { changeLoading, closeModal }] = useModalInner(async (id) => {
+    detailsData.value = {};
+    try {
+      changeLoading(true);
+      detailsData.value = await getByIdApi(id);
+    } finally {
+      changeLoading(false);
+    }
+  });
+</script>
+
+<style scoped></style>

+ 32 - 0
src/modules/system/views/log/lang/en_US.ts

@@ -0,0 +1,32 @@
+export default {
+  system: {
+    views: {
+      log: {
+        title: {
+          operation: 'Log content',
+          logSource: 'Log source',
+          createUserId: 'Operator',
+          ip: 'Access IP',
+          operationType: 'Operation type',
+          requestPath: 'Request path',
+          statusCode: 'Status code',
+          method: 'Request function',
+          useTime: 'Use time(ms)',
+          createTime: 'Create time',
+          logSourceAuto: 'Automatic',
+          logSourceManual: 'Manual',
+          logSourceLoginSuccess: 'Login success',
+          logSourceLogout: 'Logout log',
+          logSourceLoginFail: 'Login fail',
+          operationTypeAdd: 'Add',
+          operationTypeDelete: 'Delete',
+          operationTypeUpdate: 'Update',
+          operationTypeQuery: 'Query',
+          params: 'Request parameters',
+          result: 'Return results',
+          errorMessage: 'Error message',
+        },
+      },
+    },
+  },
+};

+ 32 - 0
src/modules/system/views/log/lang/zh_CN.ts

@@ -0,0 +1,32 @@
+export default {
+  system: {
+    views: {
+      log: {
+        title: {
+          operation: '日志内容',
+          logSource: '日志来源',
+          createUserId: '操作人',
+          ip: '访问IP',
+          operationType: '操作类型',
+          requestPath: '请求路径',
+          statusCode: '状态码',
+          method: '请求函数',
+          useTime: '用时(ms)',
+          createTime: '创建时间',
+          logSourceAuto: '自动',
+          logSourceManual: '手动',
+          logSourceLoginSuccess: '登录成功',
+          logSourceLogout: '登出日志',
+          logSourceLoginFail: '登录失败',
+          operationTypeAdd: '增加',
+          operationTypeDelete: '删除',
+          operationTypeUpdate: '更新',
+          operationTypeQuery: '查询',
+          params: '请求参数',
+          result: '返回结果',
+          errorMessage: '异常信息',
+        },
+      },
+    },
+  },
+};