diff --git a/sql/init_admin_users.sql b/sql/init_admin_users.sql new file mode 100644 index 0000000..143c524 --- /dev/null +++ b/sql/init_admin_users.sql @@ -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. 部署到生产环境时,请修改默认密码 +-- ===================================================== \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 740e6ba..fecf287 100644 --- a/src/App.tsx +++ b/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 ( - - - {/* 首页导航 */} - } /> - - {/* 玩家服务中心登录页面 */} - } /> - - {/* 运营管理系统后台登录页面 */} - } /> - - {/* 默认重定向到首页 */} - } /> - - + + + + {/* 首页导航 */} + } /> + + {/* 玩家服务中心登录页面 */} + } /> + + {/* 运营管理系统后台登录页面 */} + } /> + + {/* 后台管理系统路由(需要认证) */} + + + + } /> + + {/* 默认重定向到首页 */} + } /> + + + ); diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx new file mode 100644 index 0000000..bd742cf --- /dev/null +++ b/src/contexts/AuthContext.tsx @@ -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; + logout: () => void; +} + +// 创建认证上下文 +const AuthContext = createContext(undefined); + +// 模拟API调用 +const mockApiCall = { + login: async (username: string, password: string): Promise => { + // 模拟网络延迟 + 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(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 - 登录是否成功 + */ + const login = async (username: string, password: string): Promise => { + 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 ( + + {children} + + ); +}; + +/** + * 使用认证上下文的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 ( +
+ 正在验证身份... +
+ ); + } + + if (!isAuthenticated) { + return fallback ||
请先登录
; + } + + return <>{children}; +}; + +export default AuthContext; \ No newline at end of file diff --git a/src/layouts/AdminLayout.tsx b/src/layouts/AdminLayout.tsx new file mode 100644 index 0000000..4346b4c --- /dev/null +++ b/src/layouts/AdminLayout.tsx @@ -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: , + label: '工作台' + }, + { + key: 'system', + icon: , + label: '系统管理', + children: [ + { + key: '/admin/system/users', + icon: , + label: '用户管理' + }, + { + key: '/admin/system/roles', + icon: , + label: '角色管理' + }, + { + key: '/admin/system/permissions', + icon: , + label: '权限管理' + }, + { + key: '/admin/system/config', + icon: , + label: '系统配置' + } + ] + }, + { + key: 'game', + icon: , + label: '游戏管理', + children: [ + { + key: '/admin/game/servers', + icon: , + label: '服务器管理' + }, + { + key: '/admin/game/goods', + icon: , + label: '道具管理' + }, + { + key: '/admin/game/announcement', + icon: , + label: '公告管理' + } + ] + }, + { + key: 'finance', + icon: , + label: '财务管理', + children: [ + { + key: '/admin/finance/recharge', + icon: , + label: '充值记录' + }, + { + key: '/admin/finance/order', + icon: , + label: '订单管理' + } + ] + }, + { + key: 'report', + icon: , + label: '数据统计', + children: [ + { + key: '/admin/report/user', + icon: , + label: '用户统计' + }, + { + key: '/admin/report/finance', + icon: , + label: '财务统计' + }, + { + key: '/admin/report/game', + icon: , + 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: , + label: '个人资料' + }, + { + key: 'settings', + icon: , + label: '系统设置' + }, + { + type: 'divider' + }, + { + key: 'logout', + icon: , + 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 = ( +
+ {/* Logo 区域 */} +
+ {!collapsed ? ( + +
+ +
+ + 运营管理系统 + +
+ ) : ( +
+ +
+ )} +
+ + {/* 菜单区域 */} +
+ +
+
+ ); + + return ( + + {/* 侧边栏 - 桌面端 */} + {!isMobile && ( + + {siderContent} + + )} + + {/* 侧边栏 - 移动端 Drawer */} + setMobileDrawerVisible(false)} + open={mobileDrawerVisible} + styles={{ body: { padding: 0 } }} + size="default" + > + {siderContent} + + + + {/* 头部 */} +
+ {/* 左侧控制区 */} + + {/* 折叠按钮 */} +
+ + {/* 标签导航 */} + + + {/* 内容区域 */} + + {children} + +
+
+ ); +}; + +export default AdminLayout; \ No newline at end of file diff --git a/src/layouts/TabNavigation.tsx b/src/layouts/TabNavigation.tsx new file mode 100644 index 0000000..cc3cd47 --- /dev/null +++ b/src/layouts/TabNavigation.tsx @@ -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([]); + const [activeKey, setActiveKey] = useState(''); + const navigate = useNavigate(); + const location = useLocation(); + const { token } = theme.useToken(); + + /** + * 路由到标签配置的映射 + */ + const routeConfig: Record = { + '/admin/dashboard': { label: '工作台', icon: , closable: false }, + '/admin/system/users': { label: '用户管理', icon: , closable: true }, + '/admin/system/roles': { label: '角色管理', icon: , closable: true }, + '/admin/system/permissions': { label: '权限管理', icon: , closable: true }, + '/admin/system/config': { label: '系统配置', icon: , closable: true }, + '/admin/game/servers': { label: '服务器管理', icon: , closable: true }, + '/admin/game/goods': { label: '道具管理', icon: , closable: true }, + '/admin/game/announcement': { label: '公告管理', icon: , closable: true }, + '/admin/finance/recharge': { label: '充值记录', icon: , closable: true }, + '/admin/finance/order': { label: '订单管理', icon: , closable: true }, + '/admin/report/user': { label: '用户统计', icon: , closable: true }, + '/admin/report/finance': { label: '财务统计', icon: , closable: true }, + '/admin/report/game': { label: '游戏统计', icon: , closable: true }, + '/admin/profile': { label: '个人资料', icon: , closable: true }, + '/admin/settings': { label: '系统设置', icon: , 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: ( + + {item.icon} + + {item.label} + + + ), + 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 ( +
+
+ {/* 标签页区域 */} +
+ { + if (action === 'remove') { + handleClose(targetKey as string); + } + }} + onChange={handleTabClick} + hideAdd + size="small" + style={{ + margin: 0 + }} + tabBarStyle={{ + margin: 0, + background: 'transparent' + }} + /> +
+ + {/* 更多操作按钮 */} + {tabItems.filter(item => item.closable !== false).length > 0 && ( +
+ { + const action = moreMenuItems.find(item => item.key === key); + action?.onClick(); + } + }} + placement="bottomRight" + arrow + > +
+ )} +
+
+ ); +}; + +export default TabNavigation; \ No newline at end of file diff --git a/src/pages/AdminDashboard.tsx b/src/pages/AdminDashboard.tsx new file mode 100644 index 0000000..7e5dd8e --- /dev/null +++ b/src/pages/AdminDashboard.tsx @@ -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: , + color: '#1890ff', + action: () => console.log('跳转到用户管理') + }, + { + title: '服务器监控', + icon: , + color: '#fa541c', + action: () => console.log('跳转到服务器监控') + }, + { + title: '订单处理', + icon: , + color: '#52c41a', + action: () => console.log('跳转到订单处理') + }, + { + title: '系统公告', + icon: , + 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 ; + case 'warning': + return ; + case 'error': + return ; + case 'info': + default: + return ; + } + }; + + // 获取告警标签颜色 + const getAlertColor = (type: SystemAlert['type']) => { + switch (type) { + case 'error': + return 'error'; + case 'warning': + return 'warning'; + case 'info': + default: + return 'processing'; + } + }; + + return ( +
+ {/* 欢迎标题 */} +
+ + 欢迎回来,{user?.real_name || user?.username}! + + + 今天是 {new Date().toLocaleDateString('zh-CN', { + year: 'numeric', + month: 'long', + day: 'numeric', + weekday: 'long' + })} + +
+ + {/* 统计卡片 */} + + + + } + suffix="人" + styles={{ content: { color: '#1890ff' } }} + /> +
+ + 较上月增长 +12.5% + +
+
+ + + + + } + suffix="人" + styles={{ content: { color: '#52c41a' } }} + /> +
+ +
+
+ + + + + } + suffix="元" + precision={2} + styles={{ content: { color: '#fa8c16' } }} + /> +
+ + 较昨日 +8.3% + +
+
+ + + + + } + suffix="元" + precision={2} + styles={{ content: { color: '#722ed1' } }} + /> +
+ + 目标完成度 85.6% + +
+
+ +
+ + {/* 主要内容区域 */} + + {/* 快速操作 */} + + + + {quickActions.map((action, index) => ( + + + + ))} + + + + + {/* 最近活动 */} + + +
+ {recentActivities.map((item) => ( +
+
+ {getStatusIcon(item.status)} +
+
+
+ {item.user} + {item.time} +
+ + {item.action} + +
+
+ ))} +
+
+ +
+
+ + + {/* 系统状态 */} + + +
+
+ 服务器在线率 + + {Math.round((dashboardStats.onlineServers / dashboardStats.totalServers) * 100)}% + +
+ +
+ +
+ 系统告警 +
+ +
+ {systemAlerts.map((alert) => ( +
+ + {alert.type === 'error' ? '错误' : alert.type === 'warning' ? '警告' : '信息'} + + + {alert.message} + + + {alert.time} + +
+ ))} +
+
+ +
+
+ +
+ + {/* 底部信息 */} + + + +
+ + +
+ 本月运营之星 +
+ + 李明 - 完成订单处理 1,256 单 + +
+
+ + + + 系统版本:v1.0.0 | 最后更新:2025-12-12 + + +
+
+ +
+
+ ); +}; + +export default AdminDashboard; \ No newline at end of file diff --git a/src/pages/AdminLogin.tsx b/src/pages/AdminLogin.tsx index 7c900b8..b8de0e0 100644 --- a/src/pages/AdminLogin.tsx +++ b/src/pages/AdminLogin.tsx @@ -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); }