feat: 实现用户认证与权限管理系统
- 添加用户、角色、权限等数据模型 - 实现JWT认证中间件和密码加密 - 添加用户注册、登录、个人信息管理接口 - 实现前端认证上下文和受保护路由 - 添加登录历史记录和操作日志功能 - 提供管理员初始化脚本和修复工具 - 实现完整的登录页面和用户管理界面
This commit is contained in:
@@ -0,0 +1,33 @@
|
|||||||
|
const { sequelize } = require('./db');
|
||||||
|
const User = require('./models/User');
|
||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
await sequelize.authenticate();
|
||||||
|
console.log('数据库连接成功!\n');
|
||||||
|
|
||||||
|
const users = await User.findAll({
|
||||||
|
attributes: ['userId', 'username', 'password', 'email', 'status', 'createdAt']
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('用户列表:');
|
||||||
|
console.log('============');
|
||||||
|
|
||||||
|
for (const user of users) {
|
||||||
|
console.log('用户名:', user.username);
|
||||||
|
console.log('密码(加密后):', user.password);
|
||||||
|
console.log('状态:', user.status);
|
||||||
|
console.log('创建时间:', user.createdAt);
|
||||||
|
console.log('------------');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\n' + users.length + ' 个用户');
|
||||||
|
|
||||||
|
await sequelize.close();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('错误:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
const { sequelize } = require('./db');
|
||||||
|
const User = require('./models/User');
|
||||||
|
const Role = require('./models/Role');
|
||||||
|
const UserRole = require('./models/UserRole');
|
||||||
|
|
||||||
|
async function fixAdminUser() {
|
||||||
|
try {
|
||||||
|
await sequelize.authenticate();
|
||||||
|
console.log('数据库连接成功!\n');
|
||||||
|
|
||||||
|
const adminUser = await User.findOne({
|
||||||
|
where: { username: 'admin' }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!adminUser) {
|
||||||
|
console.log('未找到 admin 用户');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('当前 admin 用户信息:');
|
||||||
|
console.log('用户名:', adminUser.username);
|
||||||
|
console.log('邮箱:', adminUser.email);
|
||||||
|
console.log('状态:', adminUser.status);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
const userRoles = await UserRole.findAll({
|
||||||
|
where: { UserId: adminUser.userId },
|
||||||
|
include: [{ model: Role }]
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('当前用户角色:');
|
||||||
|
if (userRoles.length === 0) {
|
||||||
|
console.log(' - 无角色分配');
|
||||||
|
} else {
|
||||||
|
userRoles.forEach(ur => {
|
||||||
|
console.log(` - ${ur.Role.roleName} (${ur.Role.roleCode})`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
let adminRole = await Role.findOne({
|
||||||
|
where: { roleCode: 'admin' }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!adminRole) {
|
||||||
|
console.log('正在创建管理员角色...');
|
||||||
|
adminRole = await Role.create({
|
||||||
|
roleId: 'role_admin',
|
||||||
|
roleName: '管理员',
|
||||||
|
roleCode: 'admin',
|
||||||
|
description: '系统管理员,拥有所有权限',
|
||||||
|
status: 'active',
|
||||||
|
permissions: []
|
||||||
|
});
|
||||||
|
console.log('管理员角色创建成功!');
|
||||||
|
} else {
|
||||||
|
console.log('管理员角色已存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingUserRole = await UserRole.findOne({
|
||||||
|
where: {
|
||||||
|
UserId: adminUser.userId,
|
||||||
|
RoleId: adminRole.roleId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existingUserRole) {
|
||||||
|
console.log('正在为 admin 用户分配管理员角色...');
|
||||||
|
await UserRole.create({
|
||||||
|
UserId: adminUser.userId,
|
||||||
|
RoleId: adminRole.roleId
|
||||||
|
});
|
||||||
|
console.log('角色分配成功!');
|
||||||
|
} else {
|
||||||
|
console.log('管理员角色已分配给该用户');
|
||||||
|
}
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
const newPassword = 'admin123';
|
||||||
|
const salt = await bcrypt.genSalt(10);
|
||||||
|
const hashedPassword = await bcrypt.hash(newPassword, salt);
|
||||||
|
|
||||||
|
await adminUser.update({
|
||||||
|
password: hashedPassword
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('密码已重置为: admin123');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
const updatedUser = await User.findOne({
|
||||||
|
where: { username: 'admin' },
|
||||||
|
include: [{ model: Role }]
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('修复后的 admin 用户信息:');
|
||||||
|
console.log('用户名:', updatedUser.username);
|
||||||
|
console.log('邮箱:', updatedUser.email);
|
||||||
|
console.log('状态:', updatedUser.status);
|
||||||
|
console.log('角色:',
|
||||||
|
updatedUser.Roles && updatedUser.Roles.length > 0
|
||||||
|
? updatedUser.Roles.map(r => r.roleName).join(', ')
|
||||||
|
: '无角色'
|
||||||
|
);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
const testResult = bcrypt.compareSync(newPassword, updatedUser.password);
|
||||||
|
console.log('密码验证测试:');
|
||||||
|
console.log(` 输入密码: "${newPassword}"`);
|
||||||
|
console.log(` 验证结果: ${testResult ? '✓ 成功' : '✗ 失败'}`);
|
||||||
|
|
||||||
|
console.log('\n========================================');
|
||||||
|
console.log('修复完成!现在可以使用以下信息登录:');
|
||||||
|
console.log(' 用户名: admin');
|
||||||
|
console.log(' 密码: admin123');
|
||||||
|
console.log('========================================');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('修复过程中发生错误:', error.message);
|
||||||
|
} finally {
|
||||||
|
await sequelize.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fixAdminUser();
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
const { sequelize } = require('./db');
|
||||||
|
const Role = require('./models/Role');
|
||||||
|
const User = require('./models/User');
|
||||||
|
const UserRole = require('./models/UserRole');
|
||||||
|
|
||||||
|
async function fixForeignKeys() {
|
||||||
|
try {
|
||||||
|
console.log('开始修复外键约束...');
|
||||||
|
|
||||||
|
await sequelize.query('PRAGMA foreign_keys = OFF');
|
||||||
|
|
||||||
|
await sequelize.query('DROP TABLE IF EXISTS user_roles');
|
||||||
|
await sequelize.query('DROP TABLE IF EXISTS users');
|
||||||
|
await sequelize.query('DROP TABLE IF EXISTS roles');
|
||||||
|
|
||||||
|
console.log('已删除现有表,正在重新创建...');
|
||||||
|
|
||||||
|
await sequelize.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS roles (
|
||||||
|
roleId TEXT PRIMARY KEY,
|
||||||
|
roleName TEXT NOT NULL,
|
||||||
|
roleCode TEXT NOT NULL UNIQUE,
|
||||||
|
description TEXT,
|
||||||
|
permissions TEXT DEFAULT '[]',
|
||||||
|
sort INTEGER DEFAULT 0,
|
||||||
|
status TEXT DEFAULT 'active',
|
||||||
|
createdAt DATETIME,
|
||||||
|
updatedAt DATETIME
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
await sequelize.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
userId TEXT PRIMARY KEY,
|
||||||
|
username TEXT NOT NULL UNIQUE,
|
||||||
|
password TEXT NOT NULL,
|
||||||
|
email TEXT,
|
||||||
|
phone TEXT,
|
||||||
|
realName TEXT,
|
||||||
|
avatar TEXT,
|
||||||
|
status TEXT DEFAULT 'active',
|
||||||
|
lastLoginTime DATETIME,
|
||||||
|
lastLoginIp TEXT,
|
||||||
|
loginCount INTEGER DEFAULT 0,
|
||||||
|
remark TEXT,
|
||||||
|
createdAt DATETIME,
|
||||||
|
updatedAt DATETIME
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
await sequelize.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS user_roles (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
UserId TEXT NOT NULL,
|
||||||
|
RoleId TEXT NOT NULL,
|
||||||
|
createdAt DATETIME,
|
||||||
|
updatedAt DATETIME,
|
||||||
|
FOREIGN KEY (UserId) REFERENCES users(userId) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (RoleId) REFERENCES roles(roleId) ON DELETE CASCADE,
|
||||||
|
UNIQUE(UserId, RoleId)
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
await sequelize.query('PRAGMA foreign_keys = ON');
|
||||||
|
|
||||||
|
console.log('外键约束修复完成!');
|
||||||
|
console.log('正在重新初始化角色数据...');
|
||||||
|
|
||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
const SALT_ROUNDS = 10;
|
||||||
|
|
||||||
|
await Role.bulkCreate([
|
||||||
|
{
|
||||||
|
roleId: 'role_admin',
|
||||||
|
roleName: '管理员',
|
||||||
|
roleCode: 'admin',
|
||||||
|
description: '系统管理员,拥有所有权限',
|
||||||
|
permissions: '[]',
|
||||||
|
sort: 1,
|
||||||
|
status: 'active'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
roleId: 'role_operator',
|
||||||
|
roleName: '操作员',
|
||||||
|
roleCode: 'operator',
|
||||||
|
description: '设备操作员,可进行设备操作',
|
||||||
|
permissions: '[]',
|
||||||
|
sort: 2,
|
||||||
|
status: 'active'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
roleId: 'role_viewer',
|
||||||
|
roleName: '访客',
|
||||||
|
roleCode: 'viewer',
|
||||||
|
description: '只读权限,只能查看数据',
|
||||||
|
permissions: '[]',
|
||||||
|
sort: 3,
|
||||||
|
status: 'active'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log('角色数据初始化完成!');
|
||||||
|
|
||||||
|
const hashedPassword = await bcrypt.hash('admin123', SALT_ROUNDS);
|
||||||
|
|
||||||
|
const adminUser = await User.create({
|
||||||
|
userId: 'user_admin',
|
||||||
|
username: 'admin',
|
||||||
|
password: hashedPassword,
|
||||||
|
email: 'admin@example.com',
|
||||||
|
realName: '系统管理员',
|
||||||
|
status: 'active'
|
||||||
|
});
|
||||||
|
|
||||||
|
await UserRole.create({
|
||||||
|
UserId: adminUser.userId,
|
||||||
|
RoleId: 'role_admin'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('管理员账户创建完成!');
|
||||||
|
console.log('用户名: admin');
|
||||||
|
console.log('密码: admin123');
|
||||||
|
|
||||||
|
console.log('\n修复完成!请重启后端服务。');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('修复外键约束失败:', error);
|
||||||
|
} finally {
|
||||||
|
await sequelize.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fixForeignKeys();
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
const getBrowserInfo = (userAgent) => {
|
||||||
|
let device = 'Desktop';
|
||||||
|
let browser = 'Unknown';
|
||||||
|
let os = 'Unknown';
|
||||||
|
|
||||||
|
if (/Mobile|Android|iPhone|iPad|iPod/i.test(userAgent)) {
|
||||||
|
device = 'Mobile';
|
||||||
|
if (/iPad/i.test(userAgent)) device = 'Tablet';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/Firefox/i.test(userAgent)) {
|
||||||
|
browser = 'Firefox';
|
||||||
|
} else if (/Chrome/i.test(userAgent)) {
|
||||||
|
browser = 'Chrome';
|
||||||
|
} else if (/Safari/i.test(userAgent)) {
|
||||||
|
browser = 'Safari';
|
||||||
|
} else if (/Edge/i.test(userAgent)) {
|
||||||
|
browser = 'Edge';
|
||||||
|
} else if (/MSIE|Trident/i.test(userAgent)) {
|
||||||
|
browser = 'IE';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/Windows/i.test(userAgent)) {
|
||||||
|
os = 'Windows';
|
||||||
|
} else if (/Mac OS/i.test(userAgent)) {
|
||||||
|
os = 'macOS';
|
||||||
|
} else if (/Linux/i.test(userAgent)) {
|
||||||
|
os = 'Linux';
|
||||||
|
} else if (/Android/i.test(userAgent)) {
|
||||||
|
os = 'Android';
|
||||||
|
} else if (/iOS|iPhone|iPad|iPod/i.test(userAgent)) {
|
||||||
|
os = 'iOS';
|
||||||
|
}
|
||||||
|
|
||||||
|
return { device, browser, os };
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateToken = (user) => {
|
||||||
|
return jwt.sign(
|
||||||
|
{
|
||||||
|
userId: user.userId,
|
||||||
|
username: user.username,
|
||||||
|
roleId: user.roleId
|
||||||
|
},
|
||||||
|
JWT_SECRET,
|
||||||
|
{ expiresIn: TOKEN_EXPIRY }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const verifyToken = (token) => {
|
||||||
|
try {
|
||||||
|
return jwt.verify(token, JWT_SECRET);
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const authMiddleware = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
|
||||||
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: '未提供认证令牌'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = authHeader.substring(7);
|
||||||
|
const decoded = verifyToken(token);
|
||||||
|
|
||||||
|
if (!decoded) {
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: '令牌无效或已过期'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await User.findByPk(decoded.userId);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: '用户不存在'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.status === 'locked') {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: '账户已被锁定'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.status === 'inactive') {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: '账户已禁用'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
req.user = decoded;
|
||||||
|
req.userModel = user;
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('认证中间件错误:', error);
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '认证失败'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const optionalAuth = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
|
||||||
|
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||||||
|
const token = authHeader.substring(7);
|
||||||
|
const decoded = verifyToken(token);
|
||||||
|
|
||||||
|
if (decoded) {
|
||||||
|
const user = await User.findByPk(decoded.userId);
|
||||||
|
if (user && user.status === 'active') {
|
||||||
|
req.user = decoded;
|
||||||
|
req.userModel = user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
generateToken,
|
||||||
|
verifyToken,
|
||||||
|
authMiddleware,
|
||||||
|
optionalAuth,
|
||||||
|
JWT_SECRET,
|
||||||
|
TOKEN_EXPIRY
|
||||||
|
};
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = LoginHistory;
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = OperationLog;
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
const { DataTypes } = require('sequelize');
|
||||||
|
const { sequelize } = require('../db');
|
||||||
|
|
||||||
|
const Permission = sequelize.define('Permission', {
|
||||||
|
permissionId: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
primaryKey: true,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
permissionName: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
permissionCode: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
unique: true
|
||||||
|
},
|
||||||
|
parentId: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: DataTypes.ENUM('menu', 'button'),
|
||||||
|
defaultValue: 'button'
|
||||||
|
},
|
||||||
|
path: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
sort: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
defaultValue: 0
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: DataTypes.ENUM('active', 'inactive'),
|
||||||
|
defaultValue: 'active'
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
tableName: 'permissions',
|
||||||
|
timestamps: true
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = Permission;
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
const { DataTypes } = require('sequelize');
|
||||||
|
const { sequelize } = require('../db');
|
||||||
|
|
||||||
|
const Role = sequelize.define('Role', {
|
||||||
|
roleId: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
primaryKey: true,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
roleName: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
roleCode: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
unique: true
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: DataTypes.ENUM('active', 'inactive'),
|
||||||
|
defaultValue: 'active'
|
||||||
|
},
|
||||||
|
permissions: {
|
||||||
|
type: DataTypes.JSON,
|
||||||
|
defaultValue: []
|
||||||
|
},
|
||||||
|
sort: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
defaultValue: 0
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
tableName: 'roles',
|
||||||
|
timestamps: true
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = Role;
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
const { DataTypes } = require('sequelize');
|
||||||
|
const { sequelize } = require('../db');
|
||||||
|
|
||||||
|
const User = sequelize.define('User', {
|
||||||
|
userId: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
primaryKey: true,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
unique: true
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true,
|
||||||
|
validate: {
|
||||||
|
isEmail: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
phone: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
realName: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
avatar: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: DataTypes.ENUM('active', 'inactive', 'locked'),
|
||||||
|
defaultValue: 'active'
|
||||||
|
},
|
||||||
|
lastLoginTime: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
lastLoginIp: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
loginCount: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
defaultValue: 0
|
||||||
|
},
|
||||||
|
remark: {
|
||||||
|
type: DataTypes.TEXT,
|
||||||
|
allowNull: true
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
tableName: 'users',
|
||||||
|
timestamps: true
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = User;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
const { DataTypes } = require('sequelize');
|
||||||
|
const { sequelize } = require('../db');
|
||||||
|
const User = require('./User');
|
||||||
|
const Role = require('./Role');
|
||||||
|
|
||||||
|
const UserRole = sequelize.define('UserRole', {
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
tableName: 'user_roles',
|
||||||
|
timestamps: true
|
||||||
|
});
|
||||||
|
|
||||||
|
UserRole.belongsTo(User, { foreignKey: 'UserId', onDelete: 'CASCADE' });
|
||||||
|
UserRole.belongsTo(Role, { foreignKey: 'RoleId', onDelete: 'CASCADE' });
|
||||||
|
User.hasMany(UserRole, { foreignKey: 'UserId', onDelete: 'CASCADE' });
|
||||||
|
Role.hasMany(UserRole, { foreignKey: 'RoleId', onDelete: 'CASCADE' });
|
||||||
|
|
||||||
|
module.exports = UserRole;
|
||||||
Generated
+117
@@ -8,6 +8,7 @@
|
|||||||
"name": "idc-backend",
|
"name": "idc-backend",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"csv-parser": "^3.2.0",
|
"csv-parser": "^3.2.0",
|
||||||
"csv-writer": "^1.6.0",
|
"csv-writer": "^1.6.0",
|
||||||
@@ -16,6 +17,7 @@
|
|||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-fileupload": "^1.5.2",
|
"express-fileupload": "^1.5.2",
|
||||||
"iconv-lite": "^0.6.3",
|
"iconv-lite": "^0.6.3",
|
||||||
|
"jsonwebtoken": "^9.0.3",
|
||||||
"mysql2": "^3.16.0",
|
"mysql2": "^3.16.0",
|
||||||
"sequelize": "^6.32.1",
|
"sequelize": "^6.32.1",
|
||||||
"sqlite3": "^5.1.6",
|
"sqlite3": "^5.1.6",
|
||||||
@@ -281,6 +283,15 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/bcryptjs": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"bin": {
|
||||||
|
"bcrypt": "bin/bcrypt"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/binary-extensions": {
|
"node_modules/binary-extensions": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||||
@@ -398,6 +409,12 @@
|
|||||||
"ieee754": "^1.1.13"
|
"ieee754": "^1.1.13"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer-equal-constant-time": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/busboy": {
|
"node_modules/busboy": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||||
@@ -761,6 +778,15 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ecdsa-sig-formatter": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ee-first": {
|
"node_modules/ee-first": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
@@ -1522,12 +1548,103 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/jsonwebtoken": {
|
||||||
|
"version": "9.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
|
||||||
|
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jws": "^4.0.1",
|
||||||
|
"lodash.includes": "^4.3.0",
|
||||||
|
"lodash.isboolean": "^3.0.3",
|
||||||
|
"lodash.isinteger": "^4.0.4",
|
||||||
|
"lodash.isnumber": "^3.0.3",
|
||||||
|
"lodash.isplainobject": "^4.0.6",
|
||||||
|
"lodash.isstring": "^4.0.1",
|
||||||
|
"lodash.once": "^4.0.0",
|
||||||
|
"ms": "^2.1.1",
|
||||||
|
"semver": "^7.5.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12",
|
||||||
|
"npm": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jsonwebtoken/node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/jwa": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer-equal-constant-time": "^1.0.1",
|
||||||
|
"ecdsa-sig-formatter": "1.0.11",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jws": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"jwa": "^2.0.1",
|
||||||
|
"safe-buffer": "^5.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lodash": {
|
"node_modules/lodash": {
|
||||||
"version": "4.17.21",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash.includes": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isboolean": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isinteger": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isnumber": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isplainobject": {
|
||||||
|
"version": "4.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||||
|
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.isstring": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/lodash.once": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/long": {
|
"node_modules/long": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"dev": "nodemon server.js"
|
"dev": "nodemon server.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"csv-parser": "^3.2.0",
|
"csv-parser": "^3.2.0",
|
||||||
"csv-writer": "^1.6.0",
|
"csv-writer": "^1.6.0",
|
||||||
@@ -15,6 +16,7 @@
|
|||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-fileupload": "^1.5.2",
|
"express-fileupload": "^1.5.2",
|
||||||
"iconv-lite": "^0.6.3",
|
"iconv-lite": "^0.6.3",
|
||||||
|
"jsonwebtoken": "^9.0.3",
|
||||||
"mysql2": "^3.16.0",
|
"mysql2": "^3.16.0",
|
||||||
"sequelize": "^6.32.1",
|
"sequelize": "^6.32.1",
|
||||||
"sqlite3": "^5.1.6",
|
"sqlite3": "^5.1.6",
|
||||||
|
|||||||
@@ -0,0 +1,341 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
const User = require('../models/User');
|
||||||
|
const Role = require('../models/Role');
|
||||||
|
const UserRole = require('../models/UserRole');
|
||||||
|
const { generateToken, authMiddleware } = require('../middleware/auth');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
const SALT_ROUNDS = 10;
|
||||||
|
|
||||||
|
const generateId = () => {
|
||||||
|
return 'user_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 9);
|
||||||
|
};
|
||||||
|
|
||||||
|
router.post('/register', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { username, password, email, phone, realName } = req.body;
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '用户名和密码不能为空'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (username.length < 3 || username.length > 20) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '用户名长度必须在3-20个字符之间'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 6) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '密码长度不能少于6个字符'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingUser = await User.findOne({ where: { username } });
|
||||||
|
if (existingUser) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '用户名已存在'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
|
||||||
|
|
||||||
|
const userCount = await User.count();
|
||||||
|
const isFirstUser = userCount === 0;
|
||||||
|
|
||||||
|
const user = await User.create({
|
||||||
|
userId: generateId(),
|
||||||
|
username,
|
||||||
|
password: hashedPassword,
|
||||||
|
email,
|
||||||
|
phone,
|
||||||
|
realName: realName || username,
|
||||||
|
status: 'active'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isFirstUser) {
|
||||||
|
let adminRole = await Role.findOne({ where: { roleCode: 'admin' } });
|
||||||
|
|
||||||
|
if (!adminRole) {
|
||||||
|
adminRole = await Role.create({
|
||||||
|
roleId: 'role_admin',
|
||||||
|
roleName: '管理员',
|
||||||
|
roleCode: 'admin',
|
||||||
|
description: '系统管理员,拥有所有权限',
|
||||||
|
status: 'active',
|
||||||
|
permissions: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await UserRole.create({
|
||||||
|
UserId: user.userId,
|
||||||
|
RoleId: adminRole.roleId
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const defaultRole = await Role.findOne({ where: { roleCode: 'viewer' } });
|
||||||
|
|
||||||
|
if (defaultRole) {
|
||||||
|
await UserRole.create({
|
||||||
|
UserId: user.userId,
|
||||||
|
RoleId: defaultRole.roleId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = generateToken(user);
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
message: isFirstUser ? '注册成功,已为您分配管理员权限' : '注册成功',
|
||||||
|
data: {
|
||||||
|
user: {
|
||||||
|
userId: user.userId,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
realName: user.realName
|
||||||
|
},
|
||||||
|
token
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('注册错误:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '注册失败',
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/login', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { username, password } = req.body;
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '用户名和密码不能为空'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await User.findOne({ where: { username } });
|
||||||
|
if (!user) {
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: '用户名或密码错误'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.status === 'locked') {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: '账户已被锁定,请联系管理员'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.status === 'inactive') {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: '账户已禁用'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPasswordValid = await bcrypt.compare(password, user.password);
|
||||||
|
if (!isPasswordValid) {
|
||||||
|
user.loginCount = (user.loginCount || 0) + 1;
|
||||||
|
if (user.loginCount >= 5) {
|
||||||
|
user.status = 'locked';
|
||||||
|
}
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: '用户名或密码错误'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = generateToken(user);
|
||||||
|
|
||||||
|
user.lastLoginTime = new Date();
|
||||||
|
user.lastLoginIp = req.ip || req.connection.remoteAddress;
|
||||||
|
user.loginCount = 0;
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: '登录成功',
|
||||||
|
data: {
|
||||||
|
user: {
|
||||||
|
userId: user.userId,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
realName: user.realName,
|
||||||
|
avatar: user.avatar
|
||||||
|
},
|
||||||
|
token
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('登录错误:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '登录失败',
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/profile', authMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const user = await User.findByPk(req.user.userId, {
|
||||||
|
attributes: { exclude: ['password'] }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: '用户不存在'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const roles = await Role.findAll({
|
||||||
|
include: [{
|
||||||
|
model: User,
|
||||||
|
where: { userId: req.user.userId },
|
||||||
|
attributes: []
|
||||||
|
}]
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
user,
|
||||||
|
roles: roles.map(r => ({
|
||||||
|
roleId: r.roleId,
|
||||||
|
roleName: r.roleName,
|
||||||
|
roleCode: r.roleCode
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取profile错误:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '获取用户信息失败'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put('/profile', authMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { email, phone, realName, avatar } = req.body;
|
||||||
|
const user = await User.findByPk(req.user.userId);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: '用户不存在'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (email !== undefined) user.email = email;
|
||||||
|
if (phone !== undefined) user.phone = phone;
|
||||||
|
if (realName !== undefined) user.realName = realName;
|
||||||
|
if (avatar !== undefined) user.avatar = avatar;
|
||||||
|
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: '更新成功',
|
||||||
|
data: {
|
||||||
|
userId: user.userId,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
phone: user.phone,
|
||||||
|
realName: user.realName,
|
||||||
|
avatar: user.avatar
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新profile错误:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '更新失败'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put('/password', authMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { oldPassword, newPassword } = req.body;
|
||||||
|
|
||||||
|
if (!oldPassword || !newPassword) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '旧密码和新密码都不能为空'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPassword.length < 6) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '新密码长度不能少于6个字符'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = await User.findByPk(req.user.userId);
|
||||||
|
const isPasswordValid = await bcrypt.compare(oldPassword, user.password);
|
||||||
|
|
||||||
|
if (!isPasswordValid) {
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: '旧密码错误'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
user.password = await bcrypt.hash(newPassword, SALT_ROUNDS);
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: '密码修改成功'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('修改密码错误:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '密码修改失败'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/check-admin', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userCount = await User.count();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
hasAdmin: userCount > 0,
|
||||||
|
userCount
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查管理员错误:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '检查失败'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
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;
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
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');
|
||||||
@@ -0,0 +1,275 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const Role = require('../models/Role');
|
||||||
|
const Permission = require('../models/Permission');
|
||||||
|
const UserRole = require('../models/UserRole');
|
||||||
|
const User = require('../models/User');
|
||||||
|
const { authMiddleware } = require('../middleware/auth');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
const generateId = () => {
|
||||||
|
return 'role_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 9);
|
||||||
|
};
|
||||||
|
|
||||||
|
const { Op } = require('sequelize');
|
||||||
|
|
||||||
|
router.get('/', authMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { page = 1, pageSize = 10, roleName, status } = req.query;
|
||||||
|
const offset = (parseInt(page) - 1) * parseInt(pageSize);
|
||||||
|
const limit = parseInt(pageSize);
|
||||||
|
|
||||||
|
const where = {};
|
||||||
|
if (roleName) {
|
||||||
|
where.roleName = { [Op.like]: `%${roleName}%` };
|
||||||
|
}
|
||||||
|
if (status) {
|
||||||
|
where.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { count, rows: roles } = await Role.findAndCountAll({
|
||||||
|
where,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
order: [['sort', 'ASC'], ['createdAt', 'DESC']]
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
total: count,
|
||||||
|
page: parseInt(page),
|
||||||
|
pageSize: parseInt(pageSize),
|
||||||
|
roles
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取角色列表错误:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '获取角色列表失败'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/all', authMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const roles = await Role.findAll({
|
||||||
|
where: { status: 'active' },
|
||||||
|
order: [['sort', 'ASC']]
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: roles
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取所有角色错误:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '获取角色列表失败'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/:roleId', authMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const role = await Role.findByPk(req.params.roleId);
|
||||||
|
|
||||||
|
if (!role) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: '角色不存在'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const permissions = await Permission.findAll({
|
||||||
|
where: { status: 'active' },
|
||||||
|
order: [['sort', 'ASC']]
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
role,
|
||||||
|
permissions,
|
||||||
|
rolePermissions: role.permissions || []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取角色详情错误:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '获取角色详情失败'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/', authMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { roleName, roleCode, description, permissions, status, sort } = req.body;
|
||||||
|
|
||||||
|
if (!roleName || !roleCode) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '角色名称和角色编码不能为空'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingRole = await Role.findOne({ where: { roleCode } });
|
||||||
|
if (existingRole) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '角色编码已存在'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const role = await Role.create({
|
||||||
|
roleId: generateId(),
|
||||||
|
roleName,
|
||||||
|
roleCode,
|
||||||
|
description,
|
||||||
|
permissions: permissions || [],
|
||||||
|
status: status || 'active',
|
||||||
|
sort: sort || 0
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
message: '创建成功',
|
||||||
|
data: role
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建角色错误:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '创建角色失败'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put('/:roleId', authMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { roleName, description, permissions, status, sort } = req.body;
|
||||||
|
const role = await Role.findByPk(req.params.roleId);
|
||||||
|
|
||||||
|
if (!role) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: '角色不存在'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roleName !== undefined) role.roleName = roleName;
|
||||||
|
if (description !== undefined) role.description = description;
|
||||||
|
if (permissions !== undefined) role.permissions = permissions;
|
||||||
|
if (status !== undefined) role.status = status;
|
||||||
|
if (sort !== undefined) role.sort = sort;
|
||||||
|
|
||||||
|
await role.save();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: '更新成功',
|
||||||
|
data: role
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新角色错误:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '更新角色失败'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/:roleId', authMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const role = await Role.findByPk(req.params.roleId);
|
||||||
|
|
||||||
|
if (!role) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: '角色不存在'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role.roleCode === 'admin') {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '不能删除管理员角色'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const userCount = await UserRole.count({ where: { RoleId: role.roleId } });
|
||||||
|
if (userCount > 0) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '该角色下有用户,不能删除'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await role.destroy();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: '删除成功'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除角色错误:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '删除角色失败'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/init-roles', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const defaultRoles = [
|
||||||
|
{
|
||||||
|
roleId: 'role_admin',
|
||||||
|
roleName: '管理员',
|
||||||
|
roleCode: 'admin',
|
||||||
|
description: '系统管理员,拥有所有权限',
|
||||||
|
permissions: ['*'],
|
||||||
|
status: 'active',
|
||||||
|
sort: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
roleId: 'role_operator',
|
||||||
|
roleName: '运维人员',
|
||||||
|
roleCode: 'operator',
|
||||||
|
description: '负责日常运维操作',
|
||||||
|
permissions: ['devices:read', 'devices:write', 'racks:read', 'rooms:read', 'consumables:read', 'consumables:write'],
|
||||||
|
status: 'active',
|
||||||
|
sort: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
roleId: 'role_viewer',
|
||||||
|
roleName: '只读用户',
|
||||||
|
roleCode: 'viewer',
|
||||||
|
description: '仅能查看数据',
|
||||||
|
permissions: ['devices:read', 'racks:read', 'rooms:read', 'consumables:read'],
|
||||||
|
status: 'active',
|
||||||
|
sort: 3
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const roleData of defaultRoles) {
|
||||||
|
await Role.upsert(roleData);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: '初始化角色成功'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('初始化角色错误:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '初始化角色失败'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -0,0 +1,450 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const bcrypt = require('bcryptjs');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const User = require('../models/User');
|
||||||
|
const Role = require('../models/Role');
|
||||||
|
const UserRole = require('../models/UserRole');
|
||||||
|
const { authMiddleware } = require('../middleware/auth');
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
const SALT_ROUNDS = 10;
|
||||||
|
|
||||||
|
const generateId = () => {
|
||||||
|
return 'user_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 9);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getWhereClause = (query) => {
|
||||||
|
const where = {};
|
||||||
|
|
||||||
|
if (query.username) {
|
||||||
|
where.username = { [Op.like]: `%${query.username}%` };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.status) {
|
||||||
|
where.status = query.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.realName) {
|
||||||
|
where.realName = { [Op.like]: `%${query.realName}%` };
|
||||||
|
}
|
||||||
|
|
||||||
|
return where;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { Op } = require('sequelize');
|
||||||
|
|
||||||
|
router.get('/', authMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { page = 1, pageSize = 10, username, status, realName } = req.query;
|
||||||
|
const offset = (parseInt(page) - 1) * parseInt(pageSize);
|
||||||
|
const limit = parseInt(pageSize);
|
||||||
|
|
||||||
|
const where = getWhereClause({ username, status, realName });
|
||||||
|
|
||||||
|
const { count, rows: users } = await User.findAndCountAll({
|
||||||
|
where,
|
||||||
|
attributes: { exclude: ['password'] },
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
order: [['createdAt', 'DESC']]
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const user of users) {
|
||||||
|
const userRoles = await UserRole.findAll({
|
||||||
|
include: [{
|
||||||
|
model: Role,
|
||||||
|
where: { status: 'active' }
|
||||||
|
}],
|
||||||
|
where: { UserId: user.userId }
|
||||||
|
});
|
||||||
|
|
||||||
|
user.dataValues.roles = userRoles.map(ur => ({
|
||||||
|
roleId: ur.Role.roleId,
|
||||||
|
roleName: ur.Role.roleName,
|
||||||
|
roleCode: ur.Role.roleCode
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
total: count,
|
||||||
|
page: parseInt(page),
|
||||||
|
pageSize: parseInt(pageSize),
|
||||||
|
users
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户列表错误:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '获取用户列表失败'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/all', authMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const users = await User.findAll({
|
||||||
|
where: { status: 'active' },
|
||||||
|
attributes: ['userId', 'username', 'realName', 'email'],
|
||||||
|
order: [['realName', 'ASC']]
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: users
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取所有用户错误:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '获取用户列表失败'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/:userId', authMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const user = await User.findByPk(req.params.userId, {
|
||||||
|
attributes: { exclude: ['password'] }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: '用户不存在'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const userRoles = await UserRole.findAll({
|
||||||
|
include: [{
|
||||||
|
model: Role,
|
||||||
|
where: { status: 'active' }
|
||||||
|
}],
|
||||||
|
where: { UserId: user.userId }
|
||||||
|
});
|
||||||
|
|
||||||
|
user.dataValues.roles = userRoles.map(ur => ({
|
||||||
|
roleId: ur.Role.roleId,
|
||||||
|
roleName: ur.Role.roleName,
|
||||||
|
roleCode: ur.Role.roleCode
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
data: user
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户详情错误:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '获取用户详情失败'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/', authMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { username, password, email, phone, realName, roleIds, status, remark } = req.body;
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '用户名和密码不能为空'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingUser = await User.findOne({ where: { username } });
|
||||||
|
if (existingUser) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '用户名已存在'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
|
||||||
|
|
||||||
|
const user = await User.create({
|
||||||
|
userId: generateId(),
|
||||||
|
username,
|
||||||
|
password: hashedPassword,
|
||||||
|
email,
|
||||||
|
phone,
|
||||||
|
realName: realName || username,
|
||||||
|
status: status || 'active',
|
||||||
|
remark
|
||||||
|
});
|
||||||
|
|
||||||
|
if (roleIds && roleIds.length > 0) {
|
||||||
|
for (const roleId of roleIds) {
|
||||||
|
await UserRole.create({
|
||||||
|
UserId: user.userId,
|
||||||
|
RoleId: roleId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
message: '创建成功',
|
||||||
|
data: {
|
||||||
|
userId: user.userId,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
realName: user.realName,
|
||||||
|
status: user.status
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建用户错误:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '创建用户失败'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put('/:userId', authMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { username, email, phone, realName, roleIds, status, remark, newPassword } = req.body;
|
||||||
|
const user = await User.findByPk(req.params.userId);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: '用户不存在'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (username !== undefined && username !== user.username) {
|
||||||
|
const existingUser = await User.findOne({
|
||||||
|
where: { username, userId: { [Op.ne]: user.userId } }
|
||||||
|
});
|
||||||
|
if (existingUser) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '用户名已存在'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
user.username = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (email !== undefined) user.email = email;
|
||||||
|
if (phone !== undefined) user.phone = phone;
|
||||||
|
if (realName !== undefined) user.realName = realName;
|
||||||
|
if (status !== undefined) user.status = status;
|
||||||
|
if (remark !== undefined) user.remark = remark;
|
||||||
|
|
||||||
|
if (newPassword && newPassword.length >= 6) {
|
||||||
|
user.password = await bcrypt.hash(newPassword, SALT_ROUNDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
if (roleIds !== undefined) {
|
||||||
|
await UserRole.destroy({ where: { UserId: user.userId } });
|
||||||
|
|
||||||
|
for (const roleId of roleIds) {
|
||||||
|
await UserRole.create({
|
||||||
|
UserId: user.userId,
|
||||||
|
RoleId: roleId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedUser = await User.findByPk(req.params.userId, {
|
||||||
|
attributes: { exclude: ['password'] }
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: '更新成功',
|
||||||
|
data: updatedUser
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新用户错误:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '更新用户失败'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put('/:userId/password', authMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { newPassword } = req.body;
|
||||||
|
const user = await User.findByPk(req.params.userId);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: '用户不存在'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newPassword || newPassword.length < 6) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '密码长度不能少于6个字符'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
user.password = await bcrypt.hash(newPassword, SALT_ROUNDS);
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: '密码重置成功'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('重置密码错误:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '重置密码失败'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/:userId', authMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const user = await User.findByPk(req.params.userId);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: '用户不存在'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.userId === req.user.userId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '不能删除当前登录用户'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await UserRole.destroy({ where: { UserId: user.userId } });
|
||||||
|
await user.destroy();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: '删除成功'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除用户错误:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '删除用户失败'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/:userId/avatar', authMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const user = await User.findByPk(req.params.userId);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: '用户不存在'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.files || !req.files.avatar) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '请选择要上传的头像文件'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const avatarFile = req.files.avatar;
|
||||||
|
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
|
||||||
|
|
||||||
|
if (!allowedTypes.includes(avatarFile.mimetype)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '只支持 JPG、PNG、GIF 和 WebP 格式的图片'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (avatarFile.size > 5 * 1024 * 1024) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '图片大小不能超过 5MB'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadDir = path.join(__dirname, '..', 'uploads', 'avatars');
|
||||||
|
if (!fs.existsSync(uploadDir)) {
|
||||||
|
fs.mkdirSync(uploadDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const ext = path.extname(avatarFile.name) || '.jpg';
|
||||||
|
const filename = `avatar_${user.userId}_${Date.now()}${ext}`;
|
||||||
|
const filepath = path.join(uploadDir, filename);
|
||||||
|
|
||||||
|
await avatarFile.mv(filepath);
|
||||||
|
|
||||||
|
if (user.avatar && user.avatar.startsWith('/uploads/avatars/')) {
|
||||||
|
const oldFile = path.join(__dirname, '..', user.avatar);
|
||||||
|
if (fs.existsSync(oldFile)) {
|
||||||
|
fs.unlinkSync(oldFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const avatarUrl = `/uploads/avatars/${filename}`;
|
||||||
|
user.avatar = avatarUrl;
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: '头像上传成功',
|
||||||
|
data: { avatar: avatarUrl }
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('上传头像错误:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '上传头像失败'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/:userId/avatar', authMiddleware, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const user = await User.findByPk(req.params.userId);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: '用户不存在'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.avatar && user.avatar.startsWith('/uploads/avatars/')) {
|
||||||
|
const oldFile = path.join(__dirname, '..', user.avatar);
|
||||||
|
if (fs.existsSync(oldFile)) {
|
||||||
|
fs.unlinkSync(oldFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
user.avatar = null;
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: '头像删除成功'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除头像错误:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '删除头像失败'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -42,6 +42,11 @@ const backgroundRoutes = require('./routes/background');
|
|||||||
const consumableRoutes = require('./routes/consumables');
|
const consumableRoutes = require('./routes/consumables');
|
||||||
const consumableRecordRoutes = require('./routes/consumableRecords');
|
const consumableRecordRoutes = require('./routes/consumableRecords');
|
||||||
const consumableCategoryRoutes = require('./routes/consumableCategories');
|
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');
|
||||||
|
|
||||||
// 使用路由
|
// 使用路由
|
||||||
app.use('/api/devices', deviceRoutes);
|
app.use('/api/devices', deviceRoutes);
|
||||||
@@ -52,6 +57,11 @@ app.use('/api/background', backgroundRoutes);
|
|||||||
app.use('/api/consumables', consumableRoutes);
|
app.use('/api/consumables', consumableRoutes);
|
||||||
app.use('/api/consumable-records', consumableRecordRoutes);
|
app.use('/api/consumable-records', consumableRecordRoutes);
|
||||||
app.use('/api/consumable-categories', consumableCategoryRoutes);
|
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('/uploads', express.static('uploads'));
|
app.use('/uploads', express.static('uploads'));
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
$postData = @{
|
|
||||||
consumableId = "CON1766493042245"
|
|
||||||
type = "in"
|
|
||||||
quantity = 5
|
|
||||||
operator = "测试管理员"
|
|
||||||
reason = "测试入库"
|
|
||||||
notes = "测试日志记录功能"
|
|
||||||
} | ConvertTo-Json
|
|
||||||
|
|
||||||
$response = Invoke-RestMethod -Uri "http://localhost:8000/api/consumables/quick-inout" -Method Post -Body $postData -ContentType "application/json"
|
|
||||||
|
|
||||||
Write-Host "入库API响应:"
|
|
||||||
$response | ConvertTo-Json -Depth 5
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
+372
-140
@@ -1,7 +1,8 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Layout, Menu, theme, Button } from 'antd';
|
import { Layout, Menu, theme, Button, Dropdown, Avatar, message, Space, Divider } from 'antd';
|
||||||
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-dom';
|
import { BarChartOutlined, DatabaseOutlined, CloudServerOutlined, MenuUnfoldOutlined, MenuFoldOutlined, EyeOutlined, BuildOutlined, HomeOutlined, ShoppingCartOutlined, InboxOutlined, ImportOutlined, FileTextOutlined, UserOutlined, LogoutOutlined, UserOutlined as UserIcon, HistoryOutlined, AuditOutlined } from '@ant-design/icons';
|
||||||
import { BarChartOutlined, DatabaseOutlined, CloudServerOutlined, MenuUnfoldOutlined, MenuFoldOutlined, EyeOutlined, BuildOutlined, HomeOutlined, ShoppingCartOutlined, InboxOutlined, ImportOutlined, FileTextOutlined } from '@ant-design/icons';
|
import { BrowserRouter as Router, Routes, Route, Link, Navigate, useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
import { useAuth } from './context/AuthContext';
|
||||||
import Dashboard from './pages/Dashboard';
|
import Dashboard from './pages/Dashboard';
|
||||||
import DeviceManagement from './pages/DeviceManagement';
|
import DeviceManagement from './pages/DeviceManagement';
|
||||||
import RackManagement from './pages/RackManagement';
|
import RackManagement from './pages/RackManagement';
|
||||||
@@ -12,155 +13,386 @@ import ConsumableManagement from './pages/ConsumableManagement';
|
|||||||
import ConsumableStatistics from './pages/ConsumableStatistics';
|
import ConsumableStatistics from './pages/ConsumableStatistics';
|
||||||
import ConsumableLogs from './pages/ConsumableLogs';
|
import ConsumableLogs from './pages/ConsumableLogs';
|
||||||
import CategoryManagement from './pages/CategoryManagement';
|
import CategoryManagement from './pages/CategoryManagement';
|
||||||
|
import UserManagement from './pages/UserManagement';
|
||||||
|
import LoginHistory from './pages/LoginHistory';
|
||||||
|
import OperationLogs from './pages/OperationLogs';
|
||||||
|
import Login from './pages/Login';
|
||||||
|
import { Spin } from 'antd';
|
||||||
|
|
||||||
const { Content, Sider } = Layout;
|
const { Header, Content, Sider } = Layout;
|
||||||
|
|
||||||
function App() {
|
const PrivateRoute = ({ children }) => {
|
||||||
|
const { token, initialized, loading } = useAuth();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
if (!initialized) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: '100vh',
|
||||||
|
background: '#f5f5f5'
|
||||||
|
}}>
|
||||||
|
<Spin size="large" tip="正在加载认证状态..." />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return <Navigate to="/login" state={{ from: location }} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AppLayout = ({ children }) => {
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
const { user, logout } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
const {
|
const {
|
||||||
token: { colorBgContainer, borderRadiusLG },
|
token: { colorBgContainer, borderRadiusLG },
|
||||||
} = theme.useToken();
|
} = theme.useToken();
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
logout();
|
||||||
|
message.success('已退出登录');
|
||||||
|
navigate('/login');
|
||||||
|
};
|
||||||
|
|
||||||
|
const userMenuItems = [
|
||||||
|
{
|
||||||
|
key: 'logout',
|
||||||
|
icon: <LogoutOutlined />,
|
||||||
|
label: '退出登录',
|
||||||
|
onClick: handleLogout
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Layout>
|
||||||
<Layout>
|
<Sider
|
||||||
<Sider
|
width={220}
|
||||||
width={220}
|
collapsedWidth={80}
|
||||||
collapsedWidth={80}
|
collapsed={collapsed}
|
||||||
collapsed={collapsed}
|
style={{
|
||||||
|
backgroundColor: colorBgContainer,
|
||||||
|
boxShadow: '2px 0 8px rgba(0,0,0,0.08)',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: 64,
|
||||||
|
padding: '0 12px',
|
||||||
|
borderBottom: '1px solid #f0f0f0',
|
||||||
|
marginBottom: 8
|
||||||
|
}}>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
||||||
|
onClick={() => setCollapsed(!collapsed)}
|
||||||
|
style={{
|
||||||
|
fontSize: 18,
|
||||||
|
padding: '8px',
|
||||||
|
borderRadius: 4
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Menu
|
||||||
|
mode="inline"
|
||||||
|
defaultSelectedKeys={['dashboard']}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: colorBgContainer,
|
flex: 1,
|
||||||
boxShadow: '2px 0 8px rgba(0,0,0,0.08)'
|
borderRight: 0,
|
||||||
|
backgroundColor: 'transparent'
|
||||||
|
}}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
key: 'dashboard',
|
||||||
|
icon: <BarChartOutlined />,
|
||||||
|
label: <Link to="/">仪表盘</Link>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'room-management',
|
||||||
|
icon: <HomeOutlined />,
|
||||||
|
label: '机房管理',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
key: 'rooms',
|
||||||
|
icon: <HomeOutlined />,
|
||||||
|
label: <Link to="/rooms">机房管理</Link>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'racks',
|
||||||
|
icon: <DatabaseOutlined />,
|
||||||
|
label: <Link to="/racks">机柜管理</Link>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'visualization',
|
||||||
|
icon: <EyeOutlined />,
|
||||||
|
label: <Link to="/visualization">机柜可视化</Link>,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'asset-management',
|
||||||
|
icon: <BuildOutlined />,
|
||||||
|
label: '资产管理',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
key: 'devices',
|
||||||
|
icon: <CloudServerOutlined />,
|
||||||
|
label: <Link to="/devices">设备管理</Link>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'fields',
|
||||||
|
icon: <DatabaseOutlined />,
|
||||||
|
label: <Link to="/fields">字段管理</Link>,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'consumables-management',
|
||||||
|
icon: <ShoppingCartOutlined />,
|
||||||
|
label: '耗材管理',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
key: 'consumables-stats',
|
||||||
|
icon: <BarChartOutlined />,
|
||||||
|
label: <Link to="/consumables-stats">耗材统计</Link>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'consumables',
|
||||||
|
icon: <DatabaseOutlined />,
|
||||||
|
label: <Link to="/consumables">耗材列表</Link>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'consumables-categories',
|
||||||
|
icon: <ImportOutlined />,
|
||||||
|
label: <Link to="/consumables-categories">分类管理</Link>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'consumables-logs',
|
||||||
|
icon: <FileTextOutlined />,
|
||||||
|
label: <Link to="/consumables-logs">操作日志</Link>,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'system-management',
|
||||||
|
icon: <UserOutlined />,
|
||||||
|
label: '系统管理',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
key: 'users',
|
||||||
|
icon: <UserOutlined />,
|
||||||
|
label: <Link to="/users">用户管理</Link>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'login-history',
|
||||||
|
icon: <HistoryOutlined />,
|
||||||
|
label: <Link to="/login-history">登录历史</Link>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'operation-logs',
|
||||||
|
icon: <AuditOutlined />,
|
||||||
|
label: <Link to="/operation-logs">操作日志</Link>,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Sider>
|
||||||
|
<Layout style={{ padding: '0 24px 24px' }}>
|
||||||
|
<Header style={{
|
||||||
|
padding: '0 16px',
|
||||||
|
height: 56,
|
||||||
|
background: colorBgContainer,
|
||||||
|
marginBottom: 24,
|
||||||
|
borderRadius: borderRadiusLG,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
alignItems: 'center',
|
||||||
|
boxShadow: '0 1px 4px rgba(0,0,0,0.08)'
|
||||||
|
}}>
|
||||||
|
{user && (
|
||||||
|
<Space size={12}>
|
||||||
|
<Avatar
|
||||||
|
style={{ backgroundColor: '#1890ff', cursor: 'pointer' }}
|
||||||
|
icon={<UserOutlined />}
|
||||||
|
/>
|
||||||
|
<span style={{ color: '#666', fontSize: 14 }}>{user.username}</span>
|
||||||
|
<Divider type="vertical" style={{ margin: 0 }} />
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
icon={<LogoutOutlined />}
|
||||||
|
onClick={handleLogout}
|
||||||
|
style={{ padding: '4px 8px' }}
|
||||||
|
>
|
||||||
|
退出
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
</Header>
|
||||||
|
<Content
|
||||||
|
style={{
|
||||||
|
padding: 24,
|
||||||
|
margin: 0,
|
||||||
|
minHeight: 280,
|
||||||
|
background: colorBgContainer,
|
||||||
|
borderRadius: borderRadiusLG,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{
|
{children}
|
||||||
display: 'flex',
|
</Content>
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
height: 64,
|
|
||||||
borderBottom: '1px solid #f0f0f0',
|
|
||||||
marginBottom: 16
|
|
||||||
}}>
|
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
|
|
||||||
onClick={() => setCollapsed(!collapsed)}
|
|
||||||
style={{
|
|
||||||
fontSize: 18,
|
|
||||||
padding: '8px',
|
|
||||||
borderRadius: 4,
|
|
||||||
transition: 'all 0.3s'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Menu
|
|
||||||
mode="inline"
|
|
||||||
defaultSelectedKeys={['dashboard']}
|
|
||||||
style={{
|
|
||||||
height: 'calc(100% - 80px)',
|
|
||||||
borderRight: 0,
|
|
||||||
backgroundColor: 'transparent'
|
|
||||||
}}
|
|
||||||
items={[
|
|
||||||
{
|
|
||||||
key: 'dashboard',
|
|
||||||
icon: <BarChartOutlined />,
|
|
||||||
label: <Link to="/">仪表盘</Link>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'room-management',
|
|
||||||
icon: <HomeOutlined />,
|
|
||||||
label: '机房管理',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
key: 'rooms',
|
|
||||||
icon: <HomeOutlined />,
|
|
||||||
label: <Link to="/rooms">机房管理</Link>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'racks',
|
|
||||||
icon: <DatabaseOutlined />,
|
|
||||||
label: <Link to="/racks">机柜管理</Link>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'visualization',
|
|
||||||
icon: <EyeOutlined />,
|
|
||||||
label: <Link to="/visualization">机柜可视化</Link>,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'asset-management',
|
|
||||||
icon: <BuildOutlined />,
|
|
||||||
label: '资产管理',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
key: 'devices',
|
|
||||||
icon: <CloudServerOutlined />,
|
|
||||||
label: <Link to="/devices">设备管理</Link>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'fields',
|
|
||||||
icon: <DatabaseOutlined />,
|
|
||||||
label: <Link to="/fields">字段管理</Link>,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'consumables-management',
|
|
||||||
icon: <ShoppingCartOutlined />,
|
|
||||||
label: '耗材管理',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
key: 'consumables-stats',
|
|
||||||
icon: <BarChartOutlined />,
|
|
||||||
label: <Link to="/consumables-stats">耗材统计</Link>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'consumables',
|
|
||||||
icon: <DatabaseOutlined />,
|
|
||||||
label: <Link to="/consumables">耗材列表</Link>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'consumables-categories',
|
|
||||||
icon: <ImportOutlined />,
|
|
||||||
label: <Link to="/consumables-categories">分类管理</Link>,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'consumables-logs',
|
|
||||||
icon: <FileTextOutlined />,
|
|
||||||
label: <Link to="/consumables-logs">操作日志</Link>,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Sider>
|
|
||||||
<Layout style={{ padding: '0 24px 24px' }}>
|
|
||||||
<Content
|
|
||||||
style={{
|
|
||||||
padding: 24,
|
|
||||||
margin: 0,
|
|
||||||
minHeight: 280,
|
|
||||||
background: colorBgContainer,
|
|
||||||
borderRadius: borderRadiusLG,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Routes>
|
|
||||||
<Route path="/" element={<Dashboard />} />
|
|
||||||
<Route path="/devices" element={<DeviceManagement />} />
|
|
||||||
<Route path="/racks" element={<RackManagement />} />
|
|
||||||
<Route path="/rooms" element={<RoomManagement />} />
|
|
||||||
<Route path="/fields" element={<DeviceFieldManagement />} />
|
|
||||||
<Route path="/visualization" element={<RackVisualization />} />
|
|
||||||
<Route path="/consumables" element={<ConsumableManagement />} />
|
|
||||||
<Route path="/consumables-categories" element={<CategoryManagement />} />
|
|
||||||
<Route path="/consumables-stats" element={<ConsumableStatistics />} />
|
|
||||||
<Route path="/consumables-logs" element={<ConsumableLogs />} />
|
|
||||||
</Routes>
|
|
||||||
</Content>
|
|
||||||
</Layout>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<Router>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/login" element={<Login />} />
|
||||||
|
<Route
|
||||||
|
path="/"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<AppLayout>
|
||||||
|
<Dashboard />
|
||||||
|
</AppLayout>
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/devices"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<AppLayout>
|
||||||
|
<DeviceManagement />
|
||||||
|
</AppLayout>
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/racks"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<AppLayout>
|
||||||
|
<RackManagement />
|
||||||
|
</AppLayout>
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/rooms"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<AppLayout>
|
||||||
|
<RoomManagement />
|
||||||
|
</AppLayout>
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/fields"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<AppLayout>
|
||||||
|
<DeviceFieldManagement />
|
||||||
|
</AppLayout>
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/visualization"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<AppLayout>
|
||||||
|
<RackVisualization />
|
||||||
|
</AppLayout>
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/consumables"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<AppLayout>
|
||||||
|
<ConsumableManagement />
|
||||||
|
</AppLayout>
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/consumables-categories"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<AppLayout>
|
||||||
|
<CategoryManagement />
|
||||||
|
</AppLayout>
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/consumables-stats"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<AppLayout>
|
||||||
|
<ConsumableStatistics />
|
||||||
|
</AppLayout>
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/consumables-logs"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<AppLayout>
|
||||||
|
<ConsumableLogs />
|
||||||
|
</AppLayout>
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/users"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<AppLayout>
|
||||||
|
<UserManagement />
|
||||||
|
</AppLayout>
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/login-history"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<AppLayout>
|
||||||
|
<LoginHistory />
|
||||||
|
</AppLayout>
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/operation-logs"
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<AppLayout>
|
||||||
|
<OperationLogs />
|
||||||
|
</AppLayout>
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const API_BASE_URL = '/api';
|
||||||
|
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: API_BASE_URL,
|
||||||
|
timeout: 30000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
api.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
const token = localStorage.getItem('token');
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
} else {
|
||||||
|
console.log('[API] No token found in localStorage');
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
api.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
if (error.response) {
|
||||||
|
const { status, data } = error.response;
|
||||||
|
|
||||||
|
if (status === 401) {
|
||||||
|
const currentPath = window.location.pathname;
|
||||||
|
console.log('[API] 401 error, current path:', currentPath);
|
||||||
|
|
||||||
|
if (!currentPath.startsWith('/login')) {
|
||||||
|
const savedToken = localStorage.getItem('token');
|
||||||
|
if (savedToken) {
|
||||||
|
console.log('[API] Token exists but got 401, might be expired');
|
||||||
|
}
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
window.location.href = '/login';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(data.message || '请求失败');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.code === 'ECONNABORTED') {
|
||||||
|
return Promise.reject('请求超时,请稍后重试');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject('网络错误,请检查网络连接');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const authAPI = {
|
||||||
|
checkAdmin: () => api.get('/auth/check-admin'),
|
||||||
|
register: (data) => api.post('/auth/register', data),
|
||||||
|
login: (data) => api.post('/auth/login', data),
|
||||||
|
getProfile: () => api.get('/auth/profile'),
|
||||||
|
updateProfile: (data) => api.put('/auth/profile', data),
|
||||||
|
changePassword: (data) => api.put('/auth/password', data)
|
||||||
|
};
|
||||||
|
|
||||||
|
export const userAPI = {
|
||||||
|
list: (params) => api.get('/users', { params }),
|
||||||
|
all: () => api.get('/users/all'),
|
||||||
|
get: (userId) => api.get(`/users/${userId}`),
|
||||||
|
create: (data) => api.post('/users', data),
|
||||||
|
update: (userId, data) => api.put(`/users/${userId}`, data),
|
||||||
|
resetPassword: (userId, data) => api.put(`/users/${userId}/password`, data),
|
||||||
|
delete: (userId) => api.delete(`/users/${userId}`),
|
||||||
|
uploadAvatar: (userId, file) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('avatar', file);
|
||||||
|
return api.post(`/users/${userId}/avatar`, formData, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
});
|
||||||
|
},
|
||||||
|
deleteAvatar: (userId) => api.delete(`/users/${userId}/avatar`)
|
||||||
|
};
|
||||||
|
|
||||||
|
export const roleAPI = {
|
||||||
|
list: (params) => api.get('/roles', { params }),
|
||||||
|
all: () => api.get('/roles/all'),
|
||||||
|
get: (roleId) => api.get(`/roles/${roleId}`),
|
||||||
|
create: (data) => api.post('/roles', data),
|
||||||
|
update: (roleId, data) => api.put(`/roles/${roleId}`, data),
|
||||||
|
delete: (roleId) => api.delete(`/roles/${roleId}`),
|
||||||
|
initRoles: () => api.post('/roles/init-roles')
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loginHistoryAPI = {
|
||||||
|
list: (params) => api.get('/login-history', { params }),
|
||||||
|
getByUser: (userId, params) => api.get(`/login-history/user/${userId}`, { params }),
|
||||||
|
delete: (id) => api.delete(`/login-history/${id}`),
|
||||||
|
clear: (data) => api.delete('/login-history', { data })
|
||||||
|
};
|
||||||
|
|
||||||
|
export const operationLogAPI = {
|
||||||
|
list: (params) => api.get('/operation-logs', { params }),
|
||||||
|
getActions: () => api.get('/operation-logs/actions'),
|
||||||
|
getModules: () => api.get('/operation-logs/modules'),
|
||||||
|
delete: (id) => api.delete(`/operation-logs/${id}`),
|
||||||
|
clear: (data) => api.delete('/operation-logs', { data })
|
||||||
|
};
|
||||||
|
|
||||||
|
export default api;
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Navigate, useLocation } from 'react-router-dom';
|
||||||
|
import { Spin } from 'antd';
|
||||||
|
import { useAuth } from '../context/AuthContext';
|
||||||
|
|
||||||
|
const ProtectedRoute = ({ children, requiredPermission }) => {
|
||||||
|
const { user, token, loading, initialized } = useAuth();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
if (!initialized) {
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: '100vh'
|
||||||
|
}}>
|
||||||
|
<Spin size="large" tip="加载中..." />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return <Navigate to="/login" state={{ from: location }} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requiredPermission && !user) {
|
||||||
|
return <Navigate to="/" replace />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProtectedRoute;
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
||||||
|
import { authAPI } from '../api';
|
||||||
|
|
||||||
|
const AuthContext = createContext(null);
|
||||||
|
|
||||||
|
export const useAuth = () => {
|
||||||
|
const context = useContext(AuthContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useAuth必须在AuthProvider内部使用');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AuthProvider = ({ children }) => {
|
||||||
|
console.log('[AuthContext] Initializing...');
|
||||||
|
|
||||||
|
const savedToken = localStorage.getItem('token');
|
||||||
|
const savedUser = localStorage.getItem('user');
|
||||||
|
|
||||||
|
console.log('[AuthContext] Saved token:', savedToken ? 'exists' : 'null');
|
||||||
|
console.log('[AuthContext] Saved user:', savedUser ? 'exists' : 'null');
|
||||||
|
|
||||||
|
const [user, setUser] = useState(() => {
|
||||||
|
try {
|
||||||
|
if (savedUser) {
|
||||||
|
return JSON.parse(savedUser);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[AuthContext] Parse user error:', e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const [token, setToken] = useState(() => savedToken);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [initialized, setInitialized] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentToken = localStorage.getItem('token');
|
||||||
|
const currentUser = localStorage.getItem('user');
|
||||||
|
|
||||||
|
if (currentToken && currentToken === token) {
|
||||||
|
if (currentUser) {
|
||||||
|
try {
|
||||||
|
const parsedUser = JSON.parse(currentUser);
|
||||||
|
if (JSON.stringify(parsedUser) !== JSON.stringify(user)) {
|
||||||
|
setUser(parsedUser);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[AuthContext] Parse user error:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setInitialized(true);
|
||||||
|
} else if (!currentToken) {
|
||||||
|
setToken(null);
|
||||||
|
setUser(null);
|
||||||
|
setInitialized(true);
|
||||||
|
} else {
|
||||||
|
setToken(currentToken);
|
||||||
|
setInitialized(true);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchProfile = useCallback(async () => {
|
||||||
|
const currentToken = localStorage.getItem('token');
|
||||||
|
if (!currentToken) {
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await authAPI.getProfile();
|
||||||
|
if (response.success) {
|
||||||
|
setUser(response.data.user);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户信息失败:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const login = async (username, password) => {
|
||||||
|
try {
|
||||||
|
const response = await authAPI.login({ username, password });
|
||||||
|
if (response.success) {
|
||||||
|
const { token: newToken, user: userData } = response.data;
|
||||||
|
localStorage.setItem('token', newToken);
|
||||||
|
localStorage.setItem('user', JSON.stringify(userData));
|
||||||
|
setToken(newToken);
|
||||||
|
setUser(userData);
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
return { success: false, message: response.message };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, message: error };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const register = async (userData) => {
|
||||||
|
try {
|
||||||
|
const response = await authAPI.register(userData);
|
||||||
|
if (response.success) {
|
||||||
|
const { token: newToken, user: newUser } = response.data;
|
||||||
|
localStorage.setItem('token', newToken);
|
||||||
|
localStorage.setItem('user', JSON.stringify(newUser));
|
||||||
|
setToken(newToken);
|
||||||
|
setUser(newUser);
|
||||||
|
return { success: true, isFirstUser: response.data.isFirstUser };
|
||||||
|
}
|
||||||
|
return { success: false, message: response.message };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, message: error };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const logout = useCallback(() => {
|
||||||
|
localStorage.removeItem('token');
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
setToken(null);
|
||||||
|
setUser(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateUser = (newUserData) => {
|
||||||
|
const updatedUser = { ...user, ...newUserData };
|
||||||
|
setUser(updatedUser);
|
||||||
|
localStorage.setItem('user', JSON.stringify(updatedUser));
|
||||||
|
};
|
||||||
|
|
||||||
|
const hasPermission = (permission) => {
|
||||||
|
if (!user) return false;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
user,
|
||||||
|
token,
|
||||||
|
loading,
|
||||||
|
initialized,
|
||||||
|
login,
|
||||||
|
register,
|
||||||
|
logout,
|
||||||
|
updateUser,
|
||||||
|
hasPermission,
|
||||||
|
checkAdmin: () => authAPI.checkAdmin()
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AuthContext;
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
import { AuthProvider } from './context/AuthContext';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<AuthProvider>
|
||||||
|
<App />
|
||||||
|
</AuthProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
@@ -0,0 +1,303 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Form, Input, Button, Card, message, Typography, Tabs, Divider, Space, Modal, Alert } from 'antd';
|
||||||
|
import { UserOutlined, LockOutlined, MailOutlined, PhoneOutlined, SafetyCertificateOutlined, RobotOutlined } from '@ant-design/icons';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useAuth } from '../context/AuthContext';
|
||||||
|
|
||||||
|
const { Title, Text } = Typography;
|
||||||
|
|
||||||
|
const Login = () => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [isFirstUser, setIsFirstUser] = useState(false);
|
||||||
|
const [registerMode, setRegisterMode] = useState(false);
|
||||||
|
const { login, register, checkAdmin } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkIsFirstUser();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const checkIsFirstUser = async () => {
|
||||||
|
try {
|
||||||
|
const response = await checkAdmin();
|
||||||
|
if (response.success) {
|
||||||
|
setIsFirstUser(!response.data.hasAdmin);
|
||||||
|
if (!response.data.hasAdmin) {
|
||||||
|
setRegisterMode(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查用户状态失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFinishLogin = async (values) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await login(values.username, values.password);
|
||||||
|
if (result.success) {
|
||||||
|
message.success('登录成功');
|
||||||
|
navigate('/');
|
||||||
|
} else {
|
||||||
|
message.error(result.message || '登录失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error || '登录失败');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFinishRegister = async (values) => {
|
||||||
|
if (values.password !== values.confirmPassword) {
|
||||||
|
message.error('两次输入的密码不一致');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await register({
|
||||||
|
username: values.username,
|
||||||
|
password: values.password,
|
||||||
|
email: values.email,
|
||||||
|
phone: values.phone,
|
||||||
|
realName: values.realName
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
message.success(result.isFirstUser ? '注册成功,已为您创建管理员账户' : '注册成功');
|
||||||
|
navigate('/');
|
||||||
|
} else {
|
||||||
|
message.error(result.message || '注册失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error(error || '注册失败');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const containerStyle = {
|
||||||
|
minHeight: '100vh',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
|
padding: '24px'
|
||||||
|
};
|
||||||
|
|
||||||
|
const cardStyle = {
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: isFirstUser ? 450 : 400,
|
||||||
|
borderRadius: '16px',
|
||||||
|
boxShadow: '0 20px 60px rgba(0,0,0,0.3)'
|
||||||
|
};
|
||||||
|
|
||||||
|
const headerStyle = {
|
||||||
|
textAlign: 'center',
|
||||||
|
marginBottom: '32px'
|
||||||
|
};
|
||||||
|
|
||||||
|
const titleStyle = {
|
||||||
|
fontSize: '28px',
|
||||||
|
fontWeight: '700',
|
||||||
|
color: '#1a1a2e',
|
||||||
|
marginBottom: '8px'
|
||||||
|
};
|
||||||
|
|
||||||
|
const subtitleStyle = {
|
||||||
|
fontSize: '14px',
|
||||||
|
color: '#666'
|
||||||
|
};
|
||||||
|
|
||||||
|
const formStyle = {
|
||||||
|
marginTop: '24px'
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitButtonStyle = {
|
||||||
|
width: '100%',
|
||||||
|
height: '48px',
|
||||||
|
fontSize: '16px',
|
||||||
|
fontWeight: '600',
|
||||||
|
borderRadius: '8px'
|
||||||
|
};
|
||||||
|
|
||||||
|
const footerStyle = {
|
||||||
|
textAlign: 'center',
|
||||||
|
marginTop: '24px'
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={containerStyle}>
|
||||||
|
<Card style={cardStyle}>
|
||||||
|
<div style={headerStyle}>
|
||||||
|
<div style={{
|
||||||
|
fontSize: '48px',
|
||||||
|
marginBottom: '16px',
|
||||||
|
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||||
|
WebkitBackgroundClip: 'text',
|
||||||
|
WebkitTextFillColor: 'transparent'
|
||||||
|
}}>
|
||||||
|
<RobotOutlined />
|
||||||
|
</div>
|
||||||
|
<Title level={2} style={titleStyle}>
|
||||||
|
{isFirstUser ? '创建管理员账户' : 'IDC设备管理系统'}
|
||||||
|
</Title>
|
||||||
|
<Text style={subtitleStyle}>
|
||||||
|
{isFirstUser ? '首次使用,请创建系统管理员账户' : '请登录您的账户'}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isFirstUser && (
|
||||||
|
<Alert
|
||||||
|
message="欢迎使用IDC设备管理系统"
|
||||||
|
description="您是第一个用户,系统将自动为您分配管理员权限。"
|
||||||
|
type="success"
|
||||||
|
showIcon
|
||||||
|
style={{ marginBottom: '24px' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Form
|
||||||
|
name={registerMode ? 'register' : 'login'}
|
||||||
|
size="large"
|
||||||
|
onFinish={registerMode ? onFinishRegister : onFinishLogin}
|
||||||
|
style={formStyle}
|
||||||
|
>
|
||||||
|
{registerMode ? (
|
||||||
|
<>
|
||||||
|
<Form.Item
|
||||||
|
name="username"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入用户名' },
|
||||||
|
{ min: 3, max: 20, message: '用户名长度必须在3-20个字符之间' },
|
||||||
|
{ pattern: /^[a-zA-Z0-9_]+$/, message: '用户名只能包含字母、数字和下划线' }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
prefix={<UserOutlined />}
|
||||||
|
placeholder="用户名"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="realName"
|
||||||
|
rules={[{ required: true, message: '请输入真实姓名' }]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
prefix={<SafetyCertificateOutlined />}
|
||||||
|
placeholder="真实姓名"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="email"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入邮箱' },
|
||||||
|
{ type: 'email', message: '请输入有效的邮箱地址' }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
prefix={<MailOutlined />}
|
||||||
|
placeholder="邮箱"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="phone"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
prefix={<PhoneOutlined />}
|
||||||
|
placeholder="手机号(可选)"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="password"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入密码' },
|
||||||
|
{ min: 6, message: '密码长度不能少于6个字符' }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password
|
||||||
|
prefix={<LockOutlined />}
|
||||||
|
placeholder="密码"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="confirmPassword"
|
||||||
|
dependencies={['password']}
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请确认密码' },
|
||||||
|
({ getFieldValue }) => ({
|
||||||
|
validator(_, value) {
|
||||||
|
if (!value || getFieldValue('password') === value) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error('两次输入的密码不一致'));
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password
|
||||||
|
prefix={<LockOutlined />}
|
||||||
|
placeholder="确认密码"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Form.Item
|
||||||
|
name="username"
|
||||||
|
rules={[{ required: true, message: '请输入用户名' }]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
prefix={<UserOutlined />}
|
||||||
|
placeholder="用户名"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="password"
|
||||||
|
rules={[{ required: true, message: '请输入密码' }]}
|
||||||
|
>
|
||||||
|
<Input.Password
|
||||||
|
prefix={<LockOutlined />}
|
||||||
|
placeholder="密码"
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Form.Item style={{ marginBottom: '16px' }}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
htmlType="submit"
|
||||||
|
loading={loading}
|
||||||
|
style={submitButtonStyle}
|
||||||
|
>
|
||||||
|
{registerMode ? '立即注册' : '登 录'}
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
{!isFirstUser && (
|
||||||
|
<div style={footerStyle}>
|
||||||
|
<Space split={<Divider type="vertical" />}>
|
||||||
|
<Button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
onClick={() => setRegisterMode(!registerMode)}
|
||||||
|
>
|
||||||
|
{registerMode ? '已有账户?去登录' : '注册新账户'}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Login;
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
import React, { useState, useEffect } 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 = 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 loginHistoryAPI.clear({ days: 30 });
|
||||||
|
if (response.success) {
|
||||||
|
message.success('已清理30天前的登录记录');
|
||||||
|
fetchHistories();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('清理失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
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={columns}
|
||||||
|
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;
|
||||||
@@ -0,0 +1,335 @@
|
|||||||
|
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;
|
||||||
@@ -0,0 +1,567 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import { Card, Table, Button, Space, Modal, Form, Input, Select, message, Tag, Popconfirm, Avatar, Tooltip, Badge } from 'antd';
|
||||||
|
import { PlusOutlined, EditOutlined, DeleteOutlined, UserOutlined, ReloadOutlined, LockOutlined, CameraOutlined } from '@ant-design/icons';
|
||||||
|
import { userAPI, roleAPI } from '../api';
|
||||||
|
|
||||||
|
const { Option } = Select;
|
||||||
|
|
||||||
|
const UserManagement = () => {
|
||||||
|
const [users, setUsers] = useState([]);
|
||||||
|
const [roles, setRoles] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 });
|
||||||
|
const [modalVisible, setModalVisible] = useState(false);
|
||||||
|
const [passwordModalVisible, setPasswordModalVisible] = useState(false);
|
||||||
|
const [avatarModalVisible, setAvatarModalVisible] = useState(false);
|
||||||
|
const [editingUser, setEditingUser] = useState(null);
|
||||||
|
const [passwordUser, setPasswordUser] = useState(null);
|
||||||
|
const [avatarUser, setAvatarUser] = useState(null);
|
||||||
|
const [uploadLoading, setUploadLoading] = useState(false);
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const [passwordForm] = Form.useForm();
|
||||||
|
const fileInputRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchUsers();
|
||||||
|
fetchRoles();
|
||||||
|
}, [pagination.current]);
|
||||||
|
|
||||||
|
const fetchUsers = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await userAPI.list({
|
||||||
|
page: pagination.current,
|
||||||
|
pageSize: pagination.pageSize
|
||||||
|
});
|
||||||
|
if (response.success) {
|
||||||
|
setUsers(response.data.users);
|
||||||
|
setPagination(prev => ({ ...prev, total: response.data.total }));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('获取用户列表失败');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchRoles = async () => {
|
||||||
|
try {
|
||||||
|
const response = await roleAPI.all();
|
||||||
|
if (response.success) {
|
||||||
|
setRoles(response.data);
|
||||||
|
} else {
|
||||||
|
message.error('获取角色列表失败: ' + (response.message || '未知错误'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取角色列表失败:', error);
|
||||||
|
message.error('获取角色列表失败,请检查网络连接');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
setEditingUser(null);
|
||||||
|
form.resetFields();
|
||||||
|
setModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (user) => {
|
||||||
|
setEditingUser(user);
|
||||||
|
form.setFieldsValue({
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
phone: user.phone,
|
||||||
|
realName: user.realName,
|
||||||
|
status: user.status,
|
||||||
|
roleIds: user.roles?.map(r => r.roleId) || []
|
||||||
|
});
|
||||||
|
setModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetPassword = (user) => {
|
||||||
|
setPasswordUser(user);
|
||||||
|
passwordForm.resetFields();
|
||||||
|
setPasswordModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAvatarClick = (user) => {
|
||||||
|
setAvatarUser(user);
|
||||||
|
setAvatarModalVisible(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAvatarUpload = async (e) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
if (!file.type.match(/image\/(jpeg|png|gif|webp)/)) {
|
||||||
|
message.error('只支持 JPG、PNG、GIF 和 WebP 格式的图片');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.size > 5 * 1024 * 1024) {
|
||||||
|
message.error('图片大小不能超过 5MB');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setUploadLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await userAPI.uploadAvatar(avatarUser.userId, file);
|
||||||
|
if (response.success) {
|
||||||
|
message.success('头像上传成功');
|
||||||
|
fetchUsers();
|
||||||
|
setAvatarModalVisible(false);
|
||||||
|
} else {
|
||||||
|
message.error(response.message || '上传失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('上传失败');
|
||||||
|
} finally {
|
||||||
|
setUploadLoading(false);
|
||||||
|
if (fileInputRef.current) {
|
||||||
|
fileInputRef.current.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAvatarDelete = async () => {
|
||||||
|
try {
|
||||||
|
const response = await userAPI.deleteAvatar(avatarUser.userId);
|
||||||
|
if (response.success) {
|
||||||
|
message.success('头像已删除');
|
||||||
|
fetchUsers();
|
||||||
|
setAvatarModalVisible(false);
|
||||||
|
} else {
|
||||||
|
message.error(response.message || '删除失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('删除失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async (userId) => {
|
||||||
|
try {
|
||||||
|
const response = await userAPI.delete(userId);
|
||||||
|
if (response.success) {
|
||||||
|
message.success('删除成功');
|
||||||
|
fetchUsers();
|
||||||
|
} else {
|
||||||
|
message.error(response.message || '删除失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('删除失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (values) => {
|
||||||
|
try {
|
||||||
|
let response;
|
||||||
|
if (editingUser) {
|
||||||
|
response = await userAPI.update(editingUser.userId, values);
|
||||||
|
} else {
|
||||||
|
response = await userAPI.create(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
message.success(editingUser ? '更新成功' : '创建成功');
|
||||||
|
setModalVisible(false);
|
||||||
|
fetchUsers();
|
||||||
|
} else {
|
||||||
|
message.error(response.message || '操作失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('操作失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResetPasswordSubmit = async (values) => {
|
||||||
|
try {
|
||||||
|
const response = await userAPI.resetPassword(passwordUser.userId, values);
|
||||||
|
if (response.success) {
|
||||||
|
message.success('密码重置成功');
|
||||||
|
setPasswordModalVisible(false);
|
||||||
|
} else {
|
||||||
|
message.error(response.message || '重置失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('重置失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (status) => {
|
||||||
|
const colors = {
|
||||||
|
active: 'green',
|
||||||
|
inactive: 'red',
|
||||||
|
locked: 'orange'
|
||||||
|
};
|
||||||
|
return colors[status] || 'default';
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusText = (status) => {
|
||||||
|
const texts = {
|
||||||
|
active: '正常',
|
||||||
|
inactive: '禁用',
|
||||||
|
locked: '锁定'
|
||||||
|
};
|
||||||
|
return texts[status] || status;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAvatarUrl = (user) => {
|
||||||
|
if (!user?.avatar) return null;
|
||||||
|
return user.avatar;
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: '头像',
|
||||||
|
key: 'avatar',
|
||||||
|
width: 80,
|
||||||
|
render: (_, record) => (
|
||||||
|
<Badge dot={!!record.avatar} color="green" offset={[-5, 35]}>
|
||||||
|
<Avatar
|
||||||
|
size={48}
|
||||||
|
icon={!record.avatar && <UserOutlined />}
|
||||||
|
src={getAvatarUrl(record)}
|
||||||
|
style={{
|
||||||
|
backgroundColor: record.avatar ? 'transparent' : '#1890ff',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
onClick={() => handleAvatarClick(record)}
|
||||||
|
/>
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '用户名',
|
||||||
|
key: 'username',
|
||||||
|
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: 'email',
|
||||||
|
key: 'email',
|
||||||
|
width: 200,
|
||||||
|
render: (email) => email || '-'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '手机号',
|
||||||
|
dataIndex: 'phone',
|
||||||
|
key: 'phone',
|
||||||
|
width: 130,
|
||||||
|
render: (phone) => phone || '-'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '角色',
|
||||||
|
key: 'roles',
|
||||||
|
render: (_, record) => (
|
||||||
|
<Space wrap>
|
||||||
|
{record.roles?.map(role => (
|
||||||
|
<Tag key={role.roleId} color={role.roleCode === 'admin' ? 'blue' : 'green'}>
|
||||||
|
{role.roleName}
|
||||||
|
</Tag>
|
||||||
|
)) || '-'}
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'status',
|
||||||
|
key: 'status',
|
||||||
|
render: (status) => (
|
||||||
|
<Tag color={getStatusColor(status)}>{getStatusText(status)}</Tag>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '最后登录',
|
||||||
|
key: 'lastLogin',
|
||||||
|
render: (_, record) => (
|
||||||
|
<div style={{ fontSize: '12px' }}>
|
||||||
|
<div>{record.lastLoginTime ? new Date(record.lastLoginTime).toLocaleString() : '从未登录'}</div>
|
||||||
|
<div style={{ color: '#999' }}>{record.lastLoginIp || '-'}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'action',
|
||||||
|
render: (_, record) => (
|
||||||
|
<Space size="small">
|
||||||
|
<Tooltip title="编辑">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
onClick={() => handleEdit(record)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="重置密码">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<LockOutlined />}
|
||||||
|
onClick={() => handleResetPassword(record)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Popconfirm
|
||||||
|
title="确定要删除此用户吗?"
|
||||||
|
onConfirm={() => handleDelete(record.userId)}
|
||||||
|
okText="确定"
|
||||||
|
cancelText="取消"
|
||||||
|
>
|
||||||
|
<Tooltip title="删除">
|
||||||
|
<Button type="text" danger icon={<DeleteOutlined />} />
|
||||||
|
</Tooltip>
|
||||||
|
</Popconfirm>
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
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={fetchUsers}>刷新</Button>
|
||||||
|
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
|
||||||
|
添加用户
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={users}
|
||||||
|
rowKey="userId"
|
||||||
|
loading={loading}
|
||||||
|
pagination={{
|
||||||
|
...pagination,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showQuickJumper: true,
|
||||||
|
showTotal: (total) => `共 ${total} 条记录`
|
||||||
|
}}
|
||||||
|
onChange={(newPagination) => {
|
||||||
|
setPagination(prev => ({ ...prev, ...newPagination }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title={editingUser ? '编辑用户' : '添加用户'}
|
||||||
|
open={modalVisible}
|
||||||
|
onCancel={() => setModalVisible(false)}
|
||||||
|
footer={null}
|
||||||
|
width={500}
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={handleSubmit}
|
||||||
|
style={{ marginTop: '20px' }}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
name="username"
|
||||||
|
label="用户名"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入用户名' },
|
||||||
|
{ min: 3, max: 20, message: '用户名长度必须在3-20个字符之间' }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入用户名" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="realName"
|
||||||
|
label="真实姓名"
|
||||||
|
rules={[{ required: true, message: '请输入真实姓名' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入真实姓名" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="email"
|
||||||
|
label="邮箱"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入邮箱' },
|
||||||
|
{ type: 'email', message: '请输入有效的邮箱地址' }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder="请输入邮箱" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item name="phone" label="手机号">
|
||||||
|
<Input placeholder="请输入手机号" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="roleIds"
|
||||||
|
label="角色"
|
||||||
|
rules={[{ required: true, message: '请选择角色' }]}
|
||||||
|
>
|
||||||
|
<Select mode="multiple" placeholder="请选择角色">
|
||||||
|
{roles.map(role => (
|
||||||
|
<Option key={role.roleId} value={role.roleId}>
|
||||||
|
{role.roleName}
|
||||||
|
</Option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{!editingUser && (
|
||||||
|
<Form.Item
|
||||||
|
name="password"
|
||||||
|
label="初始密码"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入初始密码' },
|
||||||
|
{ min: 6, message: '密码长度不能少于6个字符' }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password placeholder="请输入初始密码" />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Form.Item name="status" label="状态">
|
||||||
|
<Select placeholder="请选择状态">
|
||||||
|
<Option value="active">正常</Option>
|
||||||
|
<Option value="inactive">禁用</Option>
|
||||||
|
<Option value="locked">锁定</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
{editingUser && (
|
||||||
|
<Form.Item
|
||||||
|
name="newPassword"
|
||||||
|
label="新密码"
|
||||||
|
rules={[
|
||||||
|
{ min: 6, message: '密码长度不能少于6个字符' }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password placeholder="留空则不修改密码" />
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
|
||||||
|
<Space>
|
||||||
|
<Button onClick={() => setModalVisible(false)}>取消</Button>
|
||||||
|
<Button type="primary" htmlType="submit" loading={loading}>
|
||||||
|
{editingUser ? '更新' : '创建'}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title={`重置密码 - ${passwordUser?.username}`}
|
||||||
|
open={passwordModalVisible}
|
||||||
|
onCancel={() => setPasswordModalVisible(false)}
|
||||||
|
footer={null}
|
||||||
|
width={400}
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={passwordForm}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={handleResetPasswordSubmit}
|
||||||
|
style={{ marginTop: '20px' }}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
name="newPassword"
|
||||||
|
label="新密码"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入新密码' },
|
||||||
|
{ min: 6, message: '密码长度不能少于6个字符' }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input.Password placeholder="请输入新密码" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
|
||||||
|
<Space>
|
||||||
|
<Button onClick={() => setPasswordModalVisible(false)}>取消</Button>
|
||||||
|
<Button type="primary" htmlType="submit" loading={loading}>
|
||||||
|
重置
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title={`设置头像 - ${avatarUser?.username}`}
|
||||||
|
open={avatarModalVisible}
|
||||||
|
onCancel={() => setAvatarModalVisible(false)}
|
||||||
|
footer={null}
|
||||||
|
width={400}
|
||||||
|
>
|
||||||
|
<div style={{ textAlign: 'center', padding: '20px 0' }}>
|
||||||
|
<div style={{ marginBottom: '24px' }}>
|
||||||
|
<Badge dot={!!avatarUser?.avatar} color="green" offset={[-5, 35]}>
|
||||||
|
<Avatar
|
||||||
|
size={120}
|
||||||
|
icon={!avatarUser?.avatar && <UserOutlined />}
|
||||||
|
src={getAvatarUrl(avatarUser)}
|
||||||
|
style={{
|
||||||
|
backgroundColor: avatarUser?.avatar ? 'transparent' : '#1890ff',
|
||||||
|
border: '1px solid #f0f0f0'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/jpeg,image/png,image/gif,image/webp"
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
ref={fileInputRef}
|
||||||
|
onChange={handleAvatarUpload}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<CameraOutlined />}
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
loading={uploadLoading}
|
||||||
|
block
|
||||||
|
>
|
||||||
|
{avatarUser?.avatar ? '更换头像' : '上传头像'}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{avatarUser?.avatar && (
|
||||||
|
<Button
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={handleAvatarDelete}
|
||||||
|
block
|
||||||
|
>
|
||||||
|
删除头像
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
|
||||||
|
<div style={{ marginTop: '16px', color: '#999', fontSize: '12px' }}>
|
||||||
|
支持 JPG、PNG、GIF、WebP 格式,大小不超过 5MB
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserManagement;
|
||||||
Reference in New Issue
Block a user