refactor: 移除登录历史和操作日志功能及相关代码

移除后端登录历史和操作日志的模型、路由及前端相关页面和路由配置
清理创建和删除索引脚本中的相关代码
调整系统管理菜单结构
This commit is contained in:
zhang1106
2026-01-20 16:54:26 +08:00
parent 61dc0eefbe
commit 2af47ee162
10 changed files with 18 additions and 1020 deletions
+17 -31
View File
@@ -16,8 +16,6 @@ const ConsumableStatistics = lazy(() => import('./pages/ConsumableStatistics'));
const ConsumableLogs = lazy(() => import('./pages/ConsumableLogs'));
const CategoryManagement = lazy(() => import('./pages/CategoryManagement'));
const UserManagement = lazy(() => import('./pages/UserManagement'));
const LoginHistory = lazy(() => import('./pages/LoginHistory'));
const OperationLogs = lazy(() => import('./pages/OperationLogs'));
const Login = lazy(() => import('./pages/Login'));
const TicketManagement = lazy(() => import('./pages/TicketManagement'));
const TicketCategoryManagement = lazy(() => import('./pages/TicketCategoryManagement'));
@@ -223,33 +221,6 @@ const AppLayout = ({ children }) => {
},
],
},
{
key: 'system-management',
icon: <UserOutlined style={{ fontSize: '18px' }} />,
label: '系统管理',
children: [
{
key: 'users',
icon: <UserOutlined style={{ fontSize: '16px' }} />,
label: <Link to="/users">用户管理</Link>,
},
{
key: 'login-history',
icon: <HistoryOutlined style={{ fontSize: '16px' }} />,
label: <Link to="/login-history">登录历史</Link>,
},
{
key: 'operation-logs',
icon: <AuditOutlined style={{ fontSize: '16px' }} />,
label: <Link to="/operation-logs">操作日志</Link>,
},
{
key: 'system-settings',
icon: <SettingOutlined style={{ fontSize: '16px' }} />,
label: <Link to="/settings">系统设置</Link>,
},
],
},
{
key: 'ticket-management',
icon: <ToolOutlined style={{ fontSize: '18px' }} />,
@@ -277,6 +248,23 @@ const AppLayout = ({ children }) => {
},
],
},
{
key: 'system-management',
icon: <UserOutlined style={{ fontSize: '18px' }} />,
label: '系统管理',
children: [
{
key: 'users',
icon: <UserOutlined style={{ fontSize: '16px' }} />,
label: <Link to="/users">用户管理</Link>,
},
{
key: 'system-settings',
icon: <SettingOutlined style={{ fontSize: '16px' }} />,
label: <Link to="/settings">系统设置</Link>,
},
],
},
];
return (
@@ -480,8 +468,6 @@ function App() {
<Route path="/consumables-stats" element={<PrivateRoute><ConsumableStatistics /></PrivateRoute>} />
<Route path="/consumables-logs" element={<PrivateRoute><ConsumableLogs /></PrivateRoute>} />
<Route path="/users" element={<PrivateRoute><UserManagement /></PrivateRoute>} />
<Route path="/login-history" element={<PrivateRoute><LoginHistory /></PrivateRoute>} />
<Route path="/operation-logs" element={<PrivateRoute><OperationLogs /></PrivateRoute>} />
<Route path="/tickets" element={<PrivateRoute><TicketManagement /></PrivateRoute>} />
<Route path="/ticket-categories" element={<PrivateRoute><TicketCategoryManagement /></PrivateRoute>} />
<Route path="/ticket-statistics" element={<PrivateRoute><TicketStatistics /></PrivateRoute>} />
-194
View File
@@ -1,194 +0,0 @@
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { Card, Table, Tag, Space, Button, DatePicker, Select, message, Popconfirm, Typography, Descriptions } from 'antd';
import { ReloadOutlined, DeleteOutlined, EyeOutlined, SafetyCertificateOutlined } from '@ant-design/icons';
import { loginHistoryAPI } from '../api';
import dayjs from 'dayjs';
const { Title } = Typography;
const { RangePicker } = DatePicker;
const LoginHistory = () => {
const [histories, setHistories] = useState([]);
const [loading, setLoading] = useState(false);
const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 });
const [filters, setFilters] = useState({});
useEffect(() => {
fetchHistories();
}, [pagination.current, filters]);
const fetchHistories = useCallback(async () => {
setLoading(true);
try {
const params = {
page: pagination.current,
pageSize: pagination.pageSize,
...filters
};
const response = await loginHistoryAPI.list(params);
if (response.success) {
setHistories(response.data.histories);
setPagination(prev => ({ ...prev, total: response.data.total }));
}
} catch (error) {
message.error('获取登录历史失败');
} finally {
setLoading(false);
}
}, [pagination.current, pagination.pageSize, filters]);
const handleFilterChange = useCallback((key, value) => {
setFilters(prev => ({ ...prev, [key]: value }));
setPagination(prev => ({ ...prev, current: 1 }));
}, []);
const handleDateChange = useCallback((dates) => {
if (dates) {
setFilters(prev => ({
...prev,
startDate: dates[0].toISOString(),
endDate: dates[1].toISOString()
}));
} else {
setFilters(prev => ({ ...prev, startDate: undefined, endDate: undefined }));
}
setPagination(prev => ({ ...prev, current: 1 }));
}, []);
const handleClear = useCallback(async () => {
try {
const response = await loginHistoryAPI.clear({ days: 30 });
if (response.success) {
message.success('已清理30天前的登录记录');
fetchHistories();
}
} catch (error) {
message.error('清理失败');
}
}, [fetchHistories]);
const tableColumns = useMemo(() => [
{
title: '用户名',
dataIndex: 'username',
key: 'username',
width: 120
},
{
title: '真实姓名',
dataIndex: 'realName',
key: 'realName',
width: 100,
render: (name) => name || '-'
},
{
title: '登录时间',
dataIndex: 'loginTime',
key: 'loginTime',
width: 180,
render: (time) => time ? dayjs(time).format('YYYY-MM-DD HH:mm:ss') : '-'
},
{
title: 'IP地址',
dataIndex: 'loginIp',
key: 'loginIp',
width: 140,
render: (ip) => ip || '-'
},
{
title: '登录状态',
dataIndex: 'loginType',
key: 'loginType',
width: 100,
render: (type) => (
<Tag color={type === 'success' ? 'green' : 'red'}>
{type === 'success' ? '成功' : '失败'}
</Tag>
)
},
{
title: '失败原因',
dataIndex: 'failReason',
key: 'failReason',
width: 150,
render: (reason) => reason || '-'
},
{
title: '浏览器',
dataIndex: 'userAgent',
key: 'userAgent',
ellipsis: true,
render: (ua) => {
if (!ua) return '-';
let browser = 'Unknown';
if (ua.includes('Chrome')) browser = 'Chrome';
else if (ua.includes('Firefox')) browser = 'Firefox';
else if (ua.includes('Safari')) browser = 'Safari';
else if (ua.includes('Edge')) browser = 'Edge';
return browser;
}
}
], []);
const pageHeaderStyle = {
marginBottom: '24px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
};
const titleStyle = {
fontSize: '20px',
fontWeight: '600',
margin: 0
};
return (
<div>
<div style={pageHeaderStyle}>
<h1 style={titleStyle}>登录历史</h1>
<Space>
<Button icon={<ReloadOutlined />} onClick={fetchHistories}>刷新</Button>
<Popconfirm title="确定清理30天前的登录记录?" onConfirm={handleClear}>
<Button danger>清理旧记录</Button>
</Popconfirm>
</Space>
</div>
<Card style={{ marginBottom: '16px' }}>
<Space wrap>
<Select
placeholder="登录状态"
allowClear
style={{ width: 120 }}
onChange={(value) => handleFilterChange('loginType', value)}
>
<Select.Option value="success">成功</Select.Option>
<Select.Option value="failed">失败</Select.Option>
</Select>
<RangePicker onChange={handleDateChange} showTime />
</Space>
</Card>
<Card>
<Table
columns={tableColumns}
dataSource={histories}
rowKey="id"
loading={loading}
pagination={{
...pagination,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total} 条记录`
}}
onChange={(newPagination) => {
setPagination(prev => ({ ...prev, ...newPagination }));
}}
/>
</Card>
</div>
);
};
export default LoginHistory;
-335
View File
@@ -1,335 +0,0 @@
import React, { useState, useEffect } from 'react';
import { Card, Table, Tag, Space, Button, DatePicker, Select, Input, message, Popconfirm, Typography, Drawer, Descriptions, Timeline } from 'antd';
import { ReloadOutlined, DeleteOutlined, EyeOutlined, FileTextOutlined } from '@ant-design/icons';
import { operationLogAPI } from '../api';
import dayjs from 'dayjs';
const { Title } = Typography;
const { RangePicker } = DatePicker;
const OperationLogs = () => {
const [logs, setLogs] = useState([]);
const [loading, setLoading] = useState(false);
const [pagination, setPagination] = useState({ current: 1, pageSize: 20, total: 0 });
const [filters, setFilters] = useState({});
const [actions, setActions] = useState([]);
const [modules, setModules] = useState([]);
const [detailVisible, setDetailVisible] = useState(false);
const [selectedLog, setSelectedLog] = useState(null);
useEffect(() => {
fetchLogs();
fetchOptions();
}, [pagination.current, filters]);
const fetchLogs = async () => {
setLoading(true);
try {
const params = {
page: pagination.current,
pageSize: pagination.pageSize,
...filters
};
const response = await operationLogAPI.list(params);
if (response.success) {
setLogs(response.data.logs);
setPagination(prev => ({ ...prev, total: response.data.total }));
}
} catch (error) {
message.error('获取操作日志失败');
} finally {
setLoading(false);
}
};
const fetchOptions = async () => {
try {
const [actionsRes, modulesRes] = await Promise.all([
operationLogAPI.getActions(),
operationLogAPI.getModules()
]);
if (actionsRes.success) setActions(actionsRes.data);
if (modulesRes.success) setModules(modulesRes.data);
} catch (error) {
console.error('获取选项失败:', error);
}
};
const handleFilterChange = (key, value) => {
setFilters(prev => ({ ...prev, [key]: value }));
setPagination(prev => ({ ...prev, current: 1 }));
};
const handleDateChange = (dates) => {
if (dates) {
setFilters(prev => ({
...prev,
startDate: dates[0].toISOString(),
endDate: dates[1].toISOString()
}));
} else {
setFilters(prev => ({ ...prev, startDate: undefined, endDate: undefined }));
}
setPagination(prev => ({ ...prev, current: 1 }));
};
const handleClear = async () => {
try {
const response = await operationLogAPI.clear({ days: 30 });
if (response.success) {
message.success('已清理30天前的日志');
fetchLogs();
}
} catch (error) {
message.error('清理失败');
}
};
const showDetail = (log) => {
setSelectedLog(log);
setDetailVisible(true);
};
const getActionColor = (action) => {
if (action.includes('删除')) return 'red';
if (action.includes('创建')) return 'green';
if (action.includes('修改')) return 'blue';
if (action.includes('登录')) return 'purple';
return 'default';
};
const getModuleColor = (module) => {
const colors = {
user: 'blue',
role: 'green',
device: 'orange',
consumable: 'purple',
system: 'cyan'
};
return colors[module] || 'default';
};
const columns = [
{
title: '操作时间',
dataIndex: 'operateTime',
key: 'operateTime',
width: 180,
sorter: (a, b) => new Date(b.operateTime) - new Date(a.operateTime),
render: (time) => time ? dayjs(time).format('YYYY-MM-DD HH:mm:ss') : '-'
},
{
title: '操作人',
key: 'operator',
width: 150,
render: (_, record) => (
<div>
<div style={{ fontWeight: 500 }}>{record.realName || record.username}</div>
<div style={{ fontSize: '12px', color: '#999' }}>@{record.username}</div>
</div>
)
},
{
title: '操作类型',
dataIndex: 'action',
key: 'action',
width: 120,
render: (action) => (
<Tag color={getActionColor(action)}>{action || '-'}</Tag>
)
},
{
title: '模块',
dataIndex: 'module',
key: 'module',
width: 100,
render: (module) => (
<Tag color={getModuleColor(module)}>
{module === 'user' ? '用户' :
module === 'role' ? '角色' :
module === 'device' ? '设备' :
module === 'consumable' ? '耗材' :
module === 'system' ? '系统' : module}
</Tag>
)
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
ellipsis: true
},
{
title: '目标',
key: 'target',
width: 120,
render: (_, record) => record.targetName || record.targetId || '-'
},
{
title: 'IP',
dataIndex: 'ip',
key: 'ip',
width: 130,
render: (ip) => ip || '-'
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 80,
render: (status) => (
<Tag color={status === 'success' ? 'green' : 'red'}>
{status === 'success' ? '成功' : '失败'}
</Tag>
)
},
{
title: '操作',
key: 'action',
width: 80,
render: (_, record) => (
<Button
type="text"
icon={<EyeOutlined />}
onClick={() => showDetail(record)}
/>
)
}
];
const pageHeaderStyle = {
marginBottom: '24px',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
};
const titleStyle = {
fontSize: '20px',
fontWeight: '600',
margin: 0
};
return (
<div>
<div style={pageHeaderStyle}>
<h1 style={titleStyle}>操作日志</h1>
<Space>
<Button icon={<ReloadOutlined />} onClick={fetchLogs}>刷新</Button>
<Popconfirm title="确定清理30天前的日志?" onConfirm={handleClear}>
<Button danger>清理旧日志</Button>
</Popconfirm>
</Space>
</div>
<Card style={{ marginBottom: '16px' }}>
<Space wrap>
<Input.Search
placeholder="搜索操作人"
style={{ width: 150 }}
onSearch={(value) => handleFilterChange('username', value)}
allowClear
/>
<Select
placeholder="操作类型"
allowClear
style={{ width: 140 }}
onChange={(value) => handleFilterChange('action', value)}
options={actions}
fieldNames={{ label: 'label', value: 'value' }}
/>
<Select
placeholder="模块"
allowClear
style={{ width: 120 }}
onChange={(value) => handleFilterChange('module', value)}
options={modules}
fieldNames={{ label: 'label', value: 'value' }}
/>
<RangePicker onChange={handleDateChange} showTime />
</Space>
</Card>
<Card>
<Table
columns={columns}
dataSource={logs}
rowKey="id"
loading={loading}
pagination={{
...pagination,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${total} 条记录`
}}
onChange={(newPagination) => {
setPagination(prev => ({ ...prev, ...newPagination }));
}}
/>
</Card>
<Drawer
title="日志详情"
placement="right"
width={500}
open={detailVisible}
onClose={() => setDetailVisible(false)}
>
{selectedLog && (
<Descriptions column={1} bordered size="small">
<Descriptions.Item label="操作时间">
{selectedLog.operateTime ? dayjs(selectedLog.operateTime).format('YYYY-MM-DD HH:mm:ss') : '-'}
</Descriptions.Item>
<Descriptions.Item label="操作人">
{selectedLog.realName || selectedLog.username} (@{selectedLog.username})
</Descriptions.Item>
<Descriptions.Item label="操作类型">
<Tag color={getActionColor(selectedLog.action)}>{selectedLog.action}</Tag>
</Descriptions.Item>
<Descriptions.Item label="模块">
<Tag color={getModuleColor(selectedLog.module)}>{selectedLog.module}</Tag>
</Descriptions.Item>
<Descriptions.Item label="描述">{selectedLog.description || '-'}</Descriptions.Item>
<Descriptions.Item label="目标对象">
{selectedLog.targetName || selectedLog.targetId || '-'}
</Descriptions.Item>
<Descriptions.Item label="IP地址">{selectedLog.ip || '-'}</Descriptions.Item>
<Descriptions.Item label="状态">
<Tag color={selectedLog.status === 'success' ? 'green' : 'red'}>
{selectedLog.status === 'success' ? '成功' : '失败'}
</Tag>
</Descriptions.Item>
{selectedLog.errorMessage && (
<Descriptions.Item label="错误信息">
<span style={{ color: 'red' }}>{selectedLog.errorMessage}</span>
</Descriptions.Item>
)}
</Descriptions>
)}
{(selectedLog?.oldValue || selectedLog?.newValue) && (
<div style={{ marginTop: '24px' }}>
<Title level={5}>变更内容</Title>
<Descriptions column={1} bordered size="small">
{selectedLog.oldValue && (
<Descriptions.Item label="旧值">
<pre style={{ margin: 0, fontSize: '12px', whiteSpace: 'pre-wrap' }}>
{JSON.stringify(JSON.parse(selectedLog.oldValue), null, 2)}
</pre>
</Descriptions.Item>
)}
{selectedLog.newValue && (
<Descriptions.Item label="新值">
<pre style={{ margin: 0, fontSize: '12px', whiteSpace: 'pre-wrap' }}>
{JSON.stringify(JSON.parse(selectedLog.newValue), null, 2)}
</pre>
</Descriptions.Item>
)}
</Descriptions>
</div>
)}
</Drawer>
</div>
);
};
export default OperationLogs;