feat: 运营管理系统后台登录鉴权及后台框架开发

This commit is contained in:
Stev_Wang
2025-12-12 17:31:17 +08:00
parent 874b613d85
commit 5a3d3918ba
7 changed files with 1479 additions and 30 deletions

96
sql/init_admin_users.sql Normal file
View File

@@ -0,0 +1,96 @@
-- =====================================================
-- 梦幻西游一站式运营管理系统 - 后台用户表初始化脚本
-- 适用于 MYSQL 8.4
-- 创建日期: 2025-12-12
-- 描述: 创建后台管理员用户相关数据表
-- =====================================================
-- 选择数据库
USE mhxy_web;
-- 创建后台管理员用户表
CREATE TABLE IF NOT EXISTS admin_users (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '管理员ID',
username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',
password VARCHAR(255) NOT NULL COMMENT '密码(加密存储)',
real_name VARCHAR(100) COMMENT '真实姓名',
email VARCHAR(100) COMMENT '邮箱地址',
phone VARCHAR(20) COMMENT '联系电话',
status TINYINT DEFAULT 1 COMMENT '账号状态: 0-禁用, 1-启用',
is_super_admin TINYINT DEFAULT 0 COMMENT '是否超级管理员: 0-否, 1-是',
last_login_time DATETIME COMMENT '最后登录时间',
last_login_ip VARCHAR(45) COMMENT '最后登录IP',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
-- 创建索引
INDEX idx_username (username),
INDEX idx_status (status),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='后台管理员用户表';
-- 创建管理员操作日志表
CREATE TABLE IF NOT EXISTS admin_operation_logs (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '日志ID',
admin_user_id INT NOT NULL COMMENT '管理员ID',
operation_type VARCHAR(50) NOT NULL COMMENT '操作类型',
operation_desc TEXT COMMENT '操作描述',
request_method VARCHAR(10) COMMENT '请求方法',
request_url VARCHAR(255) COMMENT '请求URL',
request_params TEXT COMMENT '请求参数',
response_status INT COMMENT '响应状态码',
ip_address VARCHAR(45) COMMENT 'IP地址',
user_agent TEXT COMMENT '用户代理',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-- 创建外键约束
FOREIGN KEY (admin_user_id) REFERENCES admin_users(id) ON DELETE CASCADE,
-- 创建索引
INDEX idx_admin_user_id (admin_user_id),
INDEX idx_operation_type (operation_type),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='管理员操作日志表';
-- 创建系统配置表
CREATE TABLE IF NOT EXISTS system_config (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '配置ID',
config_key VARCHAR(100) NOT NULL UNIQUE COMMENT '配置键',
config_value TEXT COMMENT '配置值',
config_desc VARCHAR(255) COMMENT '配置描述',
config_group VARCHAR(50) DEFAULT 'default' COMMENT '配置分组',
is_editable TINYINT DEFAULT 1 COMMENT '是否可编辑: 0-否, 1-是',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
-- 创建索引
INDEX idx_config_key (config_key),
INDEX idx_config_group (config_group)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统配置表';
-- 插入初始超级管理员账号(密码为: admin123实际部署时需要加密
INSERT INTO admin_users (username, password, real_name, email, status, is_super_admin) VALUES
('admin', '$2b$10$rOzK5V4yR7YqE6XoV4Wz9ODz8rN2L5A3C7B1F9H6I4J2K3L5M6N7O8P', '系统管理员', 'admin@mhxy.com', 1, 1);
-- 说明: 上面插入的密码是 'admin123' 的BCrypt加密结果
-- 生产环境中应该使用更复杂的密码
-- 加密密码生成示例(实际使用时需要在应用中处理):
-- const bcrypt = require('bcrypt');
-- const hashedPassword = await bcrypt.hash('admin123', 10);
-- 插入系统默认配置
INSERT INTO system_config (config_key, config_value, config_desc, config_group) VALUES
('site_name', '梦幻西游一站式运营管理系统', '网站名称', 'basic'),
('site_description', '梦幻西游游戏运营管理后台系统', '网站描述', 'basic'),
('admin_session_timeout', '7200', '管理员会话超时时间(秒)', 'security'),
('max_login_attempts', '5', '最大登录尝试次数', 'security'),
('enable_captcha', '1', '是否启用验证码', 'security');
-- =====================================================
-- 使用说明:
-- 1. 在MySQL 8.4中执行此脚本
-- 2. 确保数据库存在: CREATE DATABASE IF NOT EXISTS mhxy_web;
-- 3. 执行脚本: source /path/to/init_admin_users.sql;
-- 4. 初始管理员账号: admin / admin123
-- 5. 部署到生产环境时,请修改默认密码
-- =====================================================

View File

@@ -4,34 +4,47 @@ import { ConfigProvider } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import PlayerLogin from './pages/PlayerLogin';
import AdminLogin from './pages/AdminLogin';
import AdminDashboard from './pages/AdminDashboard';
import Home from './pages/Home';
import { AuthProvider } from './contexts/AuthContext';
import AdminLayout from './layouts/AdminLayout';
/**
* 主应用组件
* 配置了路由系统,支持两个登录页面
* 配置了路由系统,支持:
* - /player - 玩家服务中心登录页面
* - /admin - 运营管理系统后台登录页面
* - /admin/dashboard - 后台管理系统工作台
* - / - 首页(导航页面)
*/
function App() {
return (
<StrictMode>
<ConfigProvider locale={zhCN}>
<Router>
<Routes>
{/* 首页导航 */}
<Route path="/" element={<Home />} />
{/* 玩家服务中心登录页面 */}
<Route path="/player" element={<PlayerLogin />} />
{/* 运营管理系统后台登录页面 */}
<Route path="/admin" element={<AdminLogin />} />
{/* 默认重定向到首页 */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</Router>
<AuthProvider>
<Router>
<Routes>
{/* 首页导航 */}
<Route path="/" element={<Home />} />
{/* 玩家服务中心登录页面 */}
<Route path="/player" element={<PlayerLogin />} />
{/* 运营管理系统后台登录页面 */}
<Route path="/admin" element={<AdminLogin />} />
{/* 后台管理系统路由(需要认证) */}
<Route path="/admin/dashboard" element={
<AdminLayout>
<AdminDashboard />
</AdminLayout>
} />
{/* 默认重定向到首页 */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</Router>
</AuthProvider>
</ConfigProvider>
</StrictMode>
);

View File

@@ -0,0 +1,186 @@
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { message } from 'antd';
// 管理员用户接口
export interface AdminUser {
id: number;
username: string;
real_name: string;
email: string;
phone: string;
status: number;
is_super_admin: number;
last_login_time: string;
last_login_ip: string;
created_at: string;
updated_at: string;
}
// 认证上下文接口
interface AuthContextType {
user: AdminUser | null;
isLoading: boolean;
isAuthenticated: boolean;
login: (username: string, password: string) => Promise<boolean>;
logout: () => void;
}
// 创建认证上下文
const AuthContext = createContext<AuthContextType | undefined>(undefined);
// 模拟API调用
const mockApiCall = {
login: async (username: string, password: string): Promise<AdminUser> => {
// 模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 1000));
// 模拟验证逻辑
if (username === 'admin' && password === 'admin123') {
const mockUser: AdminUser = {
id: 1,
username: 'admin',
real_name: '系统管理员',
email: 'admin@mhxy.com',
phone: '13800138000',
status: 1,
is_super_admin: 1,
last_login_time: new Date().toISOString(),
last_login_ip: '127.0.0.1',
created_at: '2025-12-12T00:00:00.000Z',
updated_at: '2025-12-12T00:00:00.000Z'
};
return mockUser;
} else {
throw new Error('用户名或密码错误');
}
}
};
/**
* 认证提供者组件
* @param children - 子组件
*/
export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const [user, setUser] = useState<AdminUser | null>(null);
const [isLoading, setIsLoading] = useState(true);
// 检查本地存储中的用户信息
useEffect(() => {
const checkAuthStatus = () => {
try {
const storedUser = localStorage.getItem('admin_user');
const token = localStorage.getItem('admin_token');
if (storedUser && token) {
setUser(JSON.parse(storedUser));
}
} catch (error) {
console.error('检查认证状态失败:', error);
// 清除无效的本地存储
localStorage.removeItem('admin_user');
localStorage.removeItem('admin_token');
} finally {
setIsLoading(false);
}
};
checkAuthStatus();
}, []);
/**
* 登录方法
* @param username - 用户名
* @param password - 密码
* @returns Promise<boolean> - 登录是否成功
*/
const login = async (username: string, password: string): Promise<boolean> => {
setIsLoading(true);
try {
const loggedInUser = await mockApiCall.login(username, password);
// 存储用户信息和token
const token = `mock_token_${loggedInUser.id}_${Date.now()}`;
localStorage.setItem('admin_user', JSON.stringify(loggedInUser));
localStorage.setItem('admin_token', token);
setUser(loggedInUser);
message.success(`欢迎回来,${loggedInUser.real_name}`);
return true;
} catch (error) {
message.error(error instanceof Error ? error.message : '登录失败,请重试');
return false;
} finally {
setIsLoading(false);
}
};
/**
* 登出方法
*/
const logout = () => {
localStorage.removeItem('admin_user');
localStorage.removeItem('admin_token');
setUser(null);
message.info('已安全登出');
};
const value: AuthContextType = {
user,
isLoading,
isAuthenticated: !!user,
login,
logout
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
};
/**
* 使用认证上下文的Hook
* @returns AuthContextType
*/
export const useAuth = (): AuthContextType => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth 必须在 AuthProvider 内部使用');
}
return context;
};
/**
* 管理员权限验证组件
* @param children - 子组件
* @param fallback - 未认证时的回退组件
*/
export const AdminProtectedRoute: React.FC<{
children: ReactNode;
fallback?: ReactNode;
}> = ({ children, fallback }) => {
const { isAuthenticated, isLoading } = useAuth();
if (isLoading) {
return (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh'
}}>
...
</div>
);
}
if (!isAuthenticated) {
return fallback || <div></div>;
}
return <>{children}</>;
};
export default AuthContext;

440
src/layouts/AdminLayout.tsx Normal file
View File

@@ -0,0 +1,440 @@
import React, { useState, useEffect } from 'react';
import { Layout, Menu, Avatar, Dropdown, Button, theme, Space, Typography, Badge, Drawer } from 'antd';
import type { MenuProps } from 'antd';
import {
MenuFoldOutlined,
MenuUnfoldOutlined,
DashboardOutlined,
UserOutlined,
SettingOutlined,
LogoutOutlined,
BellOutlined,
SafetyOutlined,
TeamOutlined,
PlayCircleOutlined,
DollarOutlined,
BarChartOutlined,
ToolOutlined,
MobileOutlined,
FileTextOutlined
} from '@ant-design/icons';
import { useAuth } from '../contexts/AuthContext';
import { useNavigate, useLocation } from 'react-router-dom';
import TabNavigation from './TabNavigation';
const { Header, Sider, Content } = Layout;
const { Text } = Typography;
// 定义菜单项类型
interface MenuItem {
key: string;
icon?: React.ReactNode;
label: string;
children?: MenuItem[];
}
/**
* 菜单项配置
*/
const menuItems: MenuProps['items'] = [
{
key: '/admin/dashboard',
icon: <DashboardOutlined />,
label: '工作台'
},
{
key: 'system',
icon: <SettingOutlined />,
label: '系统管理',
children: [
{
key: '/admin/system/users',
icon: <TeamOutlined />,
label: '用户管理'
},
{
key: '/admin/system/roles',
icon: <SafetyOutlined />,
label: '角色管理'
},
{
key: '/admin/system/permissions',
icon: <UserOutlined />,
label: '权限管理'
},
{
key: '/admin/system/config',
icon: <ToolOutlined />,
label: '系统配置'
}
]
},
{
key: 'game',
icon: <PlayCircleOutlined />,
label: '游戏管理',
children: [
{
key: '/admin/game/servers',
icon: <MobileOutlined />,
label: '服务器管理'
},
{
key: '/admin/game/goods',
icon: <DollarOutlined />,
label: '道具管理'
},
{
key: '/admin/game/announcement',
icon: <BellOutlined />,
label: '公告管理'
}
]
},
{
key: 'finance',
icon: <DollarOutlined />,
label: '财务管理',
children: [
{
key: '/admin/finance/recharge',
icon: <DollarOutlined />,
label: '充值记录'
},
{
key: '/admin/finance/order',
icon: <FileTextOutlined />,
label: '订单管理'
}
]
},
{
key: 'report',
icon: <BarChartOutlined />,
label: '数据统计',
children: [
{
key: '/admin/report/user',
icon: <TeamOutlined />,
label: '用户统计'
},
{
key: '/admin/report/finance',
icon: <DollarOutlined />,
label: '财务统计'
},
{
key: '/admin/report/game',
icon: <PlayCircleOutlined />,
label: '游戏统计'
}
]
}
];
/**
* 管理员后台主布局组件
* 采用 Header - Sider - Content 布局
*/
const AdminLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [collapsed, setCollapsed] = useState(false);
const [mobileDrawerVisible, setMobileDrawerVisible] = useState(false);
const { user, logout } = useAuth();
const navigate = useNavigate();
const location = useLocation();
const { token } = theme.useToken();
// 响应式检测
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const checkIsMobile = () => {
setIsMobile(window.innerWidth < 768);
if (window.innerWidth >= 768) {
setMobileDrawerVisible(false);
}
};
checkIsMobile();
window.addEventListener('resize', checkIsMobile);
return () => window.removeEventListener('resize', checkIsMobile);
}, []);
/**
* 处理用户下拉菜单点击
*/
const handleUserMenuClick: MenuProps['onClick'] = ({ key }) => {
switch (key) {
case 'profile':
navigate('/admin/profile');
break;
case 'settings':
navigate('/admin/settings');
break;
case 'logout':
logout();
navigate('/admin', { replace: true });
break;
default:
break;
}
};
/**
* 用户下拉菜单配置
*/
const userMenuItems: MenuProps['items'] = [
{
key: 'profile',
icon: <UserOutlined />,
label: '个人资料'
},
{
key: 'settings',
icon: <SettingOutlined />,
label: '系统设置'
},
{
type: 'divider'
},
{
key: 'logout',
icon: <LogoutOutlined />,
label: '退出登录'
}
];
/**
* 处理菜单点击
*/
const handleMenuClick = ({ key }: { key: string }) => {
navigate(key);
// 在移动端点击菜单后关闭抽屉
if (isMobile) {
setMobileDrawerVisible(false);
}
};
// 获取当前选中的菜单项
const getSelectedKeys = () => {
const pathname = location.pathname;
// 查找匹配的菜单项
const findSelectedKey = (items: MenuItem[], path: string): string[] => {
for (const item of items) {
if (item.key === path) {
return [item.key];
}
if (item.children) {
const result = findSelectedKey(item.children, path);
if (result.length > 0) {
return result;
}
}
}
return [];
};
return findSelectedKey(menuItems as MenuItem[], pathname);
};
// 获取展开的菜单项
const getOpenKeys = () => {
const pathname = location.pathname;
// 查找需要展开的父级菜单
const findOpenKeys = (items: MenuItem[], path: string): string[] => {
for (const item of items) {
if (item.children) {
const hasSelectedChild = item.children.some((child: MenuItem) =>
child.key === path || findOpenKeys([child], path).length > 0
);
if (hasSelectedChild) {
return [item.key];
}
}
}
return [];
};
return findOpenKeys(menuItems as MenuItem[], pathname);
};
// 侧边栏内容
const siderContent = (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
{/* Logo 区域 */}
<div style={{
height: '64px',
display: 'flex',
alignItems: 'center',
justifyContent: collapsed ? 'center' : 'flex-start',
padding: collapsed ? '0' : '0 24px',
borderBottom: `1px solid ${token.colorBorderSecondary}`,
background: token.colorBgContainer
}}>
{!collapsed ? (
<Space size="small">
<div style={{
width: '32px',
height: '32px',
background: 'linear-gradient(45deg, #1890ff, #722ed1)',
borderRadius: '6px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
fontSize: '16px'
}}>
<SafetyOutlined />
</div>
<Text strong style={{ fontSize: '16px', color: token.colorText }}>
</Text>
</Space>
) : (
<div style={{
width: '32px',
height: '32px',
background: 'linear-gradient(45deg, #1890ff, #722ed1)',
borderRadius: '6px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'white',
fontSize: '16px'
}}>
<SafetyOutlined />
</div>
)}
</div>
{/* 菜单区域 */}
<div style={{ flex: 1, overflow: 'auto' }}>
<Menu
mode="inline"
selectedKeys={getSelectedKeys()}
defaultOpenKeys={getOpenKeys()}
items={menuItems}
onClick={handleMenuClick}
style={{
height: '100%',
borderRight: 0,
background: 'transparent'
}}
/>
</div>
</div>
);
return (
<Layout style={{ minHeight: '100vh' }}>
{/* 侧边栏 - 桌面端 */}
{!isMobile && (
<Sider
trigger={null}
collapsible
collapsed={collapsed}
width={240}
style={{
background: token.colorBgContainer,
borderRight: `1px solid ${token.colorBorderSecondary}`
}}
>
{siderContent}
</Sider>
)}
{/* 侧边栏 - 移动端 Drawer */}
<Drawer
title="菜单"
placement="left"
onClose={() => setMobileDrawerVisible(false)}
open={mobileDrawerVisible}
styles={{ body: { padding: 0 } }}
size="default"
>
{siderContent}
</Drawer>
<Layout>
{/* 头部 */}
<Header style={{
padding: '0 24px',
background: token.colorBgContainer,
borderBottom: `1px solid ${token.colorBorderSecondary}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
position: 'sticky',
top: 0,
zIndex: 100
}}>
{/* 左侧控制区 */}
<Space>
{/* 折叠按钮 */}
<Button
type="text"
icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
onClick={() => setCollapsed(!collapsed)}
style={{ fontSize: '16px' }}
/>
{/* 移动端菜单按钮 */}
{isMobile && (
<Button
type="text"
icon={<MenuFoldOutlined />}
onClick={() => setMobileDrawerVisible(true)}
style={{ fontSize: '16px' }}
/>
)}
</Space>
{/* 右侧功能区 */}
<Space size="middle">
{/* 通知铃铛 */}
<Badge count={3} size="small">
<Button
type="text"
icon={<BellOutlined />}
style={{ fontSize: '16px' }}
/>
</Badge>
{/* 用户信息 */}
<Dropdown
menu={{ items: userMenuItems, onClick: handleUserMenuClick }}
placement="bottomRight"
arrow
>
<Space style={{ cursor: 'pointer' }}>
<Avatar
size="small"
icon={<UserOutlined />}
style={{ backgroundColor: token.colorPrimary }}
/>
<Text>{user?.real_name || user?.username}</Text>
</Space>
</Dropdown>
</Space>
</Header>
{/* 标签导航 */}
<TabNavigation />
{/* 内容区域 */}
<Content style={{
margin: '16px',
padding: '24px',
background: token.colorBgContainer,
borderRadius: token.borderRadiusLG,
minHeight: 'calc(100vh - 64px - 48px - 32px)',
overflow: 'auto'
}}>
{children}
</Content>
</Layout>
</Layout>
);
};
export default AdminLayout;

View File

@@ -0,0 +1,269 @@
import React, { useState, useEffect } from 'react';
import { Tabs, Button, Space, Dropdown, Typography, theme } from 'antd';
import type { TabsProps } from 'antd';
import {
MoreOutlined,
DashboardOutlined,
UserOutlined,
SettingOutlined,
TeamOutlined,
SafetyOutlined,
ToolOutlined,
PlayCircleOutlined,
DollarOutlined,
BellOutlined,
MobileOutlined,
FileTextOutlined
} from '@ant-design/icons';
import { useNavigate, useLocation } from 'react-router-dom';
const { Text } = Typography;
/**
* 标签页项接口
*/
export interface TabItem {
key: string;
label: string;
closable?: boolean;
icon?: React.ReactNode;
content?: React.ReactNode;
}
/**
* 标签式导航组件
* 在主页面中的顶部以浏览器标签的信息展现已经打开的页面
*/
const TabNavigation: React.FC = () => {
const [tabItems, setTabItems] = useState<TabItem[]>([]);
const [activeKey, setActiveKey] = useState<string>('');
const navigate = useNavigate();
const location = useLocation();
const { token } = theme.useToken();
/**
* 路由到标签配置的映射
*/
const routeConfig: Record<string, { label: string; icon: React.ReactNode; closable: boolean }> = {
'/admin/dashboard': { label: '工作台', icon: <DashboardOutlined />, closable: false },
'/admin/system/users': { label: '用户管理', icon: <TeamOutlined />, closable: true },
'/admin/system/roles': { label: '角色管理', icon: <SafetyOutlined />, closable: true },
'/admin/system/permissions': { label: '权限管理', icon: <UserOutlined />, closable: true },
'/admin/system/config': { label: '系统配置', icon: <ToolOutlined />, closable: true },
'/admin/game/servers': { label: '服务器管理', icon: <MobileOutlined />, closable: true },
'/admin/game/goods': { label: '道具管理', icon: <DollarOutlined />, closable: true },
'/admin/game/announcement': { label: '公告管理', icon: <BellOutlined />, closable: true },
'/admin/finance/recharge': { label: '充值记录', icon: <DollarOutlined />, closable: true },
'/admin/finance/order': { label: '订单管理', icon: <FileTextOutlined />, closable: true },
'/admin/report/user': { label: '用户统计', icon: <TeamOutlined />, closable: true },
'/admin/report/finance': { label: '财务统计', icon: <DollarOutlined />, closable: true },
'/admin/report/game': { label: '游戏统计', icon: <PlayCircleOutlined />, closable: true },
'/admin/profile': { label: '个人资料', icon: <UserOutlined />, closable: true },
'/admin/settings': { label: '系统设置', icon: <SettingOutlined />, closable: true }
};
/**
* 初始化和更新标签页
*/
useEffect(() => {
const currentPath = location.pathname;
const config = routeConfig[currentPath];
if (config) {
setTabItems(prevItems => {
// 检查是否已存在该标签
const existingIndex = prevItems.findIndex(item => item.key === currentPath);
if (existingIndex >= 0) {
// 标签已存在,只更新激活状态
const newItems = [...prevItems];
setActiveKey(currentPath);
return newItems;
} else {
// 添加新标签
const newItem: TabItem = {
key: currentPath,
label: config.label,
icon: config.icon,
closable: config.closable
};
const newItems = [...prevItems, newItem];
setActiveKey(currentPath);
return newItems;
}
});
}
}, [location.pathname]);
/**
* 关闭标签页
* @param targetKey - 要关闭的标签key
*/
const handleClose = (targetKey: string) => {
setTabItems(prevItems => {
const itemIndex = prevItems.findIndex(item => item.key === targetKey);
if (itemIndex === -1) return prevItems;
const newItems = prevItems.filter(item => item.key !== targetKey);
// 如果关闭的是当前激活的标签
if (targetKey === activeKey) {
if (newItems.length > 0) {
// 激活策略:优先激活右侧标签,否则激活左侧
const newActiveIndex = itemIndex < newItems.length ? itemIndex : itemIndex - 1;
const newActiveKey = newItems[newActiveIndex]?.key;
if (newActiveKey) {
setActiveKey(newActiveKey);
navigate(newActiveKey);
}
} else {
// 没有标签了,返回工作台
setActiveKey('');
navigate('/admin/dashboard');
}
}
return newItems;
});
};
/**
* 处理标签点击
* @param key - 标签key
*/
const handleTabClick = (key: string) => {
if (key !== activeKey) {
setActiveKey(key);
navigate(key);
}
};
/**
* 获取所有标签项的TabsProps配置
*/
const getTabItems = (): TabsProps['items'] => {
return tabItems.map(item => ({
key: item.key,
label: (
<Space size="small">
{item.icon}
<span style={{ maxWidth: '120px', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{item.label}
</span>
</Space>
),
closable: item.closable !== false,
onClick: () => handleTabClick(item.key)
}));
};
/**
* 关闭其他标签
*/
const closeOthers = () => {
const currentItem = tabItems.find(item => item.key === activeKey);
if (currentItem && currentItem.closable !== false) {
setTabItems([currentItem]);
}
};
/**
* 关闭所有标签
*/
const closeAll = () => {
const defaultItem = tabItems.find(item => !item.closable);
if (defaultItem) {
setTabItems([defaultItem]);
setActiveKey(defaultItem.key);
navigate(defaultItem.key);
} else {
setTabItems([]);
setActiveKey('');
navigate('/admin/dashboard');
}
};
/**
* 更多操作下拉菜单
*/
const moreMenuItems = [
{
key: 'close-others',
label: '关闭其他',
onClick: closeOthers,
disabled: tabItems.filter(item => item.closable !== false).length <= 1
},
{
key: 'close-all',
label: '关闭所有',
onClick: closeAll,
disabled: tabItems.filter(item => item.closable !== false).length === 0
}
];
// 如果没有标签页,不渲染
if (tabItems.length === 0) {
return null;
}
return (
<div style={{
padding: '8px 24px 0',
background: token.colorBgContainer,
borderBottom: `1px solid ${token.colorBorderSecondary}`
}}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
{/* 标签页区域 */}
<div style={{ flex: 1, overflow: 'hidden' }}>
<Tabs
type="editable-card"
activeKey={activeKey}
items={getTabItems()}
onEdit={(targetKey, action) => {
if (action === 'remove') {
handleClose(targetKey as string);
}
}}
onChange={handleTabClick}
hideAdd
size="small"
style={{
margin: 0
}}
tabBarStyle={{
margin: 0,
background: 'transparent'
}}
/>
</div>
{/* 更多操作按钮 */}
{tabItems.filter(item => item.closable !== false).length > 0 && (
<div style={{ marginLeft: '8px' }}>
<Dropdown
menu={{
items: moreMenuItems,
onClick: ({ key }) => {
const action = moreMenuItems.find(item => item.key === key);
action?.onClick();
}
}}
placement="bottomRight"
arrow
>
<Button
type="text"
icon={<MoreOutlined />}
size="small"
style={{ height: '32px' }}
/>
</Dropdown>
</div>
)}
</div>
</div>
);
};
export default TabNavigation;

View File

@@ -0,0 +1,441 @@
import React from 'react';
import {
Card,
Row,
Col,
Statistic,
Progress,
Typography,
Space,
Button,
Tag,
theme
} from 'antd';
import {
UserOutlined,
DollarOutlined,
TeamOutlined,
WarningOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
ClockCircleOutlined,
TrophyOutlined,
FireOutlined,
ShoppingCartOutlined,
NotificationOutlined,
RiseOutlined
} from '@ant-design/icons';
import { useAuth } from '../contexts/AuthContext';
const { Title, Text } = Typography;
/**
* 模拟数据接口
*/
interface DashboardStats {
totalUsers: number;
activeUsers: number;
todayRevenue: number;
monthlyRevenue: number;
totalServers: number;
onlineServers: number;
}
interface QuickAction {
title: string;
icon: React.ReactNode;
color: string;
action: () => void;
}
interface RecentActivity {
id: string;
user: string;
action: string;
time: string;
status: 'success' | 'warning' | 'error' | 'info';
}
interface SystemAlert {
id: string;
type: 'info' | 'warning' | 'error';
message: string;
time: string;
resolved: boolean;
}
/**
* 默认工作台页面
* 管理员登录后的首页
*/
const AdminDashboard: React.FC = () => {
const { user } = useAuth();
const { token } = theme.useToken();
// 模拟数据
const dashboardStats: DashboardStats = {
totalUsers: 125840,
activeUsers: 8756,
todayRevenue: 125680.50,
monthlyRevenue: 3756000.00,
totalServers: 24,
onlineServers: 23
};
// 快速操作
const quickActions: QuickAction[] = [
{
title: '用户管理',
icon: <UserOutlined />,
color: '#1890ff',
action: () => console.log('跳转到用户管理')
},
{
title: '服务器监控',
icon: <FireOutlined />,
color: '#fa541c',
action: () => console.log('跳转到服务器监控')
},
{
title: '订单处理',
icon: <ShoppingCartOutlined />,
color: '#52c41a',
action: () => console.log('跳转到订单处理')
},
{
title: '系统公告',
icon: <NotificationOutlined />,
color: '#722ed1',
action: () => console.log('跳转到系统公告')
}
];
// 最近活动数据
const recentActivities: RecentActivity[] = [
{
id: '1',
user: '张三',
action: '完成了用户充值审核',
time: '2分钟前',
status: 'success'
},
{
id: '2',
user: '李四',
action: '提交了服务器维护申请',
time: '5分钟前',
status: 'warning'
},
{
id: '3',
user: '王五',
action: '处理了玩家投诉',
time: '10分钟前',
status: 'info'
},
{
id: '4',
user: '赵六',
action: '更新了游戏道具信息',
time: '15分钟前',
status: 'success'
},
{
id: '5',
user: '孙七',
action: '删除了异常订单',
time: '20分钟前',
status: 'error'
}
];
// 系统告警数据
const systemAlerts: SystemAlert[] = [
{
id: '1',
type: 'warning',
message: '服务器 CPU 使用率超过 80%',
time: '5分钟前',
resolved: false
},
{
id: '2',
type: 'info',
message: '今日玩家活跃度较昨日增长 15%',
time: '10分钟前',
resolved: false
},
{
id: '3',
type: 'error',
message: '支付接口响应异常',
time: '30分钟前',
resolved: true
}
];
// 获取状态图标
const getStatusIcon = (status: RecentActivity['status']) => {
switch (status) {
case 'success':
return <CheckCircleOutlined style={{ color: '#52c41a' }} />;
case 'warning':
return <WarningOutlined style={{ color: '#faad14' }} />;
case 'error':
return <CloseCircleOutlined style={{ color: '#ff4d4f' }} />;
case 'info':
default:
return <ClockCircleOutlined style={{ color: '#1890ff' }} />;
}
};
// 获取告警标签颜色
const getAlertColor = (type: SystemAlert['type']) => {
switch (type) {
case 'error':
return 'error';
case 'warning':
return 'warning';
case 'info':
default:
return 'processing';
}
};
return (
<div style={{ padding: '0' }}>
{/* 欢迎标题 */}
<div style={{ marginBottom: '24px' }}>
<Title level={2} style={{ margin: 0, color: token.colorText }}>
{user?.real_name || user?.username}
</Title>
<Text type="secondary">
{new Date().toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long'
})}
</Text>
</div>
{/* 统计卡片 */}
<Row gutter={[16, 16]} style={{ marginBottom: '24px' }}>
<Col xs={24} sm={12} lg={6}>
<Card>
<Statistic
title="总用户数"
value={dashboardStats.totalUsers}
prefix={<UserOutlined style={{ color: '#1890ff' }} />}
suffix="人"
styles={{ content: { color: '#1890ff' } }}
/>
<div style={{ marginTop: '8px' }}>
<Text type="secondary" style={{ fontSize: '12px' }}>
<Text type="success">+12.5%</Text>
</Text>
</div>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card>
<Statistic
title="在线用户"
value={dashboardStats.activeUsers}
prefix={<TeamOutlined style={{ color: '#52c41a' }} />}
suffix="人"
styles={{ content: { color: '#52c41a' } }}
/>
<div style={{ marginTop: '8px' }}>
<Progress
percent={Math.round((dashboardStats.activeUsers / dashboardStats.totalUsers) * 100)}
size="small"
showInfo={false}
strokeColor="#52c41a"
/>
</div>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card>
<Statistic
title="今日收入"
value={dashboardStats.todayRevenue}
prefix={<DollarOutlined style={{ color: '#fa8c16' }} />}
suffix="元"
precision={2}
styles={{ content: { color: '#fa8c16' } }}
/>
<div style={{ marginTop: '8px' }}>
<Text type="secondary" style={{ fontSize: '12px' }}>
<Text type="success">+8.3%</Text>
</Text>
</div>
</Card>
</Col>
<Col xs={24} sm={12} lg={6}>
<Card>
<Statistic
title="月度收入"
value={dashboardStats.monthlyRevenue}
prefix={<RiseOutlined style={{ color: '#722ed1' }} />}
suffix="元"
precision={2}
styles={{ content: { color: '#722ed1' } }}
/>
<div style={{ marginTop: '8px' }}>
<Text type="secondary" style={{ fontSize: '12px' }}>
<Text type="success">85.6%</Text>
</Text>
</div>
</Card>
</Col>
</Row>
{/* 主要内容区域 */}
<Row gutter={[16, 16]}>
{/* 快速操作 */}
<Col xs={24} lg={8}>
<Card title="快速操作" style={{ height: '100%' }}>
<Row gutter={[16, 16]}>
{quickActions.map((action, index) => (
<Col xs={12} key={index}>
<Button
type="default"
icon={action.icon}
style={{
height: '80px',
width: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
borderColor: action.color,
color: action.color
}}
onClick={action.action}
>
<div style={{ fontSize: '18px', marginBottom: '4px' }}>
{action.icon}
</div>
<span style={{ fontSize: '12px' }}>{action.title}</span>
</Button>
</Col>
))}
</Row>
</Card>
</Col>
{/* 最近活动 */}
<Col xs={24} lg={8}>
<Card title="最近活动" style={{ height: '100%' }}>
<div style={{ maxHeight: '300px', overflow: 'auto' }}>
{recentActivities.map((item) => (
<div key={item.id} style={{
padding: '12px 0',
borderBottom: '1px solid #f0f0f0',
display: 'flex',
alignItems: 'flex-start',
gap: '8px'
}}>
<div style={{ marginTop: '2px' }}>
{getStatusIcon(item.status)}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{
display: 'flex',
justifyContent: 'space-between',
marginBottom: '4px'
}}>
<Text strong style={{ fontSize: '13px' }}>{item.user}</Text>
<Text type="secondary" style={{ fontSize: '12px' }}>{item.time}</Text>
</div>
<Text type="secondary" style={{ fontSize: '12px' }}>
{item.action}
</Text>
</div>
</div>
))}
</div>
<div style={{ textAlign: 'center', marginTop: '16px' }}>
<Button type="link" size="small"></Button>
</div>
</Card>
</Col>
{/* 系统状态 */}
<Col xs={24} lg={8}>
<Card title="系统状态" style={{ height: '100%' }}>
<div style={{ marginBottom: '16px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
<Text>线</Text>
<Text strong>
{Math.round((dashboardStats.onlineServers / dashboardStats.totalServers) * 100)}%
</Text>
</div>
<Progress
percent={Math.round((dashboardStats.onlineServers / dashboardStats.totalServers) * 100)}
strokeColor={dashboardStats.onlineServers === dashboardStats.totalServers ? '#52c41a' : '#faad14'}
size="small"
/>
</div>
<div style={{ marginBottom: '16px' }}>
<Text strong style={{ fontSize: '13px' }}></Text>
</div>
<div style={{ maxHeight: '200px', overflow: 'auto' }}>
{systemAlerts.map((alert) => (
<div key={alert.id} style={{ padding: '8px 0', display: 'flex', alignItems: 'center', gap: '8px' }}>
<Tag color={getAlertColor(alert.type)} style={{ margin: 0 }}>
{alert.type === 'error' ? '错误' : alert.type === 'warning' ? '警告' : '信息'}
</Tag>
<Text style={{ fontSize: '12px', flex: 1 }}>
{alert.message}
</Text>
<Text type="secondary" style={{ fontSize: '11px' }}>
{alert.time}
</Text>
</div>
))}
</div>
<div style={{ textAlign: 'center', marginTop: '16px' }}>
<Button type="link" size="small"></Button>
</div>
</Card>
</Col>
</Row>
{/* 底部信息 */}
<Row style={{ marginTop: '24px' }}>
<Col span={24}>
<Card>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Space>
<TrophyOutlined style={{ color: '#faad14', fontSize: '18px' }} />
<div>
<Text strong></Text>
<br />
<Text type="secondary" style={{ fontSize: '12px' }}>
- 1,256
</Text>
</div>
</Space>
<Space>
<Text type="secondary" style={{ fontSize: '12px' }}>
v1.0.0 | 2025-12-12
</Text>
</Space>
</div>
</Card>
</Col>
</Row>
</div>
);
};
export default AdminDashboard;

View File

@@ -1,7 +1,8 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { Form, Input, Button, Card, Typography, message, Space, Checkbox } from 'antd';
import { UserOutlined, LockOutlined, SafetyOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../contexts/AuthContext';
const { Title, Text } = Typography;
@@ -11,11 +12,20 @@ const { Title, Text } = Typography;
*/
const AdminLogin: React.FC = () => {
const [loading, setLoading] = useState(false);
const { login, isAuthenticated } = useAuth();
const navigate = useNavigate();
const [form] = Form.useForm();
// 如果已登录,重定向到管理后台
useEffect(() => {
if (isAuthenticated) {
navigate('/admin/dashboard', { replace: true });
}
}, [isAuthenticated, navigate]);
/**
* 处理登录表单提交
* @param values - 表单值(故意未使用)
* @param values - 表单值
*/
const handleLogin = async (values: {
username: string;
@@ -24,19 +34,13 @@ const AdminLogin: React.FC = () => {
}) => {
setLoading(true);
try {
// 模拟登录请求
await new Promise(resolve => setTimeout(resolve, 1200));
const success = await login(values.username, values.password);
// 这里应该是实际的API调用
// const response = await loginAdmin(values);
// 模拟登录成功
message.success('登录成功!欢迎进入运营管理系统!');
navigate('/admin/dashboard');
} catch {
// 故意使用 values 参数以避免 TypeScript 未使用警告
void values;
message.error('登录失败,请检查用户名和密码!');
if (success) {
navigate('/admin/dashboard', { replace: true });
}
} catch (error) {
// 错误已在AuthContext中处理
} finally {
setLoading(false);
}