feat: 操作日志

This commit is contained in:
2025-09-17 18:14:39 +08:00
parent 6d1db25c05
commit 73bc5aec6b
9 changed files with 478 additions and 154 deletions

View File

@@ -1,20 +1,20 @@
// https://umijs.org/config/
import { join } from "node:path";
import { defineConfig } from "@umijs/max";
import defaultSettings from "./defaultSettings";
import proxy from "./proxy";
import { join } from 'node:path';
import { defineConfig } from '@umijs/max';
import defaultSettings from './defaultSettings';
import proxy from './proxy';
import routes from "./routes";
import routes from './routes';
const { UMI_ENV = "dev" } = process.env;
const { UMI_ENV = 'dev' } = process.env;
/**
* @name 使用公共路径
* @description 部署时的路径,如果部署在非根目录下,需要配置这个变量
* @doc https://umijs.org/docs/api/config#publicpath
*/
const PUBLIC_PATH: string = "/";
const PUBLIC_PATH: string = '/';
export default defineConfig({
/**
@@ -83,7 +83,7 @@ export default defineConfig({
* @name layout 插件
* @doc https://umijs.org/docs/max/layout-menu
*/
title: "Ant Design Pro",
title: 'Ant Design Pro',
layout: {
locale: true,
...defaultSettings,
@@ -94,8 +94,8 @@ export default defineConfig({
* @doc https://umijs.org/docs/max/moment2dayjs
*/
moment2dayjs: {
preset: "antd",
plugins: ["duration"],
preset: 'antd',
plugins: ['duration'],
},
/**
* @name 国际化插件
@@ -103,7 +103,7 @@ export default defineConfig({
*/
locale: {
// default zh-CN
default: "zh-CN",
default: 'zh-CN',
antd: true,
// default true, when it is true, will use `navigator.language` overwrite default
baseNavigator: true,
@@ -119,7 +119,7 @@ export default defineConfig({
theme: {
cssVar: true,
token: {
fontFamily: "AlibabaSans, sans-serif",
fontFamily: 'AlibabaSans, sans-serif',
},
},
},
@@ -142,30 +142,33 @@ export default defineConfig({
*/
headScripts: [
// 解决首次加载时白屏的问题
{ src: join(PUBLIC_PATH, "scripts/loading.js"), async: true },
{ src: join(PUBLIC_PATH, 'scripts/loading.js'), async: true },
],
//================ pro 插件配置 =================
presets: ["umi-presets-pro"],
presets: ['umi-presets-pro'],
/**
* @name openAPI 插件的配置
* @description 基于 openapi 的规范生成serve 和mock能减少很多样板代码
* @doc https://pro.ant.design/zh-cn/docs/openapi/
*/
openAPI: [
// {
// requestLibPath: "import { request } from '@umijs/max'",
// schemaPath: join(__dirname, "oneapi/prodapi.json"),
// mock: false,
// projectName: "prodApi",
// },
// {schemaPath: "./docs/apifox-api.json",
// requestLibPath: "import { request } from '@umijs/max'",
// schemaPath: join(__dirname, "oneapi.json"),
// projectName: "login",
// },
],
// openAPI: [
// // {
// // requestLibPath: "import { request } from '@umijs/max'",
// // schemaPath: join(__dirname, "oneapi/prodapi.json"),
// // mock: false,
// // projectName: "prodApi",
// // },
// // {schemaPath: "./docs/apifox-api.json",
// // requestLibPath: "import { request } from '@umijs/max'",
// // schemaPath: join(__dirname, "oneapi.json"),
// // projectName: "login",
// // },
// ],
codeSplitting: {
jsStrategy: 'granularChunks',
},
mock: {
include: ["mock/**/*", "src/pages/**/_mock.ts"],
include: ['mock/**/*', 'src/pages/**/_mock.ts'],
},
/**
* @name 是否开启 mako
@@ -177,6 +180,6 @@ export default defineConfig({
requestRecord: {},
exportStatic: {},
define: {
"process.env.CI": process.env.CI,
'process.env.CI': process.env.CI,
},
});

View File

@@ -7,8 +7,8 @@
"scripts": {
"analyze": "cross-env ANALYZE=1 max build",
"build": "max build",
"deploy": "npm run build && npm run gh-pages",
"dev": "npm run start:dev",
"deploy": "pnpm run build && npm run gh-pages",
"dev": "pnpm run start:dev",
"gh-pages": "gh-pages -d dist",
"i18n-remove": "pro i18n-remove --locale=zh-CN --write",
"postinstall": "max setup",

12
pnpm-lock.yaml generated
View File

@@ -14075,14 +14075,14 @@ snapshots:
'@loadable/component@5.15.2(react@18.3.1)':
dependencies:
'@babel/runtime': 7.23.6
'@babel/runtime': 7.28.4
hoist-non-react-statics: 3.3.2
react: 18.3.1
react-is: 16.13.1
'@loadable/component@5.15.2(react@19.1.1)':
dependencies:
'@babel/runtime': 7.23.6
'@babel/runtime': 7.28.4
hoist-non-react-statics: 3.3.2
react: 19.1.1
react-is: 16.13.1
@@ -15402,7 +15402,7 @@ snapshots:
'@umijs/history@5.3.1':
dependencies:
'@babel/runtime': 7.23.6
'@babel/runtime': 7.28.4
query-string: 6.14.1
'@umijs/lint@4.4.12(eslint@8.35.0)(jest@30.1.3(@types/node@24.3.1)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@24.3.1)(typescript@5.9.2)))(stylelint@14.8.2)(typescript@5.9.2)':
@@ -16597,7 +16597,7 @@ snapshots:
babel-plugin-macros@2.6.1:
dependencies:
'@babel/runtime': 7.4.5
'@babel/runtime': 7.28.4
cosmiconfig: 5.2.1
resolve: 1.22.10
@@ -23257,7 +23257,7 @@ snapshots:
react-helmet-async@1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@babel/runtime': 7.23.6
'@babel/runtime': 7.28.4
invariant: 2.2.4
prop-types: 15.8.1
react: 18.3.1
@@ -23267,7 +23267,7 @@ snapshots:
react-helmet-async@1.3.0(react-dom@19.1.1(react@19.1.1))(react@19.1.1):
dependencies:
'@babel/runtime': 7.23.6
'@babel/runtime': 7.28.4
invariant: 2.2.4
prop-types: 15.8.1
react: 19.1.1

View File

@@ -1,36 +1,37 @@
// components/EnhancedProTable/EnhancedProTable.tsx
import { PlusOutlined } from '@ant-design/icons';
import {
type ActionType,
type ParamsType,
ProColumns,
ProTable,
TableDropdown,
} from '@ant-design/pro-components';
import { Button, Space } from 'antd';
import React, {
useRef,
useState,
useCallback,
useMemo,
act,
forwardRef,
} from "react";
import {
ProTable,
ProColumns,
ActionType,
TableDropdown,
ParamsType,
} from "@ant-design/pro-components";
import { Button, Space } from "antd";
import {
EnhancedProTableProps,
BaseRecord,
TableAction,
ToolbarAction,
} from "./types";
useCallback,
useMemo,
useRef,
useState,
} from 'react';
import {
buildTableDropdownMenuItems,
handleTableDropdownSelect,
formatPaginationTotal,
} from "@/utils/antd/tableHelpers";
import { PlusOutlined } from "@ant-design/icons";
handleTableDropdownSelect,
} from '@/utils/antd/tableHelpers';
import {
type BaseRecord,
type EnhancedProTableProps,
TableAction,
ToolbarAction,
} from './types';
function EnhancedProTable<T extends BaseRecord, U extends ParamsType = any>(
props: EnhancedProTableProps<T, U>,
ref: React.Ref<ActionType | undefined> | undefined
ref: React.Ref<ActionType | undefined> | undefined,
) {
const {
columns,
@@ -87,7 +88,7 @@ function EnhancedProTable<T extends BaseRecord, U extends ParamsType = any>(
</Space>
);
},
[showSelection]
[showSelection],
);
const toolBarRender = useCallback(
@@ -96,7 +97,7 @@ function EnhancedProTable<T extends BaseRecord, U extends ParamsType = any>(
rows: {
selectedRowKeys?: (string | number)[] | undefined;
selectedRows?: T[] | undefined;
}
},
) => {
const toolbarElements =
toolbarActions?.map((action) => {
@@ -125,7 +126,7 @@ function EnhancedProTable<T extends BaseRecord, U extends ParamsType = any>(
// ];
return toolbarElements;
},
[toolbarActions]
[toolbarActions],
);
return (
<ProTable<T, U>
@@ -140,9 +141,9 @@ function EnhancedProTable<T extends BaseRecord, U extends ParamsType = any>(
showSorterTooltip
tableAlertRender={tableAlertRender}
// tableAlertOptionRender={tableAlertOptionRender}
scroll={{ x: "max-content" }}
scroll={{ x: 'max-content' }}
search={{
labelWidth: "auto",
labelWidth: 'auto',
defaultCollapsed: false,
...restProps.search,
}}
@@ -156,6 +157,7 @@ function EnhancedProTable<T extends BaseRecord, U extends ParamsType = any>(
pagination={{
showSizeChanger: true,
showQuickJumper: true,
pageSize: 10,
showTotal: formatPaginationTotal,
...restProps.pagination,
}}
@@ -165,7 +167,7 @@ function EnhancedProTable<T extends BaseRecord, U extends ParamsType = any>(
export default forwardRef(EnhancedProTable) as <
T extends BaseRecord,
U extends ParamsType = any
U extends ParamsType = any,
>(
props: EnhancedProTableProps<T, U> & { ref?: React.Ref<ActionType> }
props: EnhancedProTableProps<T, U> & { ref?: React.Ref<ActionType> },
) => React.ReactElement;

View File

@@ -0,0 +1,72 @@
import {
ProDescriptions,
type ProDescriptionsItemProps,
} from '@ant-design/pro-components';
import { type DescriptionsProps, Modal } from 'antd';
import React, {
forwardRef,
useCallback,
useImperativeHandle,
useState,
} from 'react';
export interface DescriptionsFormRef {
open: (data?: Record<string, any>) => void;
close: () => void;
}
interface Props {
columns: ProDescriptionsItemProps<Record<string, any>, 'text'>[];
title: string;
}
const ModalDescriptions = forwardRef((props: Props, ref) => {
const { columns, title } = props;
const [visible, setVisible] = useState<boolean>(false);
const [data, setData] = useState<DescriptionsProps['items']>([]);
useImperativeHandle(ref, () => ({
open: (data: DescriptionsProps['items']) => {
console.log(data);
if (data) {
setData(data);
}
setVisible(true);
},
close: () => setVisible(false),
}));
const changeVisible = useCallback(
(flag: boolean) => {
setVisible(flag);
},
[visible],
);
return (
<Modal
open={visible}
width={800}
onCancel={() => changeVisible(false)}
footer={null}
>
<ProDescriptions
labelStyle={{ minWidth: '200px' }}
columns={columns}
dataSource={data}
bordered
column={1}
size="small"
title={title}
// tooltip="操作日志详情"
></ProDescriptions>
{/* <Descriptions
title="操作日志"
column={1}
bordered
size="small"
items={data}
/> */}
</Modal>
);
});
export default React.memo(ModalDescriptions);

View File

@@ -0,0 +1,134 @@
import {
type ProColumns,
type ProDescriptionsItemProps,
ProFormColumnsType,
} from '@ant-design/pro-components';
import { DescriptionsProps } from 'antd';
import dayjs from 'dayjs';
import type { OperateLogVO } from '@/services/system/log/operate';
export const baseTenantColumns: ProColumns<OperateLogVO>[] = [
{
title: '日志编号',
dataIndex: 'id',
tip: '日志编号',
width: 100,
hideInSearch: true, // 在搜索表单中隐藏
},
{
title: '操作人',
dataIndex: 'userName',
tip: '操作人', // 提示信息
},
{
title: '操作模块',
dataIndex: 'type',
},
{
title: '操作名',
dataIndex: 'subType',
},
{
title: '操作内容',
dataIndex: 'action',
},
{
title: '操作时间',
dataIndex: 'createTime',
valueType: 'dateRange',
search: {
transform: (value) => {
return {
[`createTime[0]`]: dayjs(value[0])
.startOf('day')
.format('YYYY-MM-DD HH:mm:ss'),
[`createTime[1]`]: dayjs(value[1])
.endOf('day')
.format('YYYY-MM-DD HH:mm:ss'),
};
},
},
render: (_, record: OperateLogVO) =>
dayjs(record.createTime).format('YYYY-MM-DD HH:mm:ss'),
},
{
title: '业务编号',
dataIndex: 'bizId',
},
{
title: '操作 IP',
dataIndex: 'userIp',
hideInSearch: true,
},
];
export const descriptionsColumns = (): ProDescriptionsItemProps<
Record<string, any>,
'text'
>[] => [
{
title: '日志主键',
key: 'id',
dataIndex: 'id',
},
{
title: '链路追踪',
key: 'traceId',
dataIndex: 'traceId',
},
{
title: '操作人编号',
key: 'userId',
dataIndex: 'userId',
},
{
title: '操作人名字',
key: 'userName',
dataIndex: 'userName',
},
{
title: '操作人 IP',
key: 'userIp',
dataIndex: 'userIp',
},
{
title: '操作人 UA',
key: 'userAgent',
dataIndex: 'userAgent',
},
{
title: '操作模块',
key: 'type',
dataIndex: 'type',
},
{
title: '操作名',
key: 'subType',
dataIndex: 'subType',
},
{
title: '操作内容',
key: 'action',
dataIndex: 'action',
},
{
title: '操作拓展参数',
key: 'extra',
dataIndex: 'extra',
},
{
title: '请求 URL',
key: 'requestUrl',
dataIndex: 'requestUrl',
},
{
title: '操作时间',
key: 'createTime',
dataIndex: 'createTime',
},
{
title: '业务编号',
key: 'bizId',
dataIndex: 'bizId',
},
];

View File

@@ -1,5 +1,70 @@
import type { ActionType, ProColumns } from '@ant-design/pro-components';
import React, { useRef } from 'react';
import EnhancedProTable from '@/components/EnhancedProTable';
import ModalDescriptions, {
type DescriptionsFormRef,
} from '@/components/ModalDescriptions';
import {
getOperateLogPage,
type OperateLogVO,
} from '@/services/system/log/operate';
import { baseTenantColumns, descriptionsColumns } from './config';
const SyStemLogOperate = () => {
return <>SyStemLogOperate</>;
const tableRef = useRef<ActionType>(null);
const descriptionsRef = useRef<DescriptionsFormRef>(null);
const onFetch = async (
params: OperateLogVO & {
pageSize?: number;
current?: number;
},
) => {
const data = await getOperateLogPage({
...params,
pageNo: params.current,
pageSize: params.pageSize,
});
return {
data: data.list,
success: true,
total: data.total,
};
};
const handleDetail = (record: OperateLogVO) => {
descriptionsRef.current?.open(record);
};
const actionColumns: ProColumns<OperateLogVO> = {
title: '操作',
dataIndex: 'option',
valueType: 'option',
fixed: 'right',
width: 80,
render: (text: React.ReactNode, record: OperateLogVO) => [
<a key="editable" onClick={() => handleDetail(record)}>
</a>,
],
};
const columns = [...baseTenantColumns, actionColumns];
return (
<>
<EnhancedProTable<OperateLogVO>
ref={tableRef}
columns={columns}
request={onFetch}
headerTitle="操作日志"
showIndex={false}
showSelection={false}
/>
<ModalDescriptions
ref={descriptionsRef}
title="操作日志详情"
columns={descriptionsColumns()}
/>
</>
);
};
export default SyStemLogOperate;
export default React.memo(SyStemLogOperate);

View File

@@ -1,33 +1,37 @@
import { type TenantVO, deleteTenant } from "@/services/system/tenant/list";
import { ProColumns, ProFormColumnsType } from "@ant-design/pro-components";
import { DatePicker, Modal, Popconfirm } from "antd";
import { FormInstance } from "antd/lib";
import dayjs from "dayjs";
import type {
ProColumns,
ProFormColumnsType,
} from '@ant-design/pro-components';
import { DatePicker, Modal, Popconfirm } from 'antd';
import { FormInstance } from 'antd/lib';
import dayjs from 'dayjs';
import { deleteTenant, type TenantVO } from '@/services/system/tenant/list';
import { getTenantPackageList } from '@/services/system/tenant/package';
export const baseTenantColumns: ProColumns<TenantVO>[] = [
{
title: "租户编号",
dataIndex: "id",
tip: "租户编号",
title: '租户编号',
dataIndex: 'id',
tip: '租户编号',
width: 100,
hideInSearch: true, // 在搜索表单中隐藏
},
{
title: "租户名",
dataIndex: "name",
tip: "租户名", // 提示信息
title: '租户名',
dataIndex: 'name',
tip: '租户名', // 提示信息
},
{
title: "租户套餐",
dataIndex: "packageId",
valueType: "select",
title: '租户套餐',
dataIndex: 'packageId',
valueType: 'select',
hideInSearch: true, // 在搜索表单中隐藏
request: async () => {
return [
{
label: "默认套餐",
value: 1,
},
];
const packageList: { id: number; name: string }[] =
await getTenantPackageList();
console.log(packageList);
packageList.map((item) => ({ label: item.name, value: item.id }));
return packageList.map((item) => ({ label: item.name, value: item.id }));
},
// valueEnum: {
// all: { text: "全部", status: "Default" },
@@ -36,60 +40,67 @@ export const baseTenantColumns: ProColumns<TenantVO>[] = [
// },
},
{
title: "联系人",
dataIndex: "contactName",
title: '联系人',
dataIndex: 'contactName',
},
{
title: "联系手机",
dataIndex: "contactMobile",
title: '联系手机',
dataIndex: 'contactMobile',
},
{
title: "账号额度",
dataIndex: "accountCount",
title: '账号额度',
dataIndex: 'accountCount',
hideInSearch: true, // 在搜索表单中隐藏
},
{
title: "过期时间",
dataIndex: "expireTime",
valueType: "dateTime",
title: '过期时间',
dataIndex: 'expireTime',
valueType: 'dateTime',
hideInSearch: true, // 在搜索表单中隐藏
},
{ title: "绑定域名", dataIndex: "website", width: 100 },
{ title: '绑定域名', dataIndex: 'website', width: 100, hideInSearch: true },
{
title: "租户状态",
dataIndex: "status",
valueType: "select",
valueEnum: {
all: { text: "全部", status: "Default" },
open: { text: "未解决", status: "Error" },
closed: { text: "已解决", status: "Success" },
title: '租户状态',
dataIndex: 'status',
valueType: 'select',
fieldProps: {
options: [
{
label: '开启',
value: 0,
},
{
label: '关闭',
value: 1,
},
],
},
},
{
title: "创建时间",
dataIndex: "createTime",
valueType: "dateRange",
title: '创建时间',
dataIndex: 'createTime',
valueType: 'dateRange',
search: {
transform: (value) => {
return [`${value[0]} 00:00:00`, `${value[1]} 00:00:00`];
},
},
render: (_, record: TenantVO) =>
dayjs(record.createTime).format("YYYY-MM-DD HH:mm:ss"),
dayjs(record.createTime).format('YYYY-MM-DD HH:mm:ss'),
},
];
export const formColumns = (type: string): ProFormColumnsType[] => [
{
title: "租户名",
dataIndex: "name",
tip: "租户名", // 提示信息
title: '租户名',
dataIndex: 'name',
tip: '租户名', // 提示信息
formItemProps: {
rules: [
{
required: true,
message: "请输入用户名",
message: '请输入用户名',
},
// {
// min: 2,
@@ -100,121 +111,121 @@ export const formColumns = (type: string): ProFormColumnsType[] => [
},
},
{
title: "租户套餐",
dataIndex: "packageId",
valueType: "select",
title: '租户套餐',
dataIndex: 'packageId',
valueType: 'select',
formItemProps: {
rules: [
{
required: true,
message: "请选择租户套餐",
message: '请选择租户套餐',
},
],
},
fieldProps: {
placeholder: "请选择套餐类型",
placeholder: '请选择套餐类型',
options: [
{
label: "普通套餐",
label: '普通套餐',
value: 111,
},
],
},
},
{
title: "联系人",
dataIndex: "contactName",
title: '联系人',
dataIndex: 'contactName',
},
{
title: "联系手机",
dataIndex: "contactMobile",
title: '联系手机',
dataIndex: 'contactMobile',
formItemProps: {
rules: [
{
required: true,
message: "请输入联系手机",
message: '请输入联系手机',
},
],
},
},
{
title: "用户名称",
dataIndex: "username",
hideInForm: type === "update",
title: '用户名称',
dataIndex: 'username',
hideInForm: type === 'update',
formItemProps: {
rules: [
{
required: true,
message: "请输入用户名称",
message: '请输入用户名称',
},
{
pattern: /^[a-zA-Z0-9]+$/,
message: "用户账号由 0-9、a-z、A-Z 组成",
message: '用户账号由 0-9、a-z、A-Z 组成',
},
// 用户账号由 数字、字母组成
],
},
},
{
title: "用户密码",
dataIndex: "password",
valueType: "password",
hideInForm: type === "update",
title: '用户密码',
dataIndex: 'password',
valueType: 'password',
hideInForm: type === 'update',
fieldProps: {
placeholder: "请输入用户密码",
autoComplete: "new-password",
placeholder: '请输入用户密码',
autoComplete: 'new-password',
},
formItemProps: {
rules: [
{
required: true,
message: "请输入用户密码",
message: '请输入用户密码',
},
{
min: 4,
max: 16,
message: "密码长度为4-16个字符",
message: '密码长度为4-16个字符',
},
],
},
},
{
title: "账号额度",
dataIndex: "accountCount",
valueType: "digit",
title: '账号额度',
dataIndex: 'accountCount',
valueType: 'digit',
},
{
title: "过期时间",
dataIndex: "expireTime",
valueType: "date",
title: '过期时间',
dataIndex: 'expireTime',
valueType: 'date',
fieldProps: {
placeholder: "请选择过期时间",
format: "YYYY-MM-DD",
placeholder: '请选择过期时间',
format: 'YYYY-MM-DD',
},
},
{ title: "绑定域名", dataIndex: "website" },
{ title: '绑定域名', dataIndex: 'website' },
{
title: "租户状态",
dataIndex: "status",
valueType: "radio",
title: '租户状态',
dataIndex: 'status',
valueType: 'radio',
formItemProps: {
rules: [
{
required: true,
message: "请选择租户状态",
message: '请选择租户状态',
},
],
},
fieldProps: {
placeholder: "请选择套餐类型",
placeholder: '请选择套餐类型',
options: [
{
label: "启用",
label: '启用',
value: 1,
},
{
label: "禁用",
label: '禁用',
value: 0,
},
],

View File

@@ -0,0 +1,37 @@
import { request } from "@umijs/max";
export type OperateLogVO = {
id: number;
traceId: string;
userType: number;
userId: number;
userName: string;
type: string;
subType: string;
bizId: number;
action: string;
extra: string;
requestMethod: string;
requestUrl: string;
userIp: string;
userAgent: string;
creator: string;
creatorName: string;
createTime: Date;
};
// 查询操作日志列表
// export const getOperateLogPage = (params: PageParam) => {
// return request.get({ url: '/system/operate-log/page', params })
// }
export async function getOperateLogPage(params: PageParam) {
return request("/system/operate-log/page", {
method: "GET",
params,
});
}
// // 导出操作日志
// export const exportOperateLog = (params: any) => {
// return request.download({ url: "/system/operate-log/export", params });
// };