feat(用户管理): 实现用户注册审核功能
添加用户注册审核流程,包括以下变更: - 用户注册后状态为 pending 待审核 - 管理员可批准或拒绝注册申请 - 待审核用户无法登录系统 - 用户管理页面添加审核操作和状态筛选 - 添加数据库迁移脚本支持 pending 状态 - 更新前后端相关接口和页面逻辑
This commit is contained in:
@@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
## 代码仓库
|
## 代码仓库
|
||||||
|
|
||||||
- **GitHub**: https://github.com/gituib/idc_assest
|
|
||||||
- **Gitee**: https://gitee.com/zhang96110/idc_assest
|
- **Gitee**: https://gitee.com/zhang96110/idc_assest
|
||||||
|
|
||||||
## 系统概述
|
## 系统概述
|
||||||
@@ -119,7 +118,7 @@ jigui/
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 克隆项目
|
# 克隆项目
|
||||||
git clone https://github.com/gituib/idc_assest.git
|
git clone https://gitee.com/zhang96110/idc_assest.git
|
||||||
cd idc_assest
|
cd idc_assest
|
||||||
|
|
||||||
# 运行交互式安装脚本
|
# 运行交互式安装脚本
|
||||||
@@ -192,7 +191,7 @@ node install.js
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. 克隆项目
|
# 1. 克隆项目
|
||||||
git clone https://github.com/gituib/idc_assest.git
|
git clone https://gitee.com/zhang96110/idc_assest.git
|
||||||
cd idc_assest
|
cd idc_assest
|
||||||
|
|
||||||
# 2. 安装后端依赖
|
# 2. 安装后端依赖
|
||||||
@@ -221,129 +220,6 @@ npm run dev
|
|||||||
- 后端API:http://localhost:8000/api
|
- 后端API:http://localhost:8000/api
|
||||||
- 健康检查:http://localhost:8000/health
|
- 健康检查:http://localhost:8000/health
|
||||||
|
|
||||||
## 安装部署脚本
|
|
||||||
|
|
||||||
项目提供多个实用脚本,简化安装、更新、卸载等操作:
|
|
||||||
|
|
||||||
### 脚本列表
|
|
||||||
|
|
||||||
| 脚本 | 命令 | 功能说明 |
|
|
||||||
|------|------|----------|
|
|
||||||
| **install.js** | `npm run deploy` / `node install.js` | 交互式安装部署脚本 |
|
|
||||||
| **update.js** | `npm run update` / `node update.js` | 一键更新脚本 |
|
|
||||||
| **uninstall.js** | `node uninstall.js` | 卸载清理脚本 |
|
|
||||||
| **check.js** | `node check.js` | 环境检查脚本 |
|
|
||||||
| **modify.js** | `node modify.js` | 配置修改脚本 |
|
|
||||||
|
|
||||||
### install.js - 交互式安装部署
|
|
||||||
|
|
||||||
**功能特性:**
|
|
||||||
- ✅ 自动检测 Node.js、npm、PM2、Nginx 环境
|
|
||||||
- ✅ **Linux 支持自动安装 Node.js**(交互式)
|
|
||||||
- ✅ 交互式配置数据库(SQLite/MySQL)
|
|
||||||
- ✅ 交互式选择运行环境(development/production)
|
|
||||||
- ✅ 交互式选择前端部署方式(Nginx/PM2 serve)
|
|
||||||
- ✅ 自动安装项目依赖
|
|
||||||
- ✅ 自动初始化数据库
|
|
||||||
- ✅ 自动构建前端项目
|
|
||||||
- ✅ 使用 PM2 启动和管理服务
|
|
||||||
|
|
||||||
**使用方式:**
|
|
||||||
```bash
|
|
||||||
# 方式一:使用 npm 命令
|
|
||||||
npm run deploy
|
|
||||||
|
|
||||||
# 方式二:直接运行脚本
|
|
||||||
node install.js
|
|
||||||
```
|
|
||||||
|
|
||||||
### update.js - 一键更新
|
|
||||||
|
|
||||||
**功能特性:**
|
|
||||||
- ✅ 自动备份数据
|
|
||||||
- ✅ 拉取最新代码
|
|
||||||
- ✅ 更新前后端依赖
|
|
||||||
- ✅ 重建前端项目
|
|
||||||
- ✅ 重启服务
|
|
||||||
|
|
||||||
**使用方式:**
|
|
||||||
```bash
|
|
||||||
# 方式一:使用 npm 命令
|
|
||||||
npm run update
|
|
||||||
|
|
||||||
# 方式二:直接运行脚本
|
|
||||||
node update.js
|
|
||||||
```
|
|
||||||
|
|
||||||
### uninstall.js - 卸载清理
|
|
||||||
|
|
||||||
**功能特性:**
|
|
||||||
- ✅ 停止 PM2 服务
|
|
||||||
- ✅ 删除 PM2 进程配置
|
|
||||||
- ✅ 清理 Nginx 配置(可选)
|
|
||||||
- ✅ 备份数据(可选)
|
|
||||||
- ✅ 清理日志文件(可选)
|
|
||||||
|
|
||||||
**使用方式:**
|
|
||||||
```bash
|
|
||||||
node uninstall.js
|
|
||||||
```
|
|
||||||
|
|
||||||
**卸载流程:**
|
|
||||||
```
|
|
||||||
▶ 停止服务
|
|
||||||
✓ 已停止 idc-backend
|
|
||||||
✓ 已停止 idc-frontend
|
|
||||||
|
|
||||||
▶ 删除 PM2 配置
|
|
||||||
✓ 已删除 PM2 进程
|
|
||||||
|
|
||||||
▶ 清理 Nginx 配置
|
|
||||||
是否删除 Nginx 配置? (y/N): n
|
|
||||||
|
|
||||||
▶ 数据备份
|
|
||||||
是否备份数据库? (Y/n): y
|
|
||||||
✓ 数据库已备份到 backup/database_20240205_143022.sql
|
|
||||||
|
|
||||||
▶ 清理完成
|
|
||||||
✓ 卸载完成,感谢使用!
|
|
||||||
```
|
|
||||||
|
|
||||||
**注意事项:**
|
|
||||||
- 卸载前建议备份数据
|
|
||||||
- 默认保留数据库文件,可手动删除
|
|
||||||
- 上传的文件(avatars、uploads)需手动清理
|
|
||||||
|
|
||||||
### check.js - 环境检查
|
|
||||||
|
|
||||||
**功能特性:**
|
|
||||||
- ✅ 检查 Node.js 版本
|
|
||||||
- ✅ 检查 npm 版本
|
|
||||||
- ✅ 检查 PM2 安装状态
|
|
||||||
- ✅ 检查 Nginx 安装状态
|
|
||||||
- ✅ 检查端口占用情况
|
|
||||||
- ✅ 检查磁盘空间
|
|
||||||
|
|
||||||
**使用方式:**
|
|
||||||
```bash
|
|
||||||
node check.js
|
|
||||||
```
|
|
||||||
|
|
||||||
### modify.js - 配置修改
|
|
||||||
|
|
||||||
**功能特性:**
|
|
||||||
- ✅ 修改数据库配置
|
|
||||||
- ✅ 修改服务端口
|
|
||||||
- ✅ 修改运行环境
|
|
||||||
- ✅ 修改前端部署方式
|
|
||||||
|
|
||||||
**使用方式:**
|
|
||||||
```bash
|
|
||||||
node modify.js
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 更新升级
|
## 更新升级
|
||||||
|
|
||||||
### 一键更新(推荐)⭐
|
### 一键更新(推荐)⭐
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const User = sequelize.define('User', {
|
|||||||
allowNull: true
|
allowNull: true
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
type: DataTypes.ENUM('active', 'inactive', 'locked'),
|
type: DataTypes.ENUM('active', 'inactive', 'locked', 'pending'),
|
||||||
defaultValue: 'active'
|
defaultValue: 'active'
|
||||||
},
|
},
|
||||||
lastLoginTime: {
|
lastLoginTime: {
|
||||||
|
|||||||
+44
-20
@@ -58,12 +58,12 @@ router.post('/register', async (req, res) => {
|
|||||||
email,
|
email,
|
||||||
phone,
|
phone,
|
||||||
realName: realName || username,
|
realName: realName || username,
|
||||||
status: 'active'
|
status: isFirstUser ? 'active' : 'pending'
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isFirstUser) {
|
if (isFirstUser) {
|
||||||
let adminRole = await Role.findOne({ where: { roleCode: 'admin' } });
|
let adminRole = await Role.findOne({ where: { roleCode: 'admin' } });
|
||||||
|
|
||||||
if (!adminRole) {
|
if (!adminRole) {
|
||||||
adminRole = await Role.create({
|
adminRole = await Role.create({
|
||||||
roleId: 'role_admin',
|
roleId: 'role_admin',
|
||||||
@@ -74,37 +74,53 @@ router.post('/register', async (req, res) => {
|
|||||||
permissions: []
|
permissions: []
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await UserRole.create({
|
await UserRole.create({
|
||||||
UserId: user.userId,
|
UserId: user.userId,
|
||||||
RoleId: adminRole.roleId
|
RoleId: adminRole.roleId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const token = generateToken(user);
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
message: '注册成功,已为您分配管理员权限',
|
||||||
|
data: {
|
||||||
|
user: {
|
||||||
|
userId: user.userId,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
realName: user.realName
|
||||||
|
},
|
||||||
|
token,
|
||||||
|
isFirstUser: true
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
const defaultRole = await Role.findOne({ where: { roleCode: 'viewer' } });
|
const defaultRole = await Role.findOne({ where: { roleCode: 'viewer' } });
|
||||||
|
|
||||||
if (defaultRole) {
|
if (defaultRole) {
|
||||||
await UserRole.create({
|
await UserRole.create({
|
||||||
UserId: user.userId,
|
UserId: user.userId,
|
||||||
RoleId: defaultRole.roleId
|
RoleId: defaultRole.roleId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
message: '注册成功,请等待管理员审核',
|
||||||
|
data: {
|
||||||
|
user: {
|
||||||
|
userId: user.userId,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
realName: user.realName
|
||||||
|
},
|
||||||
|
isFirstUser: false,
|
||||||
|
pendingApproval: true
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
} catch (error) {
|
||||||
console.error('注册错误:', error);
|
console.error('注册错误:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
@@ -148,6 +164,14 @@ router.post('/login', async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (user.status === 'pending') {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
code: 'PENDING_APPROVAL',
|
||||||
|
message: '账户待审核,请联系管理员激活'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const isPasswordValid = await bcrypt.compare(password, user.password);
|
const isPasswordValid = await bcrypt.compare(password, user.password);
|
||||||
if (!isPasswordValid) {
|
if (!isPasswordValid) {
|
||||||
user.loginCount = (user.loginCount || 0) + 1;
|
user.loginCount = (user.loginCount || 0) + 1;
|
||||||
|
|||||||
@@ -465,4 +465,82 @@ router.delete('/:userId/avatar', authMiddleware, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.put('/:userId/approve', 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.status !== 'pending') {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '该用户不在待审核状态'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
user.status = 'active';
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: '用户审核通过',
|
||||||
|
data: {
|
||||||
|
userId: user.userId,
|
||||||
|
username: user.username,
|
||||||
|
status: user.status
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('审核用户错误:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '审核用户失败'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put('/:userId/reject', 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.status !== 'pending') {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '该用户不在待审核状态'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
user.status = 'inactive';
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: '已拒绝该用户的注册申请',
|
||||||
|
data: {
|
||||||
|
userId: user.userId,
|
||||||
|
username: user.username,
|
||||||
|
status: user.status
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('拒绝用户错误:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '操作失败'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
const { sequelize } = require('../db');
|
||||||
|
|
||||||
|
async function migrate() {
|
||||||
|
try {
|
||||||
|
console.log('开始迁移:添加 pending 状态到用户表...');
|
||||||
|
|
||||||
|
const queryInterface = sequelize.getQueryInterface();
|
||||||
|
const dialect = sequelize.getDialect();
|
||||||
|
|
||||||
|
if (dialect === 'sqlite') {
|
||||||
|
// SQLite 不支持 ALTER COLUMN,需要重建表
|
||||||
|
console.log('检测到 SQLite 数据库,使用重建表方式迁移...');
|
||||||
|
|
||||||
|
// 1. 创建新表
|
||||||
|
await queryInterface.createTable('users_new', {
|
||||||
|
userId: {
|
||||||
|
type: sequelize.Sequelize.STRING,
|
||||||
|
primaryKey: true,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
username: {
|
||||||
|
type: sequelize.Sequelize.STRING,
|
||||||
|
allowNull: false,
|
||||||
|
unique: true
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
type: sequelize.Sequelize.STRING,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
type: sequelize.Sequelize.STRING,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
phone: {
|
||||||
|
type: sequelize.Sequelize.STRING,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
realName: {
|
||||||
|
type: sequelize.Sequelize.STRING,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
avatar: {
|
||||||
|
type: sequelize.Sequelize.STRING,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: sequelize.Sequelize.ENUM('active', 'inactive', 'locked', 'pending'),
|
||||||
|
defaultValue: 'active'
|
||||||
|
},
|
||||||
|
lastLoginTime: {
|
||||||
|
type: sequelize.Sequelize.DATE,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
lastLoginIp: {
|
||||||
|
type: sequelize.Sequelize.STRING,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
loginCount: {
|
||||||
|
type: sequelize.Sequelize.INTEGER,
|
||||||
|
defaultValue: 0
|
||||||
|
},
|
||||||
|
remark: {
|
||||||
|
type: sequelize.Sequelize.TEXT,
|
||||||
|
allowNull: true
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
type: sequelize.Sequelize.DATE,
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
updatedAt: {
|
||||||
|
type: sequelize.Sequelize.DATE,
|
||||||
|
allowNull: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. 复制数据
|
||||||
|
await sequelize.query(`
|
||||||
|
INSERT INTO users_new (
|
||||||
|
userId, username, password, email, phone, realName, avatar,
|
||||||
|
status, lastLoginTime, lastLoginIp, loginCount, remark, createdAt, updatedAt
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
userId, username, password, email, phone, realName, avatar,
|
||||||
|
status, lastLoginTime, lastLoginIp, loginCount, remark, createdAt, updatedAt
|
||||||
|
FROM users
|
||||||
|
`);
|
||||||
|
|
||||||
|
// 3. 删除旧表
|
||||||
|
await queryInterface.dropTable('users');
|
||||||
|
|
||||||
|
// 4. 重命名新表
|
||||||
|
await queryInterface.renameTable('users_new', 'users');
|
||||||
|
|
||||||
|
// 5. 重新创建索引
|
||||||
|
await queryInterface.addIndex('users', ['status']);
|
||||||
|
await queryInterface.addIndex('users', ['username']);
|
||||||
|
await queryInterface.addIndex('users', ['email']);
|
||||||
|
|
||||||
|
console.log('SQLite 迁移完成');
|
||||||
|
} else if (dialect === 'mysql') {
|
||||||
|
// MySQL 可以直接修改 ENUM
|
||||||
|
console.log('检测到 MySQL 数据库,直接修改 ENUM...');
|
||||||
|
await sequelize.query(`
|
||||||
|
ALTER TABLE users
|
||||||
|
MODIFY COLUMN status ENUM('active', 'inactive', 'locked', 'pending') DEFAULT 'active'
|
||||||
|
`);
|
||||||
|
console.log('MySQL 迁移完成');
|
||||||
|
} else {
|
||||||
|
console.log(`不支持的数据库类型: ${dialect},请手动迁移`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('迁移成功完成!');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('迁移失败:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
migrate();
|
||||||
@@ -95,7 +95,9 @@ export const userAPI = {
|
|||||||
headers: { 'Content-Type': 'multipart/form-data' }
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
deleteAvatar: (userId) => api.delete(`/users/${userId}/avatar`)
|
deleteAvatar: (userId) => api.delete(`/users/${userId}/avatar`),
|
||||||
|
approve: (userId) => api.put(`/users/${userId}/approve`),
|
||||||
|
reject: (userId) => api.put(`/users/${userId}/reject`)
|
||||||
};
|
};
|
||||||
|
|
||||||
export const roleAPI = {
|
export const roleAPI = {
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export const AuthProvider = ({ children }) => {
|
|||||||
setUser(userData);
|
setUser(userData);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
return { success: false, message: response.message };
|
return { success: false, message: response.message, code: response.code };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { success: false, message: error };
|
return { success: false, message: error };
|
||||||
}
|
}
|
||||||
@@ -101,12 +101,14 @@ export const AuthProvider = ({ children }) => {
|
|||||||
try {
|
try {
|
||||||
const response = await authAPI.register(userData);
|
const response = await authAPI.register(userData);
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
const { token: newToken, user: newUser } = response.data;
|
const { token: newToken, user: newUser, isFirstUser, pendingApproval } = response.data;
|
||||||
localStorage.setItem('token', newToken);
|
if (newToken) {
|
||||||
localStorage.setItem('user', JSON.stringify(newUser));
|
localStorage.setItem('token', newToken);
|
||||||
setToken(newToken);
|
localStorage.setItem('user', JSON.stringify(newUser));
|
||||||
setUser(newUser);
|
setToken(newToken);
|
||||||
return { success: true, isFirstUser: response.data.isFirstUser };
|
setUser(newUser);
|
||||||
|
}
|
||||||
|
return { success: true, isFirstUser, pendingApproval };
|
||||||
}
|
}
|
||||||
return { success: false, message: response.message };
|
return { success: false, message: response.message };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -48,7 +48,11 @@ const Login = () => {
|
|||||||
message.success('登录成功');
|
message.success('登录成功');
|
||||||
navigate('/');
|
navigate('/');
|
||||||
} else {
|
} else {
|
||||||
message.error(result.message || '登录失败');
|
if (result.code === 'PENDING_APPROVAL') {
|
||||||
|
message.warning('账户待审核,请联系管理员激活');
|
||||||
|
} else {
|
||||||
|
message.error(result.message || '登录失败');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error(error || '登录失败');
|
message.error(error || '登录失败');
|
||||||
@@ -91,8 +95,16 @@ const Login = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
message.success(result.isFirstUser ? '注册成功,已为您创建管理员账户' : '注册成功');
|
if (result.isFirstUser) {
|
||||||
navigate('/');
|
message.success('注册成功,已为您创建管理员账户');
|
||||||
|
navigate('/');
|
||||||
|
} else if (result.pendingApproval) {
|
||||||
|
message.success('注册成功,请等待管理员审核');
|
||||||
|
setRegisterMode(false);
|
||||||
|
} else {
|
||||||
|
message.success('注册成功');
|
||||||
|
navigate('/');
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
message.error(result.message || '注册失败');
|
message.error(result.message || '注册失败');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
||||||
import { Card, Table, Button, Space, Modal, Form, Input, Select, message, Tag, Popconfirm, Avatar, Tooltip, Badge } from 'antd';
|
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 { PlusOutlined, EditOutlined, DeleteOutlined, UserOutlined, ReloadOutlined, LockOutlined, CameraOutlined, CheckOutlined, CloseOutlined } from '@ant-design/icons';
|
||||||
import { userAPI, roleAPI } from '../api';
|
import { userAPI, roleAPI } from '../api';
|
||||||
|
|
||||||
const { Option } = Select;
|
const { Option } = Select;
|
||||||
@@ -17,6 +17,7 @@ const UserManagement = () => {
|
|||||||
const [passwordUser, setPasswordUser] = useState(null);
|
const [passwordUser, setPasswordUser] = useState(null);
|
||||||
const [avatarUser, setAvatarUser] = useState(null);
|
const [avatarUser, setAvatarUser] = useState(null);
|
||||||
const [uploadLoading, setUploadLoading] = useState(false);
|
const [uploadLoading, setUploadLoading] = useState(false);
|
||||||
|
const [activeTab, setActiveTab] = useState('all');
|
||||||
const [form] = Form.useForm();
|
const [form] = Form.useForm();
|
||||||
const [passwordForm] = Form.useForm();
|
const [passwordForm] = Form.useForm();
|
||||||
const fileInputRef = useRef(null);
|
const fileInputRef = useRef(null);
|
||||||
@@ -29,10 +30,14 @@ const UserManagement = () => {
|
|||||||
const fetchUsers = useCallback(async () => {
|
const fetchUsers = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await userAPI.list({
|
const params = {
|
||||||
page: pagination.current,
|
page: pagination.current,
|
||||||
pageSize: pagination.pageSize
|
pageSize: pagination.pageSize
|
||||||
});
|
};
|
||||||
|
if (activeTab !== 'all') {
|
||||||
|
params.status = activeTab;
|
||||||
|
}
|
||||||
|
const response = await userAPI.list(params);
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
setUsers(response.data.users);
|
setUsers(response.data.users);
|
||||||
setPagination(prev => ({ ...prev, total: response.data.total }));
|
setPagination(prev => ({ ...prev, total: response.data.total }));
|
||||||
@@ -42,7 +47,7 @@ const UserManagement = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [pagination.current, pagination.pageSize]);
|
}, [pagination.current, pagination.pageSize, activeTab]);
|
||||||
|
|
||||||
const fetchRoles = useCallback(async () => {
|
const fetchRoles = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -207,7 +212,8 @@ const UserManagement = () => {
|
|||||||
const colors = {
|
const colors = {
|
||||||
active: 'green',
|
active: 'green',
|
||||||
inactive: 'red',
|
inactive: 'red',
|
||||||
locked: 'orange'
|
locked: 'orange',
|
||||||
|
pending: 'blue'
|
||||||
};
|
};
|
||||||
return colors[status] || 'default';
|
return colors[status] || 'default';
|
||||||
};
|
};
|
||||||
@@ -216,11 +222,40 @@ const UserManagement = () => {
|
|||||||
const texts = {
|
const texts = {
|
||||||
active: '正常',
|
active: '正常',
|
||||||
inactive: '禁用',
|
inactive: '禁用',
|
||||||
locked: '锁定'
|
locked: '锁定',
|
||||||
|
pending: '待审核'
|
||||||
};
|
};
|
||||||
return texts[status] || status;
|
return texts[status] || status;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleApprove = useCallback(async (userId) => {
|
||||||
|
try {
|
||||||
|
const response = await userAPI.approve(userId);
|
||||||
|
if (response.success) {
|
||||||
|
message.success('审核通过');
|
||||||
|
fetchUsers();
|
||||||
|
} else {
|
||||||
|
message.error(response.message || '审核失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('审核失败');
|
||||||
|
}
|
||||||
|
}, [fetchUsers]);
|
||||||
|
|
||||||
|
const handleReject = useCallback(async (userId) => {
|
||||||
|
try {
|
||||||
|
const response = await userAPI.reject(userId);
|
||||||
|
if (response.success) {
|
||||||
|
message.success('已拒绝该用户的注册申请');
|
||||||
|
fetchUsers();
|
||||||
|
} else {
|
||||||
|
message.error(response.message || '操作失败');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error('操作失败');
|
||||||
|
}
|
||||||
|
}, [fetchUsers]);
|
||||||
|
|
||||||
const getAvatarUrl = (user) => {
|
const getAvatarUrl = (user) => {
|
||||||
if (!user?.avatar) return null;
|
if (!user?.avatar) return null;
|
||||||
return user.avatar;
|
return user.avatar;
|
||||||
@@ -302,52 +337,88 @@ const UserManagement = () => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{title: '操作',
|
{
|
||||||
|
title: '操作',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
render: (_, record) => (
|
render: (_, record) => (
|
||||||
<Space size="small">
|
<Space size="small">
|
||||||
<Tooltip title="编辑">
|
{record.status === 'pending' ? (
|
||||||
<Button
|
<>
|
||||||
type="text"
|
<Popconfirm
|
||||||
icon={<EditOutlined />}
|
title="确定要通过该用户的注册申请吗?"
|
||||||
onClick={() => handleEdit(record)}
|
onConfirm={() => handleApprove(record.userId)}
|
||||||
/>
|
okText="通过"
|
||||||
</Tooltip>
|
cancelText="取消"
|
||||||
<Tooltip title="重置密码">
|
>
|
||||||
<Button
|
<Tooltip title="通过">
|
||||||
type="text"
|
<Button
|
||||||
icon={<LockOutlined />}
|
type="text"
|
||||||
onClick={() => handleResetPassword(record)}
|
icon={<CheckOutlined />}
|
||||||
/>
|
style={{ color: '#52c41a' }}
|
||||||
</Tooltip>
|
/>
|
||||||
<Popconfirm
|
</Tooltip>
|
||||||
title={record.status === 'locked' ? '确定要解锁此用户吗?' : '确定要锁定此用户吗?'}
|
</Popconfirm>
|
||||||
onConfirm={() => handleLockUnlock(record)}
|
<Popconfirm
|
||||||
okText="确定"
|
title="确定要拒绝该用户的注册申请吗?"
|
||||||
cancelText="取消"
|
onConfirm={() => handleReject(record.userId)}
|
||||||
>
|
okText="拒绝"
|
||||||
<Tooltip title={record.status === 'locked' ? '解锁' : '锁定'}>
|
cancelText="取消"
|
||||||
<Button
|
>
|
||||||
type="text"
|
<Tooltip title="拒绝">
|
||||||
icon={record.status === 'locked' ? <ReloadOutlined /> : <LockOutlined />}
|
<Button
|
||||||
style={{ color: record.status === 'locked' ? '#52c41a' : '#faad14' }}
|
type="text"
|
||||||
/>
|
icon={<CloseOutlined />}
|
||||||
</Tooltip>
|
style={{ color: '#ff4d4f' }}
|
||||||
</Popconfirm>
|
/>
|
||||||
<Popconfirm
|
</Tooltip>
|
||||||
title="确定要删除此用户吗?"
|
</Popconfirm>
|
||||||
onConfirm={() => handleDelete(record.userId)}
|
</>
|
||||||
okText="确定"
|
) : (
|
||||||
cancelText="取消"
|
<>
|
||||||
>
|
<Tooltip title="编辑">
|
||||||
<Tooltip title="删除">
|
<Button
|
||||||
<Button type="text" danger icon={<DeleteOutlined />} />
|
type="text"
|
||||||
</Tooltip>
|
icon={<EditOutlined />}
|
||||||
</Popconfirm>
|
onClick={() => handleEdit(record)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="重置密码">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<LockOutlined />}
|
||||||
|
onClick={() => handleResetPassword(record)}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
<Popconfirm
|
||||||
|
title={record.status === 'locked' ? '确定要解锁此用户吗?' : '确定要锁定此用户吗?'}
|
||||||
|
onConfirm={() => handleLockUnlock(record)}
|
||||||
|
okText="确定"
|
||||||
|
cancelText="取消"
|
||||||
|
>
|
||||||
|
<Tooltip title={record.status === 'locked' ? '解锁' : '锁定'}>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={record.status === 'locked' ? <ReloadOutlined /> : <LockOutlined />}
|
||||||
|
style={{ color: record.status === 'locked' ? '#52c41a' : '#faad14' }}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Popconfirm>
|
||||||
|
<Popconfirm
|
||||||
|
title="确定要删除此用户吗?"
|
||||||
|
onConfirm={() => handleDelete(record.userId)}
|
||||||
|
okText="确定"
|
||||||
|
cancelText="取消"
|
||||||
|
>
|
||||||
|
<Tooltip title="删除">
|
||||||
|
<Button type="text" danger icon={<DeleteOutlined />} />
|
||||||
|
</Tooltip>
|
||||||
|
</Popconfirm>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
], [handleAvatarClick, handleEdit, handleResetPassword, handleDelete, handleLockUnlock]);
|
], [handleAvatarClick, handleEdit, handleResetPassword, handleDelete, handleLockUnlock, handleApprove, handleReject]);
|
||||||
|
|
||||||
const pageHeaderStyle = {
|
const pageHeaderStyle = {
|
||||||
marginBottom: '24px',
|
marginBottom: '24px',
|
||||||
@@ -454,7 +525,22 @@ const UserManagement = () => {
|
|||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card style={cardStyle} styles={{ header: cardHeadStyle, body: { padding: '20px 24px' } }}>
|
<Card
|
||||||
|
style={cardStyle}
|
||||||
|
styles={{ header: cardHeadStyle, body: { padding: '20px 24px' } }}
|
||||||
|
tabList={[
|
||||||
|
{ key: 'all', tab: '全部用户' },
|
||||||
|
{ key: 'pending', tab: '待审核' },
|
||||||
|
{ key: 'active', tab: '正常' },
|
||||||
|
{ key: 'locked', tab: '锁定' },
|
||||||
|
{ key: 'inactive', tab: '禁用' }
|
||||||
|
]}
|
||||||
|
activeTabKey={activeTab}
|
||||||
|
onTabChange={(key) => {
|
||||||
|
setActiveTab(key);
|
||||||
|
setPagination(prev => ({ ...prev, current: 1 }));
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Table
|
<Table
|
||||||
columns={tableColumns}
|
columns={tableColumns}
|
||||||
dataSource={users}
|
dataSource={users}
|
||||||
|
|||||||
Reference in New Issue
Block a user