@@ -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; | |||
@@ -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[] | |||
} |
@@ -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> | |||
</> | |||
); | |||
}; |
@@ -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> | |||
</> | |||
); | |||
}; |
@@ -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' }) | |||
} | |||
}; |