feat: 添加用户管理页

This commit is contained in:
Stev_Wang
2026-01-03 19:25:56 +08:00
parent cb5088115a
commit a950d1d526
12 changed files with 491 additions and 7 deletions

View 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: '删除成功' };
}
}

View 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 {}

View 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;
}
}

View File

@@ -5,6 +5,7 @@ import { AppController } from './app.controller';
import { AppService } from './app.service'; import { AppService } from './app.service';
import { AuthModule } from './auth/auth.module'; import { AuthModule } from './auth/auth.module';
import { PlayerModule } from './player/player.module'; import { PlayerModule } from './player/player.module';
import { AdminUsersModule } from './admin-users/admin-users.module';
@Module({ @Module({
imports: [ imports: [
@@ -26,6 +27,7 @@ import { PlayerModule } from './player/player.module';
}), }),
AuthModule, AuthModule,
PlayerModule, PlayerModule,
AdminUsersModule,
], ],
controllers: [AppController], controllers: [AppController],
providers: [AppService], providers: [AppService],

View File

@@ -17,6 +17,6 @@ import { AdminUser } from '../entities/admin-user.entity';
], ],
controllers: [AuthController], controllers: [AuthController],
providers: [AuthService], providers: [AuthService],
exports: [AuthService], exports: [AuthService, JwtModule],
}) })
export class AuthModule {} export class AuthModule {}

View 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('无效的认证令牌');
}
}
}

View File

@@ -10,7 +10,7 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"antd": "^6.1.2", "antd": "6.1.3",
"axios": "^1.13.2", "axios": "^1.13.2",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",

View File

@@ -4,7 +4,7 @@ import {
MenuFoldOutlined, MenuFoldOutlined,
MenuUnfoldOutlined, MenuUnfoldOutlined,
DashboardOutlined, DashboardOutlined,
UserOutlined, SettingOutlined,
LogoutOutlined, LogoutOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { Outlet, useNavigate, useLocation } from 'react-router-dom'; import { Outlet, useNavigate, useLocation } from 'react-router-dom';
@@ -30,7 +30,7 @@ const AdminLayout = () => {
logout(); logout();
message.success('登出成功'); message.success('登出成功');
navigate('/admin/login'); navigate('/admin/login');
} catch (error) { } catch {
message.error('登出失败'); message.error('登出失败');
} }
}; };
@@ -43,12 +43,12 @@ const AdminLayout = () => {
}, },
{ {
key: 'user', key: 'user',
icon: <UserOutlined />, icon: <SettingOutlined />,
label: '用户管理', label: '系统管理',
children: [ children: [
{ {
key: '/admin/users', key: '/admin/users',
label: '用户列表', label: '用户管理',
}, },
], ],
}, },

View 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;

View File

@@ -5,6 +5,7 @@ import AdminAuthRoute from '../components/AdminAuthRoute';
import PlayerAuthRoute from '../components/PlayerAuthRoute'; import PlayerAuthRoute from '../components/PlayerAuthRoute';
import AdminLogin from '../pages/admin/AdminLogin'; import AdminLogin from '../pages/admin/AdminLogin';
import AdminDashboard from '../pages/admin/AdminDashboard'; import AdminDashboard from '../pages/admin/AdminDashboard';
import AdminUsers from '../pages/admin/AdminUsers';
import PlayerLogin from '../pages/player/PlayerLogin'; import PlayerLogin from '../pages/player/PlayerLogin';
import PlayerDashboard from '../pages/player/PlayerDashboard'; import PlayerDashboard from '../pages/player/PlayerDashboard';
@@ -30,6 +31,10 @@ const router = createBrowserRouter([
path: 'dashboard', path: 'dashboard',
element: <AdminDashboard />, element: <AdminDashboard />,
}, },
{
path: 'users',
element: <AdminUsers />,
},
{ {
index: true, index: true,
element: <Navigate to="/admin/dashboard" replace />, element: <Navigate to="/admin/dashboard" replace />,

View 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;
},
};

View File

@@ -15,5 +15,8 @@
"devDependencies": { "devDependencies": {
"commitizen": "^4.3.1", "commitizen": "^4.3.1",
"cz-git": "^1.11.0" "cz-git": "^1.11.0"
},
"dependencies": {
"@ant-design/icons": "^6.1.0"
} }
} }