Browse Source

add:operate-log、login-log

dev
powersir 11 months ago
parent
commit
dbf10da65c
5 changed files with 563 additions and 0 deletions
  1. +1
    -0
      src/models/index.ts
  2. +51
    -0
      src/models/logs.data.ts
  3. +213
    -0
      src/pages/system/log/login-log/index.tsx
  4. +266
    -0
      src/pages/system/log/operate-log/index.tsx
  5. +32
    -0
      src/request/service/logs.ts

+ 1
- 0
src/models/index.ts View File

@@ -7,6 +7,7 @@ export * from './tenant.data.ts'
export * from './template.data.ts'
export * from './platform-product.data.ts'
export * from './notice.data.ts'
export * from './logs.data.ts'

export interface ResponseDTO<T>{
code: number;


+ 51
- 0
src/models/logs.data.ts View File

@@ -0,0 +1,51 @@
export type OperateLogVO = {
id: number
userNickname: string
traceId: string
userId: number
module: string
name: string
type: number
content: string
exts: Map<String, Object>
requestMethod: string
requestUrl: string
userIp: string
userAgent: string
javaMethod: string
javaMethodArgs: string
startTime: Date
duration: number
resultCode: number
resultMsg: string
resultData: string
}

export interface OperateLogPageReqVO extends PageParam {
module?: string
userNickname?: string
type?: number
success?: boolean
startTime?: Date[]
}


export interface LoginLogVO {
id: number
logType: number
traceId: number
userId: number
userType: number
username: string
status: number
userIp: string
userAgent: string
createTime: Date
}

export interface LoginLogReqVO extends PageParam {
userIp?: string
username?: string
status?: boolean
createTime?: Date[]
}

+ 213
- 0
src/pages/system/log/login-log/index.tsx View File

@@ -0,0 +1,213 @@
import React, { useState, useEffect, useRef } from 'react';
import { useSetState } from 'ahooks';
import { Space, Table, Button, Input, FloatButton, Divider, Tag, Card, Tooltip } from 'antd';
import type { TableColumnsType, InputRef } from 'antd';
import type { ColumnType, TableProps } from 'antd/es/table';
import { t } from '@/utils/i18n';
import { DownloadOutlined , SearchOutlined } from '@ant-design/icons';
import { antdUtils } from '@/utils/antd';
import { useRequest } from '@/hooks/use-request';
import { formatDate } from '@/utils/formatTime'
import logsService from '@/request/service/logs';
import { LoginLogVO, LoginLogReqVO } from '@/models';


type DataIndex = keyof LoginLogVO;

const mapOperation = (operation: number) => {
if (operation === 1) {
return (<Tag color="purple">通知</Tag>)
} else if (operation === 2) {
return (<Tag color="purple">新增</Tag>)
} else if (operation === 3) {
return (<Tag color="blue">修改</Tag>)
} else if (operation === 4) {
return (<Tag color="red">删除</Tag>)
} else {
return (<Tag color="red">未知({operation})</Tag>)
}
}

export default () => {
const [dataSource, setDataSource] = useState<LoginLogVO[]>([]);

const { runAsync: getPageData } = useRequest(logsService.getLoginLogPageApi, { manual: true });

const [searchState, setSearchState] = useSetState<LoginLogReqVO>({
pageNo: 1,
pageSize: 10
});
const [total, setTotal] = useState(0)
const searchInput = useRef<InputRef>(null);
const [onSearching, setOnSearching] = useState(false);

const load = async () => {
console.log(searchState)
const [error, { code, msg, data }] = await getPageData(searchState);
setOnSearching(false);
if (error || code !== 0) {
antdUtils.message?.open({ type: 'error', content: msg ?? '操作失败' });
return
}
setTotal(data.total);
setDataSource(data.list);
};

const getColumnSearchProps = (dataIndex: DataIndex): ColumnType<LoginLogVO> => ({
filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters, close }) => (
<div style={{ padding: 8 }} onKeyDown={(e) => e.stopPropagation()}>
<Input.Search
ref={searchInput}
placeholder={"输入公告标题"}
value={selectedKeys[0]}
onChange={(e) => {
setSelectedKeys(e.target.value && e.target.value !== '' ? [e.target.value] : [])
}}
onSearch={(value) => {
if (value === '' && clearFilters) {
clearFilters!!()
}
confirm();
}}
onPressEnter={() => confirm()}
allowClear
style={{ marginBottom: 8, display: 'block' }}
enterButton="搜索"
size="middle"
loading={onSearching}
/>
</div>
),
filterIcon: (filtered: boolean) => (
<SearchOutlined style={{ color: filtered ? 'primaryColor' : undefined }} />
),
onFilterDropdownOpenChange: (visible) => {
if (visible) {
setTimeout(() => searchInput.current?.select(), 100);
}
},
});

const columns: TableColumnsType<LoginLogVO> = [
{
title: '日志编号',
dataIndex: 'id',
key: 'id',
align: 'center',
width: 100,
},
{
title: '日志类型',
dataIndex: 'logType',
key: 'logType',
align: 'center',
width: 100,
render: (value, record) => (
value === 100 ? <Tag color="purple"> 账号登陆 </Tag> : <Tag color='yellow'> 退出登陆</Tag>
)
},
{
title: '用户名称',
dataIndex: 'username',
key: 'username',
align: 'center',
width: 150,
},
{
title: '登录地址',
dataIndex: 'userIp',
key: 'userIp',
align: 'center',
width: 150,
},
{
title: '浏览器',
dataIndex: 'userAgent',
key: 'userIp',
align: 'center',
width: 300,
render: (text, record) => (
<Tooltip title={text}>
<div className='text-ellipsis overflow-hidden whitespace-nowrap max-w-xs'>
{text}
</div>
</Tooltip>
)
},
{
title: '登陆结果',
dataIndex: 'result',
key: 'result',
align: 'center',
width: 100,
},
{
title: '登录日期',
key: 'createTime',
dataIndex: 'createTime',
width: 200,
align: 'center',
render: (value: number) => {
return formatDate(new Date(value), "YYYY-mm-dd HH:MM:SS")
}
},
{
title: t("QkOmYwne" /* 操作 */),
key: 'action',
fixed: 'right',
align: 'center',
width: 100,
render: (value: LoginLogVO, record) => (
<Space size="small" split={(<Divider type='vertical' />)}>
<a onClick={() => {

}}>
详情
</a>
</Space>
),
},
];

useEffect(() => {
load();
}, [searchState]);

const onChange: TableProps<LoginLogVO>['onChange'] = (pagination, filters, sorter, extra) => {
const state: LoginLogReqVO = {
// title: filters.title ? filters.title[0] as string : undefined,
// status: filters.status ? filters.status[0] as number : undefined,
pageNo: pagination.current,
pageSize: pagination.pageSize
}
setOnSearching(true);
setSearchState(state);
};

return (
<>
<div>
<Card className='mt-[4px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]'>
<Table rowKey="id"
scroll={{ x: true }}
columns={columns}
dataSource={dataSource}
onChange={onChange}
className='bg-transparent'
pagination={{
position: ['bottomRight'],
current: searchState.pageNo,
pageSize: searchState.pageSize,
total
}}
/>
</Card>
<FloatButton
type='primary'
tooltip={<div>导出日志</div>}
icon={<DownloadOutlined />}
/>
</div>
</>
);
};

+ 266
- 0
src/pages/system/log/operate-log/index.tsx View File

@@ -0,0 +1,266 @@
import React, { useState, useEffect, useRef } from 'react';
import { useSetState } from 'ahooks';
import { Space, Table, Button, Input, FloatButton, Divider, Tag, Card, Tooltip } from 'antd';
import type { TableColumnsType, InputRef } from 'antd';
import type { ColumnType, TableProps } from 'antd/es/table';
import { t } from '@/utils/i18n';
import { DownloadOutlined, SearchOutlined } from '@ant-design/icons';
import { antdUtils } from '@/utils/antd';
import { useRequest } from '@/hooks/use-request';
import { formatDate } from '@/utils/formatTime'
import logsService from '@/request/service/logs';
import { OperateLogVO, OperateLogPageReqVO } from '@/models';
// import NoticeEditor from './notice-editor';


type DataIndex = keyof OperateLogVO;

const mapOperation = (operation: number) => {
if (operation === 1) {
return (<Tag color="purple">通知</Tag>)
} else if (operation === 2) {
return (<Tag color="purple">新增</Tag>)
} else if (operation === 3) {
return (<Tag color="yellow">修改</Tag>)
} else if (operation === 4) {
return (<Tag color="red">删除</Tag>)
} else if (operation === 5) {
return (<Tag color="blue">导出</Tag>)
} else {
return (<Tag color="red">未知({operation})</Tag>)
}
}

export default () => {
const [dataSource, setDataSource] = useState<OperateLogVO[]>([]);

const { runAsync: getPageData } = useRequest(logsService.getOperateLogPageApi, { manual: true });
const { runAsync: exportOperateLog } = useRequest(logsService.exportOperateLogApi, { manual: true });

const [searchState, setSearchState] = useSetState<OperateLogPageReqVO>({
pageNo: 1,
pageSize: 10
});
const [total, setTotal] = useState(0)
const searchInput = useRef<InputRef>(null);
const [onSearching, setOnSearching] = useState(false);

const load = async () => {
console.log(searchState)
const [error, { code, msg, data }] = await getPageData(searchState);
setOnSearching(false);
if (error || code !== 0) {
antdUtils.message?.open({ type: 'error', content: msg ?? '操作失败' });
return
}
setTotal(data.total);
setDataSource(data.list);
};

const getColumnSearchProps = (dataIndex: DataIndex): ColumnType<OperateLogVO> => ({
filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters, close }) => (
<div style={{ padding: 8 }} onKeyDown={(e) => e.stopPropagation()}>
<Input.Search
ref={searchInput}
placeholder={"输入公告标题"}
value={selectedKeys[0]}
onChange={(e) => {
setSelectedKeys(e.target.value && e.target.value !== '' ? [e.target.value] : [])
}}
onSearch={(value) => {
if (value === '' && clearFilters) {
clearFilters!!()
}
confirm();
}}
onPressEnter={() => confirm()}
allowClear
style={{ marginBottom: 8, display: 'block' }}
enterButton="搜索"
size="middle"
loading={onSearching}
/>
</div>
),
filterIcon: (filtered: boolean) => (
<SearchOutlined style={{ color: filtered ? 'primaryColor' : undefined }} />
),
onFilterDropdownOpenChange: (visible) => {
if (visible) {
setTimeout(() => searchInput.current?.select(), 100);
}
},
});

const columns: TableColumnsType<OperateLogVO> = [
{
title: '编号',
dataIndex: 'id',
key: 'id',
align: 'center',
fixed: 'left',
width: 120,
},
{
title: '模块',
dataIndex: 'module',
key: 'module',
fixed: 'left',
filterSearch: true,
align: 'center',
width: 200,
// ...getColumnSearchProps('title')
},
{
title: '操作名',
dataIndex: 'name',
key: 'name',
align: 'center',
width: 120,
},
{
title: '操作类型',
dataIndex: 'type',
key: 'type',
align: 'center',
width: 120,
render: (value: number) => {
return mapOperation(value)
}
},
{
title: '请求方法',
dataIndex: 'requestMethod',
key: 'requestMethod',
align: 'center',
width: 100,
},
{
title: '请求地址',
dataIndex: 'requestUrl',
key: 'requestUrl',
align: 'center',
width: 150,
},
{
title: '操作人员',
dataIndex: 'userNickname',
key: 'userNickname',
align: 'center',
width: 150,
},
{
title: '用户 IP',
dataIndex: 'userIp',
key: 'userIp',
align: 'center',
width: 150,
},
{
title: 'userAgent',
dataIndex: 'userAgent',
key: 'userAgent',
align: 'center',
width: 150,
render: (text, record) => (
<Tooltip title={text}>
<div className='text-ellipsis overflow-hidden whitespace-nowrap w-[100px]'>
{text}
</div>
</Tooltip>
)
},
{
title: '操作结果',
dataIndex: 'resultCode',
key: 'resultCode',
align: 'center',
width: 150,
},
{
title: '操作日期',
key: 'startTime',
dataIndex: 'startTime',
width: 200,
align: 'center',
render: (value: number) => {
return formatDate(new Date(value), "YYYY-mm-dd HH:MM:SS")
}
},
{
title: '执行时长',
dataIndex: 'duration',
key: 'duration',
align: 'center',
width: 150,
render: (text, record) => (
<p className='text-ellipsis overflow-hidden max-w-xs break-keep'>
{text}ms
</p>
)
},
{
title: t("QkOmYwne" /* 操作 */),
key: 'action',
fixed: 'right',
align: 'center',
width: 100,
render: (value: OperateLogVO, record) => (
<Space size="small" split={(<Divider type='vertical' />)}>
<a onClick={() => {

}}>
详情
</a>
</Space>
),
},
];

const exportLogs = async () => {
await exportOperateLog()
}

useEffect(() => {
load();
}, [searchState]);

const onChange: TableProps<OperateLogVO>['onChange'] = (pagination, filters, sorter, extra) => {
const state: OperateLogPageReqVO = {
// title: filters.title ? filters.title[0] as string : undefined,
// status: filters.status ? filters.status[0] as number : undefined,
pageNo: pagination.current,
pageSize: pagination.pageSize
}
setOnSearching(true);
setSearchState(state);
};

return (
<>
<div>
<Card className='mt-[4px] dark:bg-[rgb(33,41,70)] bg-white roundle-lg px[12px]'>
<Table rowKey="id"
scroll={{ x: true }}
columns={columns}
dataSource={dataSource}
onChange={onChange}
className='bg-transparent'
pagination={{
position: ['bottomRight'],
current: searchState.pageNo,
pageSize: searchState.pageSize,
total
}}
/>
</Card>
<FloatButton
type='primary'
tooltip={<div>导出日志</div>}
icon={<DownloadOutlined />}
onClick={exportLogs}
/>
</div>
</>
);
};

+ 32
- 0
src/request/service/logs.ts View File

@@ -0,0 +1,32 @@
import request from '@/request';
import {
OperateLogVO,
OperateLogPageReqVO,
LoginLogVO,
LoginLogReqVO,
PageData
} from '@/models';

const BASE_OPERATE_URL = '/admin-api/system/operate-log';
const BASE_LOGIN_URL = '/admin-api/system/login-log';

export default {
// 查询操作日志列表
getOperateLogPageApi: (params: OperateLogPageReqVO) => {
return request.get<PageData<OperateLogVO>>(`${BASE_OPERATE_URL}/page`, { params })
},

// 导出操作日志
exportOperateLogApi: (params: OperateLogPageReqVO) => {
return request.get<PageData<OperateLogVO>>(`${BASE_OPERATE_URL}/export`, { params, responseType: 'blob' })
},

// 查询登录日志列表
getLoginLogPageApi: (params: LoginLogReqVO) => {
return request.get<PageData<LoginLogVO>>(`${BASE_LOGIN_URL}/page`, { params })
},
// 导出登录日志
exportLoginLogApi: (params: LoginLogReqVO) => {
return request.get(`${BASE_LOGIN_URL}/export`, { params, responseType: 'blob' })
}
};

Loading…
Cancel
Save