feat: ✨ 运营管理系统后台登录鉴权及后台框架开发
This commit is contained in:
96
sql/init_admin_users.sql
Normal file
96
sql/init_admin_users.sql
Normal 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. 部署到生产环境时,请修改默认密码
|
||||
-- =====================================================
|
||||
45
src/App.tsx
45
src/App.tsx
@@ -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>
|
||||
);
|
||||
|
||||
186
src/contexts/AuthContext.tsx
Normal file
186
src/contexts/AuthContext.tsx
Normal 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
440
src/layouts/AdminLayout.tsx
Normal 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;
|
||||
269
src/layouts/TabNavigation.tsx
Normal file
269
src/layouts/TabNavigation.tsx
Normal 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;
|
||||
441
src/pages/AdminDashboard.tsx
Normal file
441
src/pages/AdminDashboard.tsx
Normal 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;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user