@@ -7,6 +7,7 @@ export * from './tenant.data.ts' | |||||
export * from './template.data.ts' | export * from './template.data.ts' | ||||
export * from './platform-product.data.ts' | export * from './platform-product.data.ts' | ||||
export * from './notice.data.ts' | export * from './notice.data.ts' | ||||
export * from './logs.data.ts' | |||||
export interface ResponseDTO<T>{ | export interface ResponseDTO<T>{ | ||||
code: number; | 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' }) | |||||
} | |||||
}; |