style(backend): 格式化模型文件代码 style(frontend): 调整组件代码格式 chore: 删除旧 ESLint 配置并添加新配置 refactor(backend): 重构模型定义语法 style: 统一箭头函数和对象属性简写
515 lines
15 KiB
React
515 lines
15 KiB
React
import React, { useState, useEffect, useCallback } from 'react';
|
||
import {
|
||
Table,
|
||
Card,
|
||
Space,
|
||
Select,
|
||
DatePicker,
|
||
Input,
|
||
Tag,
|
||
Button,
|
||
message,
|
||
Modal,
|
||
Row,
|
||
Col,
|
||
Descriptions,
|
||
Typography,
|
||
Drawer,
|
||
Statistic,
|
||
} from 'antd';
|
||
import {
|
||
HistoryOutlined,
|
||
SearchOutlined,
|
||
EyeOutlined,
|
||
FilterOutlined,
|
||
ClearOutlined,
|
||
} from '@ant-design/icons';
|
||
import api from '../api';
|
||
import CloseButton from '../components/CloseButton';
|
||
import dayjs from 'dayjs';
|
||
import {
|
||
selectStyles,
|
||
filterInputStyles,
|
||
inputPlaceholders,
|
||
} from '../styles/deviceManagementStyles';
|
||
|
||
const { RangePicker } = DatePicker;
|
||
const { Option } = Select;
|
||
const { Text } = Typography;
|
||
|
||
const MODULE_OPTIONS = [
|
||
{ value: 'device', label: '设备管理' },
|
||
{ value: 'user', label: '用户管理' },
|
||
{ value: 'role', label: '角色管理' },
|
||
];
|
||
|
||
const OPERATION_TYPE_OPTIONS = [
|
||
{ value: 'create', label: '创建' },
|
||
{ value: 'update', label: '更新' },
|
||
{ value: 'delete', label: '删除' },
|
||
{ value: 'batch_delete', label: '批量删除' },
|
||
{ value: 'status_change', label: '状态变更' },
|
||
{ value: 'move', label: '移动' },
|
||
{ value: 'permission_change', label: '权限变更' },
|
||
];
|
||
|
||
const RESULT_OPTIONS = [
|
||
{ value: 'success', label: '成功' },
|
||
{ value: 'failed', label: '失败' },
|
||
];
|
||
|
||
function OperationLogs() {
|
||
const [logs, setLogs] = useState([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [pagination, setPagination] = useState({ current: 1, pageSize: 20, total: 0 });
|
||
const [filters, setFilters] = useState({
|
||
module: null,
|
||
operationType: null,
|
||
keyword: '',
|
||
dateRange: null,
|
||
result: null,
|
||
});
|
||
const [detailVisible, setDetailVisible] = useState(false);
|
||
const [currentLog, setCurrentLog] = useState(null);
|
||
const [detailLoading, setDetailLoading] = useState(false);
|
||
|
||
const fetchLogs = useCallback(async (page = 1, pageSize = 20, currentFilters = filters) => {
|
||
try {
|
||
setLoading(true);
|
||
const params = { page, pageSize };
|
||
|
||
if (currentFilters.module) {
|
||
params.module = currentFilters.module;
|
||
}
|
||
if (currentFilters.operationType) {
|
||
params.operationType = currentFilters.operationType;
|
||
}
|
||
if (currentFilters.keyword) {
|
||
params.keyword = currentFilters.keyword;
|
||
}
|
||
if (currentFilters.result) {
|
||
params.result = currentFilters.result;
|
||
}
|
||
if (currentFilters.dateRange && currentFilters.dateRange.length === 2) {
|
||
params.startDate = currentFilters.dateRange[0].format('YYYY-MM-DD');
|
||
params.endDate = currentFilters.dateRange[1].format('YYYY-MM-DD');
|
||
}
|
||
|
||
const response = await api.get('/operation-logs', { params });
|
||
if (response.success) {
|
||
setLogs(response.data.logs);
|
||
setPagination(prev => ({
|
||
...prev,
|
||
current: page,
|
||
pageSize,
|
||
total: response.data.total,
|
||
}));
|
||
}
|
||
} catch (error) {
|
||
message.error('获取操作日志失败');
|
||
console.error('获取操作日志失败:', error);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
fetchLogs(1, pagination.pageSize, filters);
|
||
}, []);
|
||
|
||
const handleTableChange = (newPagination, tableFilters) => {
|
||
fetchLogs(newPagination.current, newPagination.pageSize, filters);
|
||
};
|
||
|
||
const handleFilterChange = (key, value) => {
|
||
const newFilters = { ...filters, [key]: value };
|
||
setFilters(newFilters);
|
||
fetchLogs(1, pagination.pageSize, newFilters);
|
||
};
|
||
|
||
const handleClearFilters = () => {
|
||
const clearedFilters = {
|
||
module: null,
|
||
operationType: null,
|
||
keyword: '',
|
||
dateRange: null,
|
||
result: null,
|
||
};
|
||
setFilters(clearedFilters);
|
||
fetchLogs(1, pagination.pageSize, clearedFilters);
|
||
};
|
||
|
||
const handleViewDetail = async record => {
|
||
setDetailLoading(true);
|
||
setDetailVisible(true);
|
||
try {
|
||
const response = await api.get(`/operation-logs/${record.recordId}`);
|
||
if (response.success) {
|
||
setCurrentLog(response.data);
|
||
}
|
||
} catch (error) {
|
||
message.error('获取日志详情失败');
|
||
console.error('获取日志详情失败:', error);
|
||
} finally {
|
||
setDetailLoading(false);
|
||
}
|
||
};
|
||
|
||
const getModuleTag = module => {
|
||
const colors = {
|
||
device: 'blue',
|
||
user: 'green',
|
||
role: 'purple',
|
||
consumable: 'orange',
|
||
rack: 'cyan',
|
||
room: 'magenta',
|
||
ticket: 'red',
|
||
backup: 'gold',
|
||
};
|
||
const names = {
|
||
device: '设备',
|
||
user: '用户',
|
||
role: '角色',
|
||
consumable: '耗材',
|
||
rack: '机柜',
|
||
room: '机房',
|
||
ticket: '工单',
|
||
backup: '备份',
|
||
};
|
||
return <Tag color={colors[module] || 'default'}>{names[module] || module}</Tag>;
|
||
};
|
||
|
||
const getOperationTag = type => {
|
||
const colors = {
|
||
create: 'green',
|
||
update: 'blue',
|
||
delete: 'red',
|
||
batch_delete: 'red',
|
||
batch_update: 'orange',
|
||
status_change: 'cyan',
|
||
move: 'purple',
|
||
permission_change: 'gold',
|
||
import: 'lime',
|
||
export: 'lime',
|
||
};
|
||
const names = {
|
||
create: '创建',
|
||
update: '更新',
|
||
delete: '删除',
|
||
batch_delete: '批量删除',
|
||
batch_update: '批量更新',
|
||
status_change: '状态变更',
|
||
move: '移动',
|
||
permission_change: '权限变更',
|
||
import: '导入',
|
||
export: '导出',
|
||
};
|
||
return <Tag color={colors[type] || 'default'}>{names[type] || type}</Tag>;
|
||
};
|
||
|
||
const getResultTag = result => {
|
||
return result === 'success' ? <Tag color="success">成功</Tag> : <Tag color="error">失败</Tag>;
|
||
};
|
||
|
||
const columns = [
|
||
{
|
||
title: '时间',
|
||
dataIndex: 'createdAt',
|
||
key: 'createdAt',
|
||
width: 180,
|
||
sorter: (a, b) => new Date(a.createdAt) - new Date(b.createdAt),
|
||
render: date => dayjs(date).format('YYYY-MM-DD HH:mm:ss'),
|
||
},
|
||
{
|
||
title: '模块',
|
||
dataIndex: 'module',
|
||
key: 'module',
|
||
width: 100,
|
||
render: module => getModuleTag(module),
|
||
},
|
||
{
|
||
title: '操作类型',
|
||
dataIndex: 'operationType',
|
||
key: 'operationType',
|
||
width: 120,
|
||
render: type => getOperationTag(type),
|
||
},
|
||
{
|
||
title: '操作描述',
|
||
dataIndex: 'operationDescription',
|
||
key: 'operationDescription',
|
||
ellipsis: true,
|
||
},
|
||
{
|
||
title: '操作对象',
|
||
dataIndex: 'targetName',
|
||
key: 'targetName',
|
||
width: 150,
|
||
ellipsis: true,
|
||
render: (text, record) => text || record.targetId,
|
||
},
|
||
{
|
||
title: '操作人',
|
||
dataIndex: 'operatorName',
|
||
key: 'operatorName',
|
||
width: 120,
|
||
},
|
||
{
|
||
title: '结果',
|
||
dataIndex: 'result',
|
||
key: 'result',
|
||
width: 80,
|
||
render: result => getResultTag(result),
|
||
},
|
||
{
|
||
title: 'IP地址',
|
||
dataIndex: 'ipAddress',
|
||
key: 'ipAddress',
|
||
width: 140,
|
||
render: ip => ip || '-',
|
||
},
|
||
{
|
||
title: '操作',
|
||
key: 'action',
|
||
width: 80,
|
||
fixed: 'right',
|
||
render: (_, record) => (
|
||
<Button type="link" icon={<EyeOutlined />} onClick={() => handleViewDetail(record)}>
|
||
详情
|
||
</Button>
|
||
),
|
||
},
|
||
];
|
||
|
||
const hasFilters =
|
||
filters.module ||
|
||
filters.operationType ||
|
||
filters.keyword ||
|
||
filters.dateRange ||
|
||
filters.result;
|
||
|
||
return (
|
||
<div style={{ padding: '24px' }}>
|
||
<Card
|
||
title={
|
||
<Space>
|
||
<HistoryOutlined />
|
||
<span>操作日志审计</span>
|
||
</Space>
|
||
}
|
||
extra={
|
||
<Space>
|
||
<Text type="secondary">共 {pagination.total} 条记录</Text>
|
||
</Space>
|
||
}
|
||
>
|
||
<Card size="small" style={{ marginBottom: 16 }}>
|
||
<Row gutter={16} align="middle">
|
||
<Col flex="200px">
|
||
<Select
|
||
placeholder="选择模块"
|
||
allowClear
|
||
style={{ width: '100%', ...selectStyles }}
|
||
value={filters.module}
|
||
onChange={value => handleFilterChange('module', value)}
|
||
>
|
||
{MODULE_OPTIONS.map(opt => (
|
||
<Option key={opt.value} value={opt.value}>
|
||
{opt.label}
|
||
</Option>
|
||
))}
|
||
</Select>
|
||
</Col>
|
||
<Col flex="200px">
|
||
<Select
|
||
placeholder="选择操作类型"
|
||
allowClear
|
||
style={{ width: '100%', ...selectStyles }}
|
||
value={filters.operationType}
|
||
onChange={value => handleFilterChange('operationType', value)}
|
||
>
|
||
{OPERATION_TYPE_OPTIONS.map(opt => (
|
||
<Option key={opt.value} value={opt.value}>
|
||
{opt.label}
|
||
</Option>
|
||
))}
|
||
</Select>
|
||
</Col>
|
||
<Col flex="200px">
|
||
<Select
|
||
placeholder="选择结果"
|
||
allowClear
|
||
style={{ width: '100%', ...selectStyles }}
|
||
value={filters.result}
|
||
onChange={value => handleFilterChange('result', value)}
|
||
>
|
||
{RESULT_OPTIONS.map(opt => (
|
||
<Option key={opt.value} value={opt.value}>
|
||
{opt.label}
|
||
</Option>
|
||
))}
|
||
</Select>
|
||
</Col>
|
||
<Col flex="auto">
|
||
<Input
|
||
placeholder={inputPlaceholders.keyword || '搜索操作描述/对象/操作人'}
|
||
prefix={<SearchOutlined />}
|
||
style={{ ...filterInputStyles }}
|
||
value={filters.keyword}
|
||
onChange={e => handleFilterChange('keyword', e.target.value)}
|
||
allowClear
|
||
/>
|
||
</Col>
|
||
<Col flex="280px">
|
||
<RangePicker
|
||
style={{ width: '100%' }}
|
||
value={filters.dateRange}
|
||
onChange={dates => handleFilterChange('dateRange', dates)}
|
||
placeholder={['开始日期', '结束日期']}
|
||
/>
|
||
</Col>
|
||
<Col>
|
||
<Space>
|
||
<Button
|
||
icon={<FilterOutlined />}
|
||
onClick={() => fetchLogs(1, pagination.pageSize, filters)}
|
||
>
|
||
筛选
|
||
</Button>
|
||
{hasFilters && (
|
||
<Button icon={<ClearOutlined />} onClick={handleClearFilters}>
|
||
清除
|
||
</Button>
|
||
)}
|
||
</Space>
|
||
</Col>
|
||
</Row>
|
||
</Card>
|
||
|
||
<Table
|
||
columns={columns}
|
||
dataSource={logs}
|
||
loading={loading}
|
||
rowKey="recordId"
|
||
pagination={{
|
||
current: pagination.current,
|
||
pageSize: pagination.pageSize,
|
||
total: pagination.total,
|
||
showSizeChanger: true,
|
||
showQuickJumper: true,
|
||
showTotal: total => `共 ${total} 条`,
|
||
pageSizeOptions: ['10', '20', '50', '100'],
|
||
}}
|
||
onChange={handleTableChange}
|
||
scroll={{ x: 1200 }}
|
||
/>
|
||
</Card>
|
||
|
||
<Drawer
|
||
title="操作日志详情"
|
||
placement="right"
|
||
width={600}
|
||
onClose={() => {
|
||
setDetailVisible(false);
|
||
setCurrentLog(null);
|
||
}}
|
||
open={detailVisible}
|
||
extra={
|
||
currentLog && (
|
||
<Space>
|
||
{getModuleTag(currentLog.module)}
|
||
{getOperationTag(currentLog.operationType)}
|
||
{getResultTag(currentLog.result)}
|
||
</Space>
|
||
)
|
||
}
|
||
>
|
||
{detailLoading ? (
|
||
<div style={{ textAlign: 'center', padding: 40 }}>加载中...</div>
|
||
) : currentLog ? (
|
||
<Descriptions column={1} bordered size="small">
|
||
<Descriptions.Item label="日志ID">{currentLog.recordId}</Descriptions.Item>
|
||
<Descriptions.Item label="操作时间">
|
||
{dayjs(currentLog.createdAt).format('YYYY-MM-DD HH:mm:ss')}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="模块">{currentLog.module}</Descriptions.Item>
|
||
<Descriptions.Item label="操作类型">{currentLog.operationType}</Descriptions.Item>
|
||
<Descriptions.Item label="操作描述">
|
||
{currentLog.operationDescription}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="目标ID">{currentLog.targetId || '-'}</Descriptions.Item>
|
||
<Descriptions.Item label="目标名称">{currentLog.targetName || '-'}</Descriptions.Item>
|
||
<Descriptions.Item label="操作人ID">{currentLog.operatorId}</Descriptions.Item>
|
||
<Descriptions.Item label="操作人">{currentLog.operatorName}</Descriptions.Item>
|
||
{currentLog.operatorRole && (
|
||
<Descriptions.Item label="操作人角色">{currentLog.operatorRole}</Descriptions.Item>
|
||
)}
|
||
<Descriptions.Item label="IP地址">{currentLog.ipAddress || '-'}</Descriptions.Item>
|
||
<Descriptions.Item label="用户代理">{currentLog.userAgent || '-'}</Descriptions.Item>
|
||
<Descriptions.Item label="结果">
|
||
{currentLog.result === 'success' ? '成功' : '失败'}
|
||
</Descriptions.Item>
|
||
</Descriptions>
|
||
) : null}
|
||
|
||
{currentLog && (currentLog.beforeState || currentLog.afterState) && (
|
||
<>
|
||
<h4 style={{ marginTop: 16 }}>状态变更</h4>
|
||
{currentLog.beforeState && (
|
||
<>
|
||
<Text strong>变更前:</Text>
|
||
<pre
|
||
style={{
|
||
background: '#f5f5f5',
|
||
padding: 12,
|
||
borderRadius: 4,
|
||
overflow: 'auto',
|
||
maxHeight: 200,
|
||
}}
|
||
>
|
||
{JSON.stringify(currentLog.beforeState, null, 2)}
|
||
</pre>
|
||
</>
|
||
)}
|
||
{currentLog.afterState && (
|
||
<>
|
||
<Text strong>变更后:</Text>
|
||
<pre
|
||
style={{
|
||
background: '#f0f0f0',
|
||
padding: 12,
|
||
borderRadius: 4,
|
||
overflow: 'auto',
|
||
maxHeight: 200,
|
||
}}
|
||
>
|
||
{JSON.stringify(currentLog.afterState, null, 2)}
|
||
</pre>
|
||
</>
|
||
)}
|
||
</>
|
||
)}
|
||
|
||
{currentLog && currentLog.metadata && Object.keys(currentLog.metadata).length > 0 && (
|
||
<>
|
||
<h4 style={{ marginTop: 16 }}>扩展信息</h4>
|
||
<pre
|
||
style={{
|
||
background: '#f5f5f5',
|
||
padding: 12,
|
||
borderRadius: 4,
|
||
overflow: 'auto',
|
||
maxHeight: 200,
|
||
}}
|
||
>
|
||
{JSON.stringify(currentLog.metadata, null, 2)}
|
||
</pre>
|
||
</>
|
||
)}
|
||
</Drawer>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default OperationLogs;
|