refactor: 移除登录历史和操作日志功能及相关代码
移除后端登录历史和操作日志的模型、路由及前端相关页面和路由配置 清理创建和删除索引脚本中的相关代码 调整系统管理菜单结构
This commit is contained in:
@@ -45,23 +45,6 @@ const createIndexes = async () => {
|
||||
await queryInterface.addIndex('consumable_logs', ['consumableId', 'createdAt']);
|
||||
console.log(' ✓ consumable_logs 表索引创建完成');
|
||||
|
||||
// OperationLog 表索引
|
||||
console.log('创建 operation_logs 表索引...');
|
||||
await queryInterface.addIndex('operation_logs', ['userId']);
|
||||
await queryInterface.addIndex('operation_logs', ['action']);
|
||||
await queryInterface.addIndex('operation_logs', ['module']);
|
||||
await queryInterface.addIndex('operation_logs', ['createdAt']);
|
||||
await queryInterface.addIndex('operation_logs', ['userId', 'createdAt']);
|
||||
console.log(' ✓ operation_logs 表索引创建完成');
|
||||
|
||||
// LoginHistory 表索引
|
||||
console.log('创建 login_histories 表索引...');
|
||||
await queryInterface.addIndex('login_histories', ['userId']);
|
||||
await queryInterface.addIndex('login_histories', ['loginTime']);
|
||||
await queryInterface.addIndex('login_histories', ['loginType']);
|
||||
await queryInterface.addIndex('login_histories', ['userId', 'loginTime']);
|
||||
console.log(' ✓ login_histories 表索引创建完成');
|
||||
|
||||
// Rack 表索引
|
||||
console.log('创建 racks 表索引...');
|
||||
await queryInterface.addIndex('racks', ['roomId']);
|
||||
@@ -87,8 +70,7 @@ const checkIndexes = async () => {
|
||||
const queryInterface = sequelize.getQueryInterface();
|
||||
const tables = [
|
||||
'devices', 'users', 'consumables', 'consumable_records',
|
||||
'consumable_logs', 'operation_logs', 'login_histories',
|
||||
'racks', 'rooms'
|
||||
'consumable_logs', 'racks', 'rooms'
|
||||
];
|
||||
|
||||
console.log('\n检查现有索引...');
|
||||
@@ -117,8 +99,6 @@ const dropIndexes = async () => {
|
||||
{ table: 'consumables', indexes: ['category', 'status', 'category_status'] },
|
||||
{ table: 'consumable_records', indexes: ['consumableId', 'type', 'createdAt'] },
|
||||
{ table: 'consumable_logs', indexes: ['consumableId', 'operationType', 'createdAt', 'consumableId_createdAt'] },
|
||||
{ table: 'operation_logs', indexes: ['userId', 'action', 'module', 'createdAt', 'userId_createdAt'] },
|
||||
{ table: 'login_histories', indexes: ['userId', 'loginTime', 'loginType', 'userId_loginTime'] },
|
||||
{ table: 'racks', indexes: ['roomId', 'status', 'roomId_status'] },
|
||||
{ table: 'rooms', indexes: ['status', 'name'] }
|
||||
];
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const User = require('../models/User');
|
||||
const LoginHistory = require('../models/LoginHistory');
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'idc-management-secret-key-2024';
|
||||
const TOKEN_EXPIRY = process.env.TOKEN_EXPIRY || '24h';
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../db');
|
||||
|
||||
const LoginHistory = sequelize.define('LoginHistory', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
userId: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: '用户ID'
|
||||
},
|
||||
username: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: '用户名'
|
||||
},
|
||||
realName: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
comment: '真实姓名'
|
||||
},
|
||||
loginTime: {
|
||||
type: DataTypes.DATE,
|
||||
defaultValue: DataTypes.NOW,
|
||||
comment: '登录时间'
|
||||
},
|
||||
loginIp: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
comment: '登录IP'
|
||||
},
|
||||
userAgent: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '浏览器UA'
|
||||
},
|
||||
loginType: {
|
||||
type: DataTypes.ENUM('success', 'failed'),
|
||||
defaultValue: 'success',
|
||||
comment: '登录结果'
|
||||
},
|
||||
failReason: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
comment: '失败原因'
|
||||
}
|
||||
}, {
|
||||
tableName: 'login_histories',
|
||||
timestamps: true,
|
||||
createdAt: 'loginTime',
|
||||
updatedAt: false,
|
||||
indexes: [
|
||||
{ fields: ['userId'] },
|
||||
{ fields: ['loginTime'] },
|
||||
{ fields: ['loginType'] },
|
||||
{ fields: ['userId', 'loginTime'] }
|
||||
]
|
||||
});
|
||||
|
||||
module.exports = LoginHistory;
|
||||
@@ -1,89 +0,0 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const { sequelize } = require('../db');
|
||||
|
||||
const OperationLog = sequelize.define('OperationLog', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
userId: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: '操作人ID'
|
||||
},
|
||||
username: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: '操作人用户名'
|
||||
},
|
||||
realName: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
comment: '操作人真实姓名'
|
||||
},
|
||||
action: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
comment: '操作类型'
|
||||
},
|
||||
module: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
comment: '操作模块'
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '操作描述'
|
||||
},
|
||||
targetId: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
comment: '目标对象ID'
|
||||
},
|
||||
targetName: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
comment: '目标对象名称'
|
||||
},
|
||||
oldValue: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '旧值'
|
||||
},
|
||||
newValue: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '新值'
|
||||
},
|
||||
ip: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
comment: '操作IP'
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('success', 'failed'),
|
||||
defaultValue: 'success',
|
||||
comment: '操作状态'
|
||||
},
|
||||
errorMessage: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
comment: '错误信息'
|
||||
}
|
||||
}, {
|
||||
tableName: 'operation_logs',
|
||||
timestamps: true,
|
||||
createdAt: 'operateTime',
|
||||
updatedAt: false,
|
||||
indexes: [
|
||||
{ fields: ['userId'] },
|
||||
{ fields: ['action'] },
|
||||
{ fields: ['module'] },
|
||||
{ fields: ['operateTime'] },
|
||||
{ fields: ['userId', 'operateTime'] }
|
||||
]
|
||||
});
|
||||
|
||||
module.exports = OperationLog;
|
||||
@@ -1,120 +0,0 @@
|
||||
const express = require('express');
|
||||
const LoginHistory = require('../models/LoginHistory');
|
||||
const User = require('../models/User');
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { page = 1, pageSize = 10, userId, loginType, startDate, endDate } = req.query;
|
||||
const offset = (parseInt(page) - 1) * parseInt(pageSize);
|
||||
const limit = parseInt(pageSize);
|
||||
const where = {};
|
||||
|
||||
if (userId) where.userId = userId;
|
||||
if (loginType) where.loginType = loginType;
|
||||
if (startDate || endDate) {
|
||||
where.loginTime = {};
|
||||
if (startDate) where.loginTime[Op.gte] = new Date(startDate);
|
||||
if (endDate) where.loginTime[Op.lte] = new Date(endDate);
|
||||
}
|
||||
|
||||
const { count, rows } = await LoginHistory.findAndCountAll({
|
||||
where,
|
||||
limit,
|
||||
offset,
|
||||
order: [['loginTime', 'DESC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
histories: rows.map(h => ({
|
||||
id: h.id,
|
||||
userId: h.userId,
|
||||
username: h.username,
|
||||
realName: h.realName,
|
||||
loginTime: h.loginTime,
|
||||
loginIp: h.loginIp,
|
||||
userAgent: h.userAgent,
|
||||
loginType: h.loginType,
|
||||
failReason: h.failReason
|
||||
}))
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取登录历史错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取登录历史失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/user/:userId', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { page = 1, pageSize = 10 } = req.query;
|
||||
const offset = (parseInt(page) - 1) * parseInt(pageSize);
|
||||
const limit = parseInt(pageSize);
|
||||
|
||||
const { count, rows } = await LoginHistory.findAndCountAll({
|
||||
where: { userId: req.params.userId },
|
||||
limit,
|
||||
offset,
|
||||
order: [['loginTime', 'DESC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
histories: rows
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取用户登录历史错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取登录历史失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/:id', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
await LoginHistory.destroy({ where: { id: req.params.id } });
|
||||
res.json({ success: true, message: '删除成功' });
|
||||
} catch (error) {
|
||||
console.error('删除登录历史错误:', error);
|
||||
res.status(500).json({ success: false, message: '删除失败' });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { days } = req.body;
|
||||
const where = { loginType: 'success' };
|
||||
|
||||
if (days) {
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setDate(cutoffDate.getDate() - days);
|
||||
where.loginTime = { [Op.lt]: cutoffDate };
|
||||
}
|
||||
|
||||
await LoginHistory.destroy({ where });
|
||||
res.json({ success: true, message: '清理成功' });
|
||||
} catch (error) {
|
||||
console.error('清理登录历史错误:', error);
|
||||
res.status(500).json({ success: false, message: '清理失败' });
|
||||
}
|
||||
});
|
||||
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
module.exports = router;
|
||||
@@ -1,162 +0,0 @@
|
||||
const express = require('express');
|
||||
const OperationLog = require('../models/OperationLog');
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const ACTION_TYPES = {
|
||||
USER_CREATE: '创建用户',
|
||||
USER_UPDATE: '修改用户',
|
||||
USER_DELETE: '删除用户',
|
||||
USER_LOGIN: '用户登录',
|
||||
USER_LOGOUT: '用户登出',
|
||||
ROLE_CREATE: '创建角色',
|
||||
ROLE_UPDATE: '修改角色',
|
||||
ROLE_DELETE: '删除角色',
|
||||
ROLE_ASSIGN: '分配角色',
|
||||
DEVICE_CREATE: '创建设备',
|
||||
DEVICE_UPDATE: '修改设备',
|
||||
DEVICE_DELETE: '删除设备',
|
||||
CONSUMABLE_IN: '耗材入库',
|
||||
CONSUMABLE_OUT: '耗材出库',
|
||||
CONSUMABLE_RECORD: '耗材记录',
|
||||
SYSTEM_CONFIG: '系统配置'
|
||||
};
|
||||
|
||||
router.get('/', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { page = 1, pageSize = 20, userId, action, module, startDate, endDate } = req.query;
|
||||
const offset = (parseInt(page) - 1) * parseInt(pageSize);
|
||||
const limit = parseInt(pageSize);
|
||||
const where = {};
|
||||
|
||||
if (userId) where.userId = userId;
|
||||
if (action) where.action = action;
|
||||
if (module) where.module = module;
|
||||
if (startDate || endDate) {
|
||||
where.operateTime = {};
|
||||
if (startDate) where.operateTime[Op.gte] = new Date(startDate);
|
||||
if (endDate) where.operateTime[Op.lte] = new Date(endDate);
|
||||
}
|
||||
|
||||
const { count, rows } = await OperationLog.findAndCountAll({
|
||||
where,
|
||||
limit,
|
||||
offset,
|
||||
order: [['operateTime', 'DESC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize),
|
||||
logs: rows.map(l => ({
|
||||
id: l.id,
|
||||
userId: l.userId,
|
||||
username: l.username,
|
||||
realName: l.realName,
|
||||
action: l.action,
|
||||
module: l.module,
|
||||
description: l.description,
|
||||
targetId: l.targetId,
|
||||
targetName: l.targetName,
|
||||
oldValue: l.oldValue,
|
||||
newValue: l.newValue,
|
||||
ip: l.ip,
|
||||
status: l.status,
|
||||
errorMessage: l.errorMessage,
|
||||
operateTime: l.operateTime
|
||||
}))
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取操作日志错误:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取操作日志失败'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/actions', authMiddleware, (req, res) => {
|
||||
res.json({
|
||||
success: true,
|
||||
data: Object.entries(ACTION_TYPES).map(([key, value]) => ({
|
||||
key,
|
||||
value,
|
||||
label: value
|
||||
}))
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/modules', authMiddleware, (req, res) => {
|
||||
res.json({
|
||||
success: true,
|
||||
data: [
|
||||
{ key: 'user', value: 'user', label: '用户管理' },
|
||||
{ key: 'role', value: 'role', label: '角色管理' },
|
||||
{ key: 'device', value: 'device', label: '设备管理' },
|
||||
{ key: 'consumable', value: 'consumable', label: '耗材管理' },
|
||||
{ key: 'system', value: 'system', label: '系统设置' }
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
router.delete('/:id', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
await OperationLog.destroy({ where: { id: req.params.id } });
|
||||
res.json({ success: true, message: '删除成功' });
|
||||
} catch (error) {
|
||||
console.error('删除操作日志错误:', error);
|
||||
res.status(500).json({ success: false, message: '删除失败' });
|
||||
}
|
||||
});
|
||||
|
||||
router.delete('/', authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { days } = req.body;
|
||||
const where = {};
|
||||
|
||||
if (days) {
|
||||
const cutoffDate = new Date();
|
||||
cutoffDate.setDate(cutoffDate.getDate() - days);
|
||||
where.operateTime = { [Op.lt]: cutoffDate };
|
||||
}
|
||||
|
||||
await OperationLog.destroy({ where });
|
||||
res.json({ success: true, message: '清理成功' });
|
||||
} catch (error) {
|
||||
console.error('清理操作日志错误:', error);
|
||||
res.status(500).json({ success: false, message: '清理失败' });
|
||||
}
|
||||
});
|
||||
|
||||
const logOperation = async (req, action, module, description, targetId, targetName, oldValue, newValue, status = 'success', errorMessage = null) => {
|
||||
try {
|
||||
await OperationLog.create({
|
||||
userId: req.user?.userId,
|
||||
username: req.user?.username,
|
||||
realName: req.userModel?.realName,
|
||||
action,
|
||||
module,
|
||||
description,
|
||||
targetId,
|
||||
targetName,
|
||||
oldValue: oldValue ? JSON.stringify(oldValue) : null,
|
||||
newValue: newValue ? JSON.stringify(newValue) : null,
|
||||
ip: req.ip,
|
||||
status,
|
||||
errorMessage
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('记录操作日志错误:', error);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = router;
|
||||
module.exports.ACTION_TYPES = ACTION_TYPES;
|
||||
module.exports.logOperation = logOperation;
|
||||
|
||||
const { Op } = require('sequelize');
|
||||
@@ -58,8 +58,6 @@ const consumableCategoryRoutes = require('./routes/consumableCategories');
|
||||
const authRoutes = require('./routes/auth');
|
||||
const usersRoutes = require('./routes/users');
|
||||
const rolesRoutes = require('./routes/roles');
|
||||
const loginHistoryRoutes = require('./routes/loginHistory');
|
||||
const operationLogsRoutes = require('./routes/operationLogs');
|
||||
const ticketRoutes = require('./routes/tickets');
|
||||
const ticketCategoryRoutes = require('./routes/ticketCategories');
|
||||
const ticketFieldRoutes = require('./routes/ticketFields');
|
||||
@@ -77,8 +75,6 @@ app.use('/api/consumable-categories', consumableCategoryRoutes);
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/users', usersRoutes);
|
||||
app.use('/api/roles', rolesRoutes);
|
||||
app.use('/api/login-history', loginHistoryRoutes);
|
||||
app.use('/api/operation-logs', operationLogsRoutes);
|
||||
app.use('/api/tickets', ticketRoutes);
|
||||
app.use('/api/ticket-categories', ticketCategoryRoutes);
|
||||
app.use('/api/ticket-fields', ticketFieldRoutes);
|
||||
|
||||
+17
-31
@@ -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>} />
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user