feat: ✨ 添加用户管理页
This commit is contained in:
52
backend/src/admin-users/admin-users.controller.ts
Normal file
52
backend/src/admin-users/admin-users.controller.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Controller, Get, Post, Put, Delete, Body, Param, HttpCode, HttpStatus, UseGuards } from '@nestjs/common';
|
||||
import { AdminUsersService } from './admin-users.service';
|
||||
import { AdminRole } from '../entities/admin-user.entity';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
|
||||
@Controller('admin/admin-users')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class AdminUsersController {
|
||||
constructor(private adminUsersService: AdminUsersService) {}
|
||||
|
||||
@Get()
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async findAll() {
|
||||
return this.adminUsersService.findAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async findOne(@Param('id') id: string) {
|
||||
return this.adminUsersService.findOne(Number(id));
|
||||
}
|
||||
|
||||
@Post()
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
async create(@Body() createAdminUserDto: {
|
||||
username: string;
|
||||
password: string;
|
||||
role: AdminRole;
|
||||
}) {
|
||||
return this.adminUsersService.create(createAdminUserDto);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async update(
|
||||
@Param('id') id: string,
|
||||
@Body() updateAdminUserDto: {
|
||||
username?: string;
|
||||
password?: string;
|
||||
role?: AdminRole;
|
||||
},
|
||||
) {
|
||||
return this.adminUsersService.update(Number(id), updateAdminUserDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async remove(@Param('id') id: string) {
|
||||
await this.adminUsersService.remove(Number(id));
|
||||
return { message: '删除成功' };
|
||||
}
|
||||
}
|
||||
14
backend/src/admin-users/admin-users.module.ts
Normal file
14
backend/src/admin-users/admin-users.module.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { AdminUsersController } from './admin-users.controller';
|
||||
import { AdminUsersService } from './admin-users.service';
|
||||
import { AdminUser } from '../entities/admin-user.entity';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([AdminUser]), AuthModule],
|
||||
controllers: [AdminUsersController],
|
||||
providers: [AdminUsersService],
|
||||
exports: [AdminUsersService],
|
||||
})
|
||||
export class AdminUsersModule {}
|
||||
75
backend/src/admin-users/admin-users.service.ts
Normal file
75
backend/src/admin-users/admin-users.service.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { AdminUser, AdminRole } from '../entities/admin-user.entity';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
|
||||
@Injectable()
|
||||
export class AdminUsersService {
|
||||
constructor(
|
||||
@InjectRepository(AdminUser)
|
||||
private adminUserRepository: Repository<AdminUser>,
|
||||
) {}
|
||||
|
||||
async findAll(): Promise<AdminUser[]> {
|
||||
return this.adminUserRepository.find({
|
||||
select: ['id', 'username', 'role', 'createdAt', 'updatedAt'],
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
async findOne(id: number): Promise<AdminUser | null> {
|
||||
return this.adminUserRepository.findOne({
|
||||
where: { id },
|
||||
select: ['id', 'username', 'role', 'createdAt', 'updatedAt'],
|
||||
});
|
||||
}
|
||||
|
||||
async findByUsername(username: string): Promise<AdminUser | null> {
|
||||
return this.adminUserRepository.findOne({
|
||||
where: { username },
|
||||
});
|
||||
}
|
||||
|
||||
async create(createAdminUserDto: {
|
||||
username: string;
|
||||
password: string;
|
||||
role: AdminRole;
|
||||
}): Promise<AdminUser> {
|
||||
const hashedPassword = await bcrypt.hash(createAdminUserDto.password, 10);
|
||||
const adminUser = this.adminUserRepository.create({
|
||||
username: createAdminUserDto.username,
|
||||
passwordHash: hashedPassword,
|
||||
role: createAdminUserDto.role,
|
||||
});
|
||||
return this.adminUserRepository.save(adminUser);
|
||||
}
|
||||
|
||||
async update(id: number, updateAdminUserDto: {
|
||||
username?: string;
|
||||
password?: string;
|
||||
role?: AdminRole;
|
||||
}): Promise<AdminUser | null> {
|
||||
const adminUser = await this.findOne(id);
|
||||
if (!adminUser) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (updateAdminUserDto.username) {
|
||||
adminUser.username = updateAdminUserDto.username;
|
||||
}
|
||||
if (updateAdminUserDto.password) {
|
||||
adminUser.passwordHash = await bcrypt.hash(updateAdminUserDto.password, 10);
|
||||
}
|
||||
if (updateAdminUserDto.role) {
|
||||
adminUser.role = updateAdminUserDto.role;
|
||||
}
|
||||
|
||||
return this.adminUserRepository.save(adminUser);
|
||||
}
|
||||
|
||||
async remove(id: number): Promise<boolean> {
|
||||
const result = await this.adminUserRepository.delete(id);
|
||||
return (result.affected ?? 0) > 0;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { PlayerModule } from './player/player.module';
|
||||
import { AdminUsersModule } from './admin-users/admin-users.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -26,6 +27,7 @@ import { PlayerModule } from './player/player.module';
|
||||
}),
|
||||
AuthModule,
|
||||
PlayerModule,
|
||||
AdminUsersModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
|
||||
@@ -17,6 +17,6 @@ import { AdminUser } from '../entities/admin-user.entity';
|
||||
],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService],
|
||||
exports: [AuthService],
|
||||
exports: [AuthService, JwtModule],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
||||
30
backend/src/auth/guards/jwt-auth.guard.ts
Normal file
30
backend/src/auth/guards/jwt-auth.guard.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard implements CanActivate {
|
||||
constructor(private jwtService: JwtService) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const authHeader = request.headers.authorization;
|
||||
|
||||
if (!authHeader) {
|
||||
throw new UnauthorizedException('未提供认证令牌');
|
||||
}
|
||||
|
||||
const [type, token] = authHeader.split(' ');
|
||||
|
||||
if (type !== 'Bearer' || !token) {
|
||||
throw new UnauthorizedException('无效的认证令牌格式');
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await this.jwtService.verifyAsync(token);
|
||||
request.user = payload;
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw new UnauthorizedException('无效的认证令牌');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"antd": "^6.1.2",
|
||||
"antd": "6.1.3",
|
||||
"axios": "^1.13.2",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
MenuFoldOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
DashboardOutlined,
|
||||
UserOutlined,
|
||||
SettingOutlined,
|
||||
LogoutOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
@@ -30,7 +30,7 @@ const AdminLayout = () => {
|
||||
logout();
|
||||
message.success('登出成功');
|
||||
navigate('/admin/login');
|
||||
} catch (error) {
|
||||
} catch {
|
||||
message.error('登出失败');
|
||||
}
|
||||
};
|
||||
@@ -43,12 +43,12 @@ const AdminLayout = () => {
|
||||
},
|
||||
{
|
||||
key: 'user',
|
||||
icon: <UserOutlined />,
|
||||
label: '用户管理',
|
||||
icon: <SettingOutlined />,
|
||||
label: '系统管理',
|
||||
children: [
|
||||
{
|
||||
key: '/admin/users',
|
||||
label: '用户列表',
|
||||
label: '用户管理',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
249
frontend/src/pages/admin/AdminUsers.tsx
Normal file
249
frontend/src/pages/admin/AdminUsers.tsx
Normal file
@@ -0,0 +1,249 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Table,
|
||||
Button,
|
||||
Space,
|
||||
Modal,
|
||||
Form,
|
||||
Input,
|
||||
Select,
|
||||
message,
|
||||
Popconfirm,
|
||||
Tag,
|
||||
} from 'antd';
|
||||
import {
|
||||
PlusOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { adminUsersService, AdminRole } from '../../services/adminUsersService';
|
||||
import type { AdminUser, CreateAdminUserDto, UpdateAdminUserDto } from '../../services/adminUsersService';
|
||||
|
||||
const AdminUsers = () => {
|
||||
const [users, setUsers] = useState<AdminUser[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [editingUser, setEditingUser] = useState<AdminUser | null>(null);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const roleOptions = [
|
||||
{ label: '超级管理员', value: AdminRole.SUPER_ADMIN },
|
||||
{ label: '操作员', value: AdminRole.OPERATOR },
|
||||
{ label: '查看者', value: AdminRole.VIEWER },
|
||||
];
|
||||
|
||||
const roleLabels: Record<AdminRole, string> = {
|
||||
[AdminRole.SUPER_ADMIN]: '超级管理员',
|
||||
[AdminRole.OPERATOR]: '操作员',
|
||||
[AdminRole.VIEWER]: '查看者',
|
||||
};
|
||||
|
||||
const roleColors: Record<AdminRole, string> = {
|
||||
[AdminRole.SUPER_ADMIN]: 'red',
|
||||
[AdminRole.OPERATOR]: 'blue',
|
||||
[AdminRole.VIEWER]: 'green',
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers();
|
||||
}, []);
|
||||
|
||||
const fetchUsers = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await adminUsersService.findAll();
|
||||
setUsers(data);
|
||||
} catch (error) {
|
||||
message.error('获取用户列表失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
setEditingUser(null);
|
||||
form.resetFields();
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const handleEdit = (user: AdminUser) => {
|
||||
setEditingUser(user);
|
||||
form.setFieldsValue({
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
});
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
await adminUsersService.remove(id);
|
||||
message.success('删除成功');
|
||||
fetchUsers();
|
||||
} catch (error) {
|
||||
message.error('删除失败');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
|
||||
if (editingUser) {
|
||||
const updateData: UpdateAdminUserDto = {
|
||||
username: values.username,
|
||||
role: values.role,
|
||||
};
|
||||
if (values.password) {
|
||||
updateData.password = values.password;
|
||||
}
|
||||
await adminUsersService.update(editingUser.id, updateData);
|
||||
message.success('更新成功');
|
||||
} else {
|
||||
const createData: CreateAdminUserDto = {
|
||||
username: values.username,
|
||||
password: values.password,
|
||||
role: values.role,
|
||||
};
|
||||
await adminUsersService.create(createData);
|
||||
message.success('创建成功');
|
||||
}
|
||||
|
||||
setModalVisible(false);
|
||||
form.resetFields();
|
||||
fetchUsers();
|
||||
} catch (error) {
|
||||
message.error(editingUser ? '更新失败' : '创建失败');
|
||||
}
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '用户名',
|
||||
dataIndex: 'username',
|
||||
key: 'username',
|
||||
},
|
||||
{
|
||||
title: '角色',
|
||||
dataIndex: 'role',
|
||||
key: 'role',
|
||||
render: (role: AdminRole) => (
|
||||
<Tag color={roleColors[role]}>{roleLabels[role]}</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
render: (date: string) => new Date(date).toLocaleString('zh-CN'),
|
||||
},
|
||||
{
|
||||
title: '更新时间',
|
||||
dataIndex: 'updatedAt',
|
||||
key: 'updatedAt',
|
||||
render: (date: string) => new Date(date).toLocaleString('zh-CN'),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
render: (_: any, record: AdminUser) => (
|
||||
<Space>
|
||||
<Button
|
||||
type="link"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleEdit(record)}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定要删除这个用户吗?"
|
||||
onConfirm={() => handleDelete(record.id)}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button type="link" danger icon={<DeleteOutlined />}>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<h2 style={{ margin: 0 }}>用户管理</h2>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>
|
||||
添加用户
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={users}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title={editingUser ? '编辑用户' : '添加用户'}
|
||||
open={modalVisible}
|
||||
onOk={handleSubmit}
|
||||
onCancel={() => {
|
||||
setModalVisible(false);
|
||||
form.resetFields();
|
||||
}}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
label="用户名"
|
||||
name="username"
|
||||
rules={[
|
||||
{ required: true, message: '请输入用户名' },
|
||||
{ min: 3, message: '用户名至少3个字符' },
|
||||
{ max: 50, message: '用户名最多50个字符' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入用户名" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="密码"
|
||||
name="password"
|
||||
rules={editingUser ? [] : [
|
||||
{ required: true, message: '请输入密码' },
|
||||
{ min: 6, message: '密码至少6个字符' },
|
||||
]}
|
||||
extra={editingUser ? '留空则不修改密码' : ''}
|
||||
>
|
||||
<Input.Password placeholder={editingUser ? '留空则不修改密码' : '请输入密码'} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="角色"
|
||||
name="role"
|
||||
rules={[{ required: true, message: '请选择角色' }]}
|
||||
>
|
||||
<Select placeholder="请选择角色" options={roleOptions} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminUsers;
|
||||
@@ -5,6 +5,7 @@ import AdminAuthRoute from '../components/AdminAuthRoute';
|
||||
import PlayerAuthRoute from '../components/PlayerAuthRoute';
|
||||
import AdminLogin from '../pages/admin/AdminLogin';
|
||||
import AdminDashboard from '../pages/admin/AdminDashboard';
|
||||
import AdminUsers from '../pages/admin/AdminUsers';
|
||||
import PlayerLogin from '../pages/player/PlayerLogin';
|
||||
import PlayerDashboard from '../pages/player/PlayerDashboard';
|
||||
|
||||
@@ -30,6 +31,10 @@ const router = createBrowserRouter([
|
||||
path: 'dashboard',
|
||||
element: <AdminDashboard />,
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
element: <AdminUsers />,
|
||||
},
|
||||
{
|
||||
index: true,
|
||||
element: <Navigate to="/admin/dashboard" replace />,
|
||||
|
||||
54
frontend/src/services/adminUsersService.ts
Normal file
54
frontend/src/services/adminUsersService.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import api from '../utils/api';
|
||||
|
||||
export enum AdminRole {
|
||||
SUPER_ADMIN = 'super_admin',
|
||||
OPERATOR = 'operator',
|
||||
VIEWER = 'viewer',
|
||||
}
|
||||
|
||||
export interface AdminUser {
|
||||
id: number;
|
||||
username: string;
|
||||
role: AdminRole;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CreateAdminUserDto {
|
||||
username: string;
|
||||
password: string;
|
||||
role: AdminRole;
|
||||
}
|
||||
|
||||
export interface UpdateAdminUserDto {
|
||||
username?: string;
|
||||
password?: string;
|
||||
role?: AdminRole;
|
||||
}
|
||||
|
||||
export const adminUsersService = {
|
||||
async findAll(): Promise<AdminUser[]> {
|
||||
const response = await api.get<AdminUser[]>('/admin/admin-users');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async findOne(id: number): Promise<AdminUser> {
|
||||
const response = await api.get<AdminUser>(`/admin/admin-users/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async create(data: CreateAdminUserDto): Promise<AdminUser> {
|
||||
const response = await api.post<AdminUser>('/admin/admin-users', data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async update(id: number, data: UpdateAdminUserDto): Promise<AdminUser> {
|
||||
const response = await api.put<AdminUser>(`/admin/admin-users/${id}`, data);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
async remove(id: number): Promise<{ message: string }> {
|
||||
const response = await api.delete<{ message: string }>(`/admin/admin-users/${id}`);
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
@@ -15,5 +15,8 @@
|
||||
"devDependencies": {
|
||||
"commitizen": "^4.3.1",
|
||||
"cz-git": "^1.11.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user