Compare commits
6 Commits
2d8566132e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c962fac20e | ||
|
|
b2ca0d86f2 | ||
|
|
5b8999b188 | ||
|
|
a950d1d526 | ||
|
|
cb5088115a | ||
|
|
598cd3026b |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -58,14 +58,14 @@ Thumbs.db
|
|||||||
*.temp
|
*.temp
|
||||||
.cache/
|
.cache/
|
||||||
|
|
||||||
# Frontend specific
|
# Frontend specific (React + Vite)
|
||||||
frontend/dist/
|
frontend/dist/
|
||||||
frontend/node_modules/
|
frontend/node_modules/
|
||||||
frontend/.env
|
frontend/.env
|
||||||
frontend/.env.local
|
frontend/.env.local
|
||||||
frontend/.env.*.local
|
frontend/.env.*.local
|
||||||
|
|
||||||
# Backend specific
|
# Backend specific (NestJS)
|
||||||
backend/dist/
|
backend/dist/
|
||||||
backend/node_modules/
|
backend/node_modules/
|
||||||
backend/.env
|
backend/.env
|
||||||
@@ -73,7 +73,7 @@ backend/.env.local
|
|||||||
backend/.env.*.local
|
backend/.env.*.local
|
||||||
backend/test-game-server.js
|
backend/test-game-server.js
|
||||||
|
|
||||||
# Package manager files
|
# Package manager lock files
|
||||||
package-lock.json
|
package-lock.json
|
||||||
yarn.lock
|
yarn.lock
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
|
|||||||
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 { 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],
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
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"
|
"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",
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import { useAuthStore } from '../stores/authStore';
|
|||||||
|
|
||||||
const AdminAuthRoute = () => {
|
const AdminAuthRoute = () => {
|
||||||
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
||||||
|
const authType = useAuthStore((state) => state.authType);
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated || authType !== 'admin') {
|
||||||
return <Navigate to="/admin/login" replace />;
|
return <Navigate to="/admin/login" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import { useState } from 'react';
|
import { useState, useMemo } from 'react';
|
||||||
import { Layout, Menu, Button, theme } from 'antd';
|
import { Layout, Menu, Button, theme, App } from 'antd';
|
||||||
import {
|
import {
|
||||||
MenuFoldOutlined,
|
MenuFoldOutlined,
|
||||||
MenuUnfoldOutlined,
|
MenuUnfoldOutlined,
|
||||||
DashboardOutlined,
|
DashboardOutlined,
|
||||||
UserOutlined,
|
SettingOutlined,
|
||||||
LogoutOutlined,
|
LogoutOutlined,
|
||||||
|
SunOutlined,
|
||||||
|
MoonOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { useAuthStore } from '../stores/authStore';
|
import { useAuthStore } from '../stores/authStore';
|
||||||
|
import { useThemeStore } from '../stores/themeStore';
|
||||||
import { adminAuthService } from '../services/adminAuthService';
|
import { adminAuthService } from '../services/adminAuthService';
|
||||||
import { message } from 'antd';
|
|
||||||
|
|
||||||
const { Header, Sider, Content, Footer } = Layout;
|
const { Header, Sider, Content, Footer } = Layout;
|
||||||
|
|
||||||
@@ -23,15 +25,41 @@ const AdminLayout = () => {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const adminUser = useAuthStore((state) => state.adminUser);
|
const adminUser = useAuthStore((state) => state.adminUser);
|
||||||
const logout = useAuthStore((state) => state.logout);
|
const logout = useAuthStore((state) => state.logout);
|
||||||
|
const { message } = App.useApp();
|
||||||
|
const { themeMode, toggleTheme } = useThemeStore();
|
||||||
|
|
||||||
|
// 跟踪用户是否手动修改过 openKeys
|
||||||
|
const [userInteracted, setUserInteracted] = useState(false);
|
||||||
|
// 用户手动设置的 openKeys
|
||||||
|
const [userOpenKeys, setUserOpenKeys] = useState<string[]>([]);
|
||||||
|
|
||||||
|
// 根据当前路径计算应该展开的菜单
|
||||||
|
const defaultOpenKeys = useMemo(() => {
|
||||||
|
const path = location.pathname;
|
||||||
|
if (path.startsWith('/admin/users')) {
|
||||||
|
return ['user'];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}, [location.pathname]);
|
||||||
|
|
||||||
|
// 最终使用的 openKeys:如果用户手动修改过,使用用户的设置;否则使用默认值
|
||||||
|
const openKeys = useMemo(() => {
|
||||||
|
return userInteracted ? userOpenKeys : defaultOpenKeys;
|
||||||
|
}, [userInteracted, userOpenKeys, defaultOpenKeys]);
|
||||||
|
|
||||||
|
// 处理菜单展开/收起
|
||||||
|
const handleOpenChange = (keys: string[]) => {
|
||||||
|
setUserInteracted(true);
|
||||||
|
setUserOpenKeys(keys);
|
||||||
|
};
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
try {
|
try {
|
||||||
await adminAuthService.logout();
|
await adminAuthService.logout();
|
||||||
localStorage.removeItem('adminToken');
|
|
||||||
logout();
|
logout();
|
||||||
message.success('登出成功');
|
message.success('登出成功');
|
||||||
navigate('/admin/login');
|
navigate('/admin/login');
|
||||||
} catch (error) {
|
} catch {
|
||||||
message.error('登出失败');
|
message.error('登出失败');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -44,12 +72,12 @@ const AdminLayout = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'user',
|
key: 'user',
|
||||||
icon: <UserOutlined />,
|
icon: <SettingOutlined />,
|
||||||
label: '用户管理',
|
label: '系统管理',
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
key: '/admin/users',
|
key: '/admin/users',
|
||||||
label: '用户列表',
|
label: '用户管理',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -57,26 +85,35 @@ const AdminLayout = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout style={{ minHeight: '100vh' }}>
|
<Layout style={{ minHeight: '100vh' }}>
|
||||||
<Sider trigger={null} collapsible collapsed={collapsed}>
|
<Sider
|
||||||
|
trigger={null}
|
||||||
|
collapsible
|
||||||
|
collapsed={collapsed}
|
||||||
|
style={{
|
||||||
|
background: themeMode === 'dark' ? '#001529' : '#ffffff',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
height: 32,
|
height: 32,
|
||||||
margin: 16,
|
margin: 16,
|
||||||
background: 'rgba(255, 255, 255, 0.2)',
|
background: themeMode === 'dark' ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.05)',
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
color: 'white',
|
color: themeMode === 'dark' ? 'white' : '#1890ff',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{collapsed ? '运营' : '运营管理系统'}
|
{collapsed ? '运营' : '运营管理系统'}
|
||||||
</div>
|
</div>
|
||||||
<Menu
|
<Menu
|
||||||
theme="dark"
|
theme={themeMode === 'dark' ? 'dark' : 'light'}
|
||||||
mode="inline"
|
mode="inline"
|
||||||
selectedKeys={[location.pathname]}
|
selectedKeys={[location.pathname]}
|
||||||
|
openKeys={openKeys}
|
||||||
|
onOpenChange={handleOpenChange}
|
||||||
items={menuItems}
|
items={menuItems}
|
||||||
onClick={({ key }) => navigate(key)}
|
onClick={({ key }) => navigate(key)}
|
||||||
/>
|
/>
|
||||||
@@ -100,14 +137,23 @@ const AdminLayout = () => {
|
|||||||
fontSize: '16px',
|
fontSize: '16px',
|
||||||
width: 64,
|
width: 64,
|
||||||
height: 64,
|
height: 64,
|
||||||
|
outline: 'none',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={themeMode === 'dark' ? <SunOutlined /> : <MoonOutlined />}
|
||||||
|
onClick={toggleTheme}
|
||||||
|
style={{ outline: 'none' }}
|
||||||
|
title={themeMode === 'dark' ? '切换到亮色主题' : '切换到暗色主题'}
|
||||||
|
/>
|
||||||
<span>欢迎, {adminUser?.username}</span>
|
<span>欢迎, {adminUser?.username}</span>
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
icon={<LogoutOutlined />}
|
icon={<LogoutOutlined />}
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
|
style={{ outline: 'none' }}
|
||||||
>
|
>
|
||||||
退出
|
退出
|
||||||
</Button>
|
</Button>
|
||||||
@@ -124,7 +170,12 @@ const AdminLayout = () => {
|
|||||||
>
|
>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Content>
|
</Content>
|
||||||
<Footer style={{ textAlign: 'center' }}>
|
<Footer
|
||||||
|
style={{
|
||||||
|
textAlign: 'center',
|
||||||
|
background: themeMode === 'dark' ? '#001529' : '#ffffff',
|
||||||
|
}}
|
||||||
|
>
|
||||||
梦幻西游一站式运营管理平台 ©2025 Created by JGE
|
梦幻西游一站式运营管理平台 ©2025 Created by JGE
|
||||||
</Footer>
|
</Footer>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -3,8 +3,25 @@ import { useAuthStore } from '../stores/authStore';
|
|||||||
|
|
||||||
const PlayerAuthRoute = () => {
|
const PlayerAuthRoute = () => {
|
||||||
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
||||||
|
const authType = useAuthStore((state) => state.authType);
|
||||||
|
const setAuth = useAuthStore((state) => state.setAuth);
|
||||||
|
|
||||||
if (!isAuthenticated) {
|
// 检查是否有playerToken
|
||||||
|
const playerToken = localStorage.getItem('playerToken');
|
||||||
|
const playerUserStr = localStorage.getItem('playerUser');
|
||||||
|
|
||||||
|
// 如果有playerToken但authType不是player,说明需要重新加载玩家认证
|
||||||
|
if (playerToken && playerUserStr && authType !== 'player') {
|
||||||
|
try {
|
||||||
|
const playerUser = JSON.parse(playerUserStr);
|
||||||
|
setAuth(playerUser, playerToken, 'player');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('解析playerUser失败:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有playerToken,重定向到登录页
|
||||||
|
if (!playerToken) {
|
||||||
return <Navigate to="/player/login" replace />;
|
return <Navigate to="/player/login" replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Layout, Menu, Button, Dropdown } from 'antd';
|
import { Layout, Menu, Button, Dropdown, theme, App } from 'antd';
|
||||||
import {
|
import {
|
||||||
HomeOutlined,
|
HomeOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
@@ -6,19 +6,21 @@ import {
|
|||||||
DownOutlined,
|
DownOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { message } from 'antd';
|
|
||||||
import { playerAuthService } from '../services/playerAuthService';
|
import { playerAuthService } from '../services/playerAuthService';
|
||||||
|
|
||||||
const { Header, Content, Footer } = Layout;
|
const { Header, Content, Footer } = Layout;
|
||||||
|
|
||||||
const PlayerLayout = () => {
|
const PlayerLayout = () => {
|
||||||
|
const {
|
||||||
|
token: { colorBgContainer, colorBgLayout },
|
||||||
|
} = theme.useToken();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const { message } = App.useApp();
|
||||||
|
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
try {
|
try {
|
||||||
await playerAuthService.logout();
|
await playerAuthService.logout();
|
||||||
localStorage.removeItem('playerToken');
|
|
||||||
message.success('登出成功');
|
message.success('登出成功');
|
||||||
navigate('/player/login');
|
navigate('/player/login');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -54,46 +56,73 @@ const PlayerLayout = () => {
|
|||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'center',
|
||||||
background: '#001529',
|
background: '#001529',
|
||||||
padding: '0 24px',
|
padding: '0',
|
||||||
|
height: 64,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{/* 导航条内容容器 */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
color: 'white',
|
|
||||||
fontSize: '20px',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 12,
|
width: '100%',
|
||||||
|
maxWidth: 1200,
|
||||||
|
padding: '0 24px',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>梦幻西游玩家服务中心</span>
|
{/* LOGO标题位 */}
|
||||||
</div>
|
<div
|
||||||
<Menu
|
style={{
|
||||||
theme="dark"
|
color: '#ffffff',
|
||||||
mode="horizontal"
|
fontSize: '20px',
|
||||||
selectedKeys={[location.pathname]}
|
fontWeight: 'bold',
|
||||||
items={menuItems}
|
display: 'flex',
|
||||||
style={{ flex: 1, minWidth: 0, justifyContent: 'center' }}
|
alignItems: 'center',
|
||||||
onClick={({ key }) => navigate(key)}
|
gap: 12,
|
||||||
/>
|
flexShrink: 0,
|
||||||
<Dropdown menu={{ items: userMenuItems }} placement="bottomRight">
|
}}
|
||||||
<Button
|
|
||||||
type="text"
|
|
||||||
style={{ color: 'white' }}
|
|
||||||
icon={<DownOutlined />}
|
|
||||||
>
|
>
|
||||||
个人中心
|
<span>梦幻西游玩家服务中心</span>
|
||||||
</Button>
|
</div>
|
||||||
</Dropdown>
|
|
||||||
|
{/* 导航菜单位 */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
margin: '0 24px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Menu
|
||||||
|
theme="dark"
|
||||||
|
mode="horizontal"
|
||||||
|
selectedKeys={[location.pathname]}
|
||||||
|
items={menuItems}
|
||||||
|
style={{ flex: 1, minWidth: 0, justifyContent: 'center', background: 'transparent' }}
|
||||||
|
onClick={({ key }) => navigate(key)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 个人中心菜单位 */}
|
||||||
|
<Dropdown menu={{ items: userMenuItems }} placement="bottomRight">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
style={{ color: '#ffffff', flexShrink: 0 }}
|
||||||
|
icon={<DownOutlined />}
|
||||||
|
>
|
||||||
|
个人中心
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
</Header>
|
</Header>
|
||||||
<Content
|
<Content
|
||||||
style={{
|
style={{
|
||||||
padding: '24px 50px',
|
padding: '24px 50px',
|
||||||
minHeight: 'calc(100vh - 128px)',
|
minHeight: 'calc(100vh - 128px)',
|
||||||
background: '#f0f2f5',
|
background: colorBgContainer,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ padding: 24, minHeight: 380, background: 'white', borderRadius: 8 }}>
|
<div style={{ padding: 24, minHeight: 380, background: 'white', borderRadius: 8 }}>
|
||||||
@@ -103,7 +132,7 @@ const PlayerLayout = () => {
|
|||||||
<Footer
|
<Footer
|
||||||
style={{
|
style={{
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
background: '#001529',
|
background: colorBgLayout,
|
||||||
color: 'rgba(255, 255, 255, 0.65)',
|
color: 'rgba(255, 255, 255, 0.65)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
34
frontend/src/components/ThemeProvider.tsx
Normal file
34
frontend/src/components/ThemeProvider.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { ReactNode, useMemo } from 'react';
|
||||||
|
import { ConfigProvider, theme } from 'antd';
|
||||||
|
import zhCN from 'antd/locale/zh_CN';
|
||||||
|
import { useThemeStore } from '../stores/themeStore';
|
||||||
|
import { getThemeByMode } from '../theme';
|
||||||
|
|
||||||
|
interface ThemeProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主题提供者组件
|
||||||
|
export const ThemeProvider = ({ children }: ThemeProviderProps) => {
|
||||||
|
const { themeMode } = useThemeStore();
|
||||||
|
|
||||||
|
// 根据主题模式获取主题配置和算法
|
||||||
|
const currentTheme = useMemo(() => {
|
||||||
|
const customTheme = getThemeByMode(themeMode);
|
||||||
|
const algorithm = themeMode === 'dark' ? theme.darkAlgorithm : theme.defaultAlgorithm;
|
||||||
|
|
||||||
|
return {
|
||||||
|
algorithm,
|
||||||
|
...customTheme,
|
||||||
|
};
|
||||||
|
}, [themeMode]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfigProvider
|
||||||
|
locale={zhCN}
|
||||||
|
theme={currentTheme}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ConfigProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
import { StrictMode } from 'react';
|
import { StrictMode } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import { RouterProvider } from 'react-router-dom';
|
import { RouterProvider } from 'react-router-dom';
|
||||||
import { ConfigProvider } from 'antd';
|
import { App } from 'antd';
|
||||||
import zhCN from 'antd/locale/zh_CN';
|
|
||||||
import './index.css';
|
import './index.css';
|
||||||
import router from './router';
|
import router from './router';
|
||||||
|
import { ThemeProvider } from './components/ThemeProvider';
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<ConfigProvider locale={zhCN}>
|
<ThemeProvider>
|
||||||
<RouterProvider router={router} />
|
<App>
|
||||||
</ConfigProvider>
|
<RouterProvider router={router} />
|
||||||
|
</App>
|
||||||
|
</ThemeProvider>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const AdminDashboard = () => {
|
|||||||
title="总用户数"
|
title="总用户数"
|
||||||
value={11280}
|
value={11280}
|
||||||
prefix={<UserOutlined />}
|
prefix={<UserOutlined />}
|
||||||
valueStyle={{ color: '#3f8600' }}
|
styles={{ content: { color: '#3f8600' } }}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -27,7 +27,7 @@ const AdminDashboard = () => {
|
|||||||
title="今日订单"
|
title="今日订单"
|
||||||
value={93}
|
value={93}
|
||||||
prefix={<ShoppingOutlined />}
|
prefix={<ShoppingOutlined />}
|
||||||
valueStyle={{ color: '#cf1322' }}
|
styles={{ content: { color: '#cf1322' } }}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -38,7 +38,7 @@ const AdminDashboard = () => {
|
|||||||
value={11280}
|
value={11280}
|
||||||
prefix={<DollarOutlined />}
|
prefix={<DollarOutlined />}
|
||||||
precision={2}
|
precision={2}
|
||||||
valueStyle={{ color: '#1890ff' }}
|
styles={{ content: { color: '#1890ff' } }}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -48,7 +48,7 @@ const AdminDashboard = () => {
|
|||||||
title="活动参与"
|
title="活动参与"
|
||||||
value={93}
|
value={93}
|
||||||
prefix={<TrophyOutlined />}
|
prefix={<TrophyOutlined />}
|
||||||
valueStyle={{ color: '#722ed1' }}
|
styles={{ content: { color: '#722ed1' } }}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Form, Input, Button, Card, message } from 'antd';
|
import { Form, Input, Button, Card, App } from 'antd';
|
||||||
import { UserOutlined, LockOutlined } from '@ant-design/icons';
|
import { UserOutlined, LockOutlined } from '@ant-design/icons';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { adminAuthService, type LoginRequest } from '../../services/adminAuthService';
|
import { adminAuthService, type LoginRequest } from '../../services/adminAuthService';
|
||||||
@@ -9,6 +9,7 @@ const AdminLogin = () => {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const setAuth = useAuthStore((state) => state.setAuth);
|
const setAuth = useAuthStore((state) => state.setAuth);
|
||||||
|
const { message } = App.useApp();
|
||||||
|
|
||||||
const onFinish = async (values: LoginRequest) => {
|
const onFinish = async (values: LoginRequest) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -20,9 +21,9 @@ const AdminLogin = () => {
|
|||||||
username: response.username,
|
username: response.username,
|
||||||
role: response.role,
|
role: response.role,
|
||||||
},
|
},
|
||||||
response.accessToken
|
response.accessToken,
|
||||||
|
'admin'
|
||||||
);
|
);
|
||||||
localStorage.setItem('adminToken', response.accessToken);
|
|
||||||
message.success('登录成功');
|
message.success('登录成功');
|
||||||
navigate('/admin/dashboard');
|
navigate('/admin/dashboard');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
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;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Card, Row, Col, Statistic, List, Avatar, Button } from 'antd';
|
import { Card, Row, Col, Statistic, Avatar, Button } from 'antd';
|
||||||
import {
|
import {
|
||||||
TrophyOutlined,
|
TrophyOutlined,
|
||||||
GiftOutlined,
|
GiftOutlined,
|
||||||
@@ -36,7 +36,7 @@ const PlayerDashboard = () => {
|
|||||||
value={100}
|
value={100}
|
||||||
suffix="级"
|
suffix="级"
|
||||||
prefix={<StarOutlined />}
|
prefix={<StarOutlined />}
|
||||||
valueStyle={{ color: '#3f8600' }}
|
styles={{ content: { color: '#3f8600' } }}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -47,7 +47,7 @@ const PlayerDashboard = () => {
|
|||||||
value={12800}
|
value={12800}
|
||||||
prefix={<GiftOutlined />}
|
prefix={<GiftOutlined />}
|
||||||
precision={2}
|
precision={2}
|
||||||
valueStyle={{ color: '#cf1322' }}
|
styles={{ content: { color: '#cf1322' } }}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -58,7 +58,7 @@ const PlayerDashboard = () => {
|
|||||||
value={365}
|
value={365}
|
||||||
suffix="小时"
|
suffix="小时"
|
||||||
prefix={<ClockCircleOutlined />}
|
prefix={<ClockCircleOutlined />}
|
||||||
valueStyle={{ color: '#1890ff' }}
|
styles={{ content: { color: '#1890ff' } }}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -68,7 +68,7 @@ const PlayerDashboard = () => {
|
|||||||
title="成就点数"
|
title="成就点数"
|
||||||
value={2560}
|
value={2560}
|
||||||
prefix={<TrophyOutlined />}
|
prefix={<TrophyOutlined />}
|
||||||
valueStyle={{ color: '#722ed1' }}
|
styles={{ content: { color: '#722ed1' } }}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -76,24 +76,33 @@ const PlayerDashboard = () => {
|
|||||||
<Row gutter={16} style={{ marginTop: 24 }}>
|
<Row gutter={16} style={{ marginTop: 24 }}>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Card title="系统公告">
|
<Card title="系统公告">
|
||||||
<List
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||||
itemLayout="horizontal"
|
{notices.map((item, index) => (
|
||||||
dataSource={notices}
|
<div
|
||||||
renderItem={(item) => (
|
key={index}
|
||||||
<List.Item>
|
style={{
|
||||||
<List.Item.Meta
|
display: 'flex',
|
||||||
avatar={<Avatar icon={<TrophyOutlined />} />}
|
alignItems: 'flex-start',
|
||||||
title={item.title}
|
padding: '12px 0',
|
||||||
description={
|
borderBottom: index !== notices.length - 1 ? '1px solid #f0f0f0' : 'none',
|
||||||
<div>
|
}}
|
||||||
<p>{item.description}</p>
|
>
|
||||||
<small style={{ color: '#999' }}>{item.time}</small>
|
<Avatar
|
||||||
</div>
|
icon={<TrophyOutlined />}
|
||||||
}
|
style={{ marginRight: 12, flexShrink: 0 }}
|
||||||
/>
|
/>
|
||||||
</List.Item>
|
<div style={{ flex: 1 }}>
|
||||||
)}
|
<div style={{ fontSize: 16, fontWeight: 500, marginBottom: 4 }}>
|
||||||
/>
|
{item.title}
|
||||||
|
</div>
|
||||||
|
<div style={{ color: '#666', marginBottom: 4 }}>
|
||||||
|
{item.description}
|
||||||
|
</div>
|
||||||
|
<small style={{ color: '#999' }}>{item.time}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Form, Input, Button, Card, message } from 'antd';
|
import { Form, Input, Button, Card, App } from 'antd';
|
||||||
import { UserOutlined, LockOutlined } from '@ant-design/icons';
|
import { UserOutlined, LockOutlined } from '@ant-design/icons';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { playerAuthService, type LoginRequest } from '../../services/playerAuthService';
|
import { playerAuthService, type LoginRequest } from '../../services/playerAuthService';
|
||||||
@@ -9,17 +9,17 @@ const PlayerLogin = () => {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const setAuth = useAuthStore((state) => state.setAuth);
|
const setAuth = useAuthStore((state) => state.setAuth);
|
||||||
|
const { message } = App.useApp();
|
||||||
|
|
||||||
const onFinish = async (values: LoginRequest) => {
|
const onFinish = async (values: LoginRequest) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await playerAuthService.login(values);
|
const response = await playerAuthService.login(values);
|
||||||
localStorage.setItem('playerToken', response.accessToken);
|
|
||||||
setAuth({
|
setAuth({
|
||||||
id: response.userId,
|
id: response.userId,
|
||||||
username: response.username,
|
username: response.username,
|
||||||
role: response.role,
|
role: response.role,
|
||||||
}, response.accessToken);
|
}, response.accessToken, 'player');
|
||||||
message.success('登录成功');
|
message.success('登录成功');
|
||||||
navigate('/player/dashboard');
|
navigate('/player/dashboard');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -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 />,
|
||||||
|
|||||||
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;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -6,28 +6,82 @@ interface AdminUser {
|
|||||||
role: string;
|
role: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AuthType = 'admin' | 'player';
|
||||||
|
|
||||||
interface AuthState {
|
interface AuthState {
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
adminUser: AdminUser | null;
|
adminUser: AdminUser | null;
|
||||||
token: string | null;
|
token: string | null;
|
||||||
setAuth: (user: AdminUser, token: string) => void;
|
authType: AuthType | null;
|
||||||
|
setAuth: (user: AdminUser, token: string, type: AuthType) => void;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useAuthStore = create<AuthState>((set) => ({
|
export const useAuthStore = create<AuthState>((set) => {
|
||||||
isAuthenticated: false,
|
// 初始化时从localStorage加载
|
||||||
adminUser: null,
|
const savedAdminToken = localStorage.getItem('adminToken');
|
||||||
token: null,
|
const savedPlayerToken = localStorage.getItem('playerToken');
|
||||||
setAuth: (user, token) =>
|
const savedAdminUserStr = localStorage.getItem('adminUser');
|
||||||
set({
|
const savedPlayerUserStr = localStorage.getItem('playerUser');
|
||||||
isAuthenticated: true,
|
|
||||||
adminUser: user,
|
let savedUser: AdminUser | null = null;
|
||||||
token,
|
let savedToken: string | null = null;
|
||||||
}),
|
let savedAuthType: AuthType | null = null;
|
||||||
logout: () =>
|
|
||||||
set({
|
// 优先加载管理员认证
|
||||||
isAuthenticated: false,
|
if (savedAdminToken && savedAdminUserStr) {
|
||||||
adminUser: null,
|
try {
|
||||||
token: null,
|
savedUser = JSON.parse(savedAdminUserStr);
|
||||||
}),
|
savedToken = savedAdminToken;
|
||||||
}));
|
savedAuthType = 'admin';
|
||||||
|
} catch (e) {
|
||||||
|
console.error('解析adminUser失败:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 如果没有管理员认证,则加载玩家认证
|
||||||
|
if (!savedToken && savedPlayerToken && savedPlayerUserStr) {
|
||||||
|
try {
|
||||||
|
savedUser = JSON.parse(savedPlayerUserStr);
|
||||||
|
savedToken = savedPlayerToken;
|
||||||
|
savedAuthType = 'player';
|
||||||
|
} catch (e) {
|
||||||
|
console.error('解析playerUser失败:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isAuthenticated: !!savedToken,
|
||||||
|
adminUser: savedUser,
|
||||||
|
token: savedToken,
|
||||||
|
authType: savedAuthType,
|
||||||
|
setAuth: (user, token, type) => {
|
||||||
|
// 根据类型保存到不同的localStorage key
|
||||||
|
if (type === 'admin') {
|
||||||
|
localStorage.setItem('adminToken', token);
|
||||||
|
localStorage.setItem('adminUser', JSON.stringify(user));
|
||||||
|
} else {
|
||||||
|
localStorage.setItem('playerToken', token);
|
||||||
|
localStorage.setItem('playerUser', JSON.stringify(user));
|
||||||
|
}
|
||||||
|
set({
|
||||||
|
isAuthenticated: true,
|
||||||
|
adminUser: user,
|
||||||
|
token,
|
||||||
|
authType: type,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
logout: () => {
|
||||||
|
// 清除所有认证数据
|
||||||
|
localStorage.removeItem('adminToken');
|
||||||
|
localStorage.removeItem('adminUser');
|
||||||
|
localStorage.removeItem('playerToken');
|
||||||
|
localStorage.removeItem('playerUser');
|
||||||
|
set({
|
||||||
|
isAuthenticated: false,
|
||||||
|
adminUser: null,
|
||||||
|
token: null,
|
||||||
|
authType: null,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|||||||
34
frontend/src/stores/themeStore.ts
Normal file
34
frontend/src/stores/themeStore.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
// 主题类型
|
||||||
|
export type ThemeMode = 'light' | 'dark';
|
||||||
|
|
||||||
|
// 从 localStorage 读取主题模式
|
||||||
|
const getInitialThemeMode = (): ThemeMode => {
|
||||||
|
const savedTheme = localStorage.getItem('adminThemeMode');
|
||||||
|
if (savedTheme === 'light' || savedTheme === 'dark') {
|
||||||
|
return savedTheme;
|
||||||
|
}
|
||||||
|
return 'dark'; // 默认暗色主题
|
||||||
|
};
|
||||||
|
|
||||||
|
// 主题状态接口
|
||||||
|
interface ThemeState {
|
||||||
|
themeMode: ThemeMode;
|
||||||
|
toggleTheme: () => void;
|
||||||
|
setTheme: (mode: ThemeMode) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主题状态管理
|
||||||
|
export const useThemeStore = create<ThemeState>((set) => ({
|
||||||
|
themeMode: getInitialThemeMode(),
|
||||||
|
toggleTheme: () => set((state) => {
|
||||||
|
const newMode = state.themeMode === 'light' ? 'dark' : 'light';
|
||||||
|
localStorage.setItem('adminThemeMode', newMode);
|
||||||
|
return { themeMode: newMode };
|
||||||
|
}),
|
||||||
|
setTheme: (mode) => {
|
||||||
|
localStorage.setItem('adminThemeMode', mode);
|
||||||
|
set({ themeMode: mode });
|
||||||
|
},
|
||||||
|
}));
|
||||||
85
frontend/src/theme/index.ts
Normal file
85
frontend/src/theme/index.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
// 全局主题配置
|
||||||
|
import type { ThemeConfig } from 'antd';
|
||||||
|
import type { ThemeMode } from '../stores/themeStore';
|
||||||
|
|
||||||
|
// 亮色主题配置
|
||||||
|
export const lightTheme: ThemeConfig = {
|
||||||
|
token: {
|
||||||
|
// 主色调
|
||||||
|
colorPrimary: '#1890ff',
|
||||||
|
colorSuccess: '#52c41a',
|
||||||
|
colorWarning: '#faad14',
|
||||||
|
colorError: '#ff4d4f',
|
||||||
|
colorInfo: '#1890ff',
|
||||||
|
|
||||||
|
// 字体设置
|
||||||
|
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
||||||
|
fontSize: 14,
|
||||||
|
|
||||||
|
// 圆角
|
||||||
|
borderRadius: 3,
|
||||||
|
},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
// Layout 组件主题
|
||||||
|
Layout: {
|
||||||
|
headerBg: '#ffffff', // 顶部导航背景色(亮色)
|
||||||
|
footerBg: '#ffffff', // 页脚背景色(亮色)
|
||||||
|
siderBg: '#ffffff', // 侧边栏背景色(亮色)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Menu 组件主题
|
||||||
|
Menu: {
|
||||||
|
itemSelectedBg: '#e6f7ff', // 菜单激活背景色(亮色)
|
||||||
|
itemHoverBg: 'rgba(24, 144, 255, 0.1)', // 菜单悬停背景色(亮色)
|
||||||
|
itemColor: 'rgba(0, 0, 0, 0.65)', // 菜单项文字颜色(亮色)
|
||||||
|
itemSelectedColor: '#1890ff', // 菜单激活文字颜色(亮色)
|
||||||
|
itemHoverColor: '#1890ff', // 菜单悬停文字颜色(亮色)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 暗色主题配置
|
||||||
|
export const darkTheme: ThemeConfig = {
|
||||||
|
token: {
|
||||||
|
// 主色调
|
||||||
|
colorPrimary: '#1890ff',
|
||||||
|
colorSuccess: '#52c41a',
|
||||||
|
colorWarning: '#faad14',
|
||||||
|
colorError: '#ff4d4f',
|
||||||
|
colorInfo: '#1890ff',
|
||||||
|
|
||||||
|
// 字体设置
|
||||||
|
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
|
||||||
|
fontSize: 14,
|
||||||
|
|
||||||
|
// 圆角
|
||||||
|
borderRadius: 3,
|
||||||
|
},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
// Layout 组件主题
|
||||||
|
Layout: {
|
||||||
|
headerBg: '#001529', // 顶部导航和侧边栏背景色(暗色)
|
||||||
|
footerBg: '#001529', // 页脚背景色(暗色)
|
||||||
|
siderBg: '#001529', // 侧边栏背景色(暗色)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Menu 组件主题
|
||||||
|
Menu: {
|
||||||
|
darkItemSelectedBg: '#1890ff', // 菜单激活背景色(暗色)
|
||||||
|
darkItemHoverBg: 'rgba(24, 144, 255, 0.2)', // 菜单悬停背景色(暗色)
|
||||||
|
darkItemColor: 'rgba(255, 255, 255, 0.65)', // 菜单项文字颜色(暗色)
|
||||||
|
darkItemSelectedColor: '#fff', // 菜单激活文字颜色(暗色)
|
||||||
|
darkItemHoverColor: '#fff', // 菜单悬停文字颜色(暗色)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据主题模式获取主题配置
|
||||||
|
export const getThemeByMode = (mode: ThemeMode): ThemeConfig => {
|
||||||
|
return mode === 'light' ? lightTheme : darkTheme;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 默认主题配置(向后兼容)
|
||||||
|
export const appTheme: ThemeConfig = lightTheme;
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user