项目架构重构

This commit is contained in:
Stev_Wang
2025-12-12 23:25:25 +08:00
parent 6485c98414
commit 8e26166b44
32 changed files with 0 additions and 10808 deletions

75
.czrc
View File

@@ -1,75 +0,0 @@
{
"$schema": "https://cdn.jsdelivr.net/npm/cz-git@1.12.0/schema/cz-git.json",
"path": "node_modules/cz-git",
"messages": {
"type": "选择你要提交的类型 :",
"scope": "选择一个提交范围(可选):",
"customScope": "请输入自定义的提交范围 :",
"subject": "填写简短精炼的变更描述 :",
"body": "填写详细描述(可选)。使用 '|' 换行 :",
"breaking": "列出任何突破性变更(可选)。使用 '|' 换行 :",
"footerPrefixesSelect": "选择关联issue前缀可选:",
"customFooterPrefix": "输入自定义issue前缀 :",
"footer": "填写关联issue (可选) 例如: #123, #456 :",
"confirmCommit": "确认提交?"
},
"types": [
{
"value": "feat",
"name": "feat: 新增功能",
"emoji": "✨"
},
{
"value": "fix",
"name": "fix: 修复缺陷",
"emoji": "🐛"
},
{
"value": "docs",
"name": "docs: 文档更新",
"emoji": "📝"
},
{
"value": "style",
"name": "style: 代码格式",
"emoji": "💄"
},
{
"value": "refactor",
"name": "refactor: 代码重构",
"emoji": "♻️"
},
{
"value": "perf",
"name": "perf: 性能优化",
"emoji": "⚡️"
},
{
"value": "test",
"name": "test: 测试相关",
"emoji": "🧪"
},
{
"value": "build",
"name": "build: 构建相关",
"emoji": "🏗️"
},
{
"value": "ci",
"name": "ci: 持续集成",
"emoji": "🔧"
},
{
"value": "chore",
"name": "chore: 其他修改",
"emoji": "📌"
},
{
"value": "revert",
"name": "revert: 回退代码",
"emoji": "⏪️"
}
],
"useEmoji": true,
"emojiAlign": "center"
}

View File

@@ -1,95 +0,0 @@
{
"cz-config": {
"enableMultipleScopes": true,
"scopes": {
"auth": "用户认证相关",
"user": "用户管理相关",
"game": "游戏功能相关",
"admin": "管理员功能相关",
"api": "API接口相关",
"ui": "用户界面相关",
"build": "构建系统相关",
"deps": "依赖管理相关",
"docs": "文档更新相关",
"style": "样式调整相关",
"test": "测试相关",
"refactor": "代码重构相关",
"perf": "性能优化相关",
"ci": "持续集成相关",
"other": "其他类型"
},
"types": [
{
"value": "feat",
"name": "✨ 功能特性: 新增功能或特性",
"detail": "代码实现了一个新的功能或特性包括新增页面、组件、API等"
},
{
"value": "fix",
"name": "🐛 问题修复: 修复已知问题",
"detail": "修复了代码中的错误、缺陷或异常行为"
},
{
"value": "docs",
"name": "📝 文档更新: 更新项目文档",
"detail": "更新了项目文档、README、注释等不涉及代码变更"
},
{
"value": "style",
"name": "🎨 样式调整: 代码格式美化",
"detail": "调整代码格式、样式、缩进、空格等,不影响功能逻辑"
},
{
"value": "refactor",
"name": "♻️ 重构优化: 代码结构重构",
"detail": "重构代码结构、简化逻辑、提升性能,但不改变功能"
},
{
"value": "perf",
"name": "⚡ 性能优化: 提升代码性能",
"detail": "优化代码性能、减少内存使用、提升运行速度"
},
{
"value": "test",
"name": "🧪 测试相关: 添加或修改测试",
"detail": "添加、修改或删除测试用例"
},
{
"value": "build",
"name": "🏗️ 构建系统: 构建配置变更",
"detail": "修改构建配置、依赖关系或构建脚本"
},
{
"value": "ci",
"name": "🚀 持续集成: CI/CD 流程更新",
"detail": "修改持续集成配置、部署流程等"
},
{
"value": "deps",
"name": "📦 依赖更新: 第三方依赖变更",
"detail": "添加、更新或移除第三方包依赖"
},
{
"value": "chore",
"name": "🔧 辅助工具: 工具或配置更新",
"detail": "修改构建脚本、开发工具、配置文件等"
},
{
"value": "revert",
"name": "⏪ 回滚操作: 撤销之前的变更",
"detail": "回滚到之前的代码版本或配置"
},
{
"value": "release",
"name": "🎉 发布版本: 版本发布",
"detail": "正式发布新版本或里程碑版本"
}
],
"subjectMaxLength": 72,
"bodyMaxLineWidth": 100,
"confirmColorize": true,
"defaultBody": "",
"defaultScope": "",
"defaultSubject": ""
}
}

View File

@@ -1,23 +0,0 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

View File

@@ -1,13 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>mhxy-web</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

6089
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,40 +0,0 @@
{
"name": "mhxy-web",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@ant-design/icons": "^6.1.0",
"antd": "^6.1.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-router-dom": "^7.10.1"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/node": "^24.10.1",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"commitizen": "^4.3.1",
"cz-git": "^1.9.0",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.46.4",
"vite": "^7.2.4"
},
"config": {
"commitizen": {
"path": "node_modules/cz-git"
}
}
}

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,96 +0,0 @@
-- =====================================================
-- 梦幻西游一站式运营管理系统 - 后台用户表初始化脚本
-- 适用于 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

@@ -1,98 +0,0 @@
-- =============================================
-- 梦幻西游运营管理系统 - 系统配置数据插入脚本 (兼容版)
-- 版本: v1.0.5
-- 创建日期: 2025-12-12
-- 描述: 插入系统配置数据,完全兼容现有表结构
-- =============================================
-- 切换到目标数据库
USE mhxy_web;
-- =============================================
-- 1. 创建配置历史记录表 (如果不存在)
-- =============================================
CREATE TABLE IF NOT EXISTS system_config_history (
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '记录ID',
config_key VARCHAR(100) NOT NULL COMMENT '配置键名',
old_value TEXT COMMENT '原配置值',
new_value TEXT COMMENT '新配置值',
changed_by INT COMMENT '修改人用户ID',
changed_reason VARCHAR(500) COMMENT '修改原因',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
INDEX idx_config_key (config_key),
INDEX idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统配置历史记录表';
-- =============================================
-- 2. 创建配置缓存表 (如果不存在)
-- =============================================
CREATE TABLE IF NOT EXISTS system_config_cache (
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '缓存ID',
cache_key VARCHAR(100) NOT NULL UNIQUE COMMENT '缓存键',
cache_value LONGTEXT COMMENT '缓存值(JSON格式)',
expires_at TIMESTAMP COMMENT '过期时间',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
INDEX idx_cache_key (cache_key),
INDEX idx_expires_at (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统配置缓存表';
-- =============================================
-- 3. 清空并重新插入配置数据
-- =============================================
-- 先删除所有现有配置数据
DELETE FROM system_config;
-- 插入基本配置 (使用现有表字段)
INSERT INTO system_config (config_key, config_value, config_desc, config_group) VALUES
('site_name', '梦幻西游一站式运营管理系统', '系统显示名称', 'basic'),
('site_version', '1.0.0', '当前系统版本号', 'basic'),
('site_description', '专业的游戏运营管理平台', '系统描述信息', 'basic'),
('admin_email', 'admin@mhxy.com', '系统管理员联系邮箱', 'basic'),
('maintenance_mode', '0', '开启后用户无法正常访问系统', 'basic'),
('default_language', 'zh-CN', '系统默认语言设置', 'basic');
-- 插入安全配置
INSERT INTO system_config (config_key, config_value, config_desc, config_group) VALUES
('jwt_secret', 'JWT_SECRET_32_BYTE_RANDOM_STRING_2025', '用于JWT令牌签名的密钥建议32位字符', 'security'),
('jwt_expires_in', '24', 'JWT访问令牌的有效期单位小时', 'security'),
('jwt_refresh_expires_in', '168', 'JWT刷新令牌的有效期单位小时', 'security'),
('login_attempt_limit', '5', '连续登录失败次数限制', 'security'),
('session_timeout', '30', '用户会话超时时间', 'security'),
('password_min_length', '6', '用户密码最小长度要求', 'security'),
('enable_2fa', '0', '是否启用双因子认证功能', 'security');
-- 插入游戏通信配置
INSERT INTO system_config (config_key, config_value, config_desc, config_group) VALUES
('game_server_api', 'http://127.0.0.1:8080/tool/http', '游戏服务端HTTP接口地址', 'game'),
('game_server_psk', 'THIS_IS_A_32_BYTE_FIXED_PSK!!!!!', '游戏服务端预共享密钥用于API认证', 'game'),
('game_server_timeout', '30', '与游戏服务端通信的超时时间', 'game'),
('game_server_retry_count', '3', 'API请求失败时的重试次数', 'game'),
('player_auto_register', '1', '新玩家是否自动创建账号', 'game'),
('game_log_level', 'info', '游戏相关操作的日志记录级别', 'game');
-- =============================================
-- 4. 验证结果
-- =============================================
-- 检查当前表结构
DESCRIBE system_config;
-- 检查各组配置数量
SELECT config_group, COUNT(*) as config_count
FROM system_config
GROUP BY config_group;
-- 检查关键配置
SELECT config_key, config_value, config_desc
FROM system_config
WHERE config_key IN ('jwt_secret', 'game_server_api', 'game_server_psk');
-- 检查新表是否创建成功
SHOW TABLES LIKE 'system_config_history';
SHOW TABLES LIKE 'system_config_cache';
-- 完成提示
SELECT 'System config data inserted successfully with compatible fields!' as message;

View File

@@ -1,40 +0,0 @@
#root {
min-height: 100vh;
width: 100%;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

View File

@@ -1,61 +0,0 @@
import { StrictMode } from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
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 SystemConfigPage from './pages/SystemConfigPage';
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}>
<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="/admin/system/config" element={
<AdminLayout>
<SystemConfigPage />
</AdminLayout>
} />
{/* 默认重定向到首页 */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</Router>
</AuthProvider>
</ConfigProvider>
</StrictMode>
);
}
export default App;

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -1,186 +0,0 @@
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;

View File

@@ -1,67 +0,0 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

View File

@@ -1,494 +0,0 @@
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 } = 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 [isMobile, setIsMobile] = useState(false);
const { user, logout, isLoading } = useAuth();
const navigate = useNavigate();
const location = useLocation();
const { token } = theme.useToken();
// 认证状态检查
useEffect(() => {
if (!isLoading && !user) {
navigate('/admin', { replace: true });
}
}, [user, navigate, isLoading]);
// 响应式检测
useEffect(() => {
const checkIsMobile = () => {
setIsMobile(window.innerWidth < 768);
if (window.innerWidth >= 768) {
setMobileDrawerVisible(false);
}
};
checkIsMobile();
window.addEventListener('resize', checkIsMobile);
return () => window.removeEventListener('resize', checkIsMobile);
}, []);
// 加载状态
if (isLoading) {
return (
<div style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
fontSize: '16px',
color: token.colorText
}}>
...
</div>
);
}
/**
* 处理用户下拉菜单点击
*/
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',
height: '100%'
}}>
<Menu
mode="inline"
selectedKeys={getSelectedKeys()}
defaultOpenKeys={getOpenKeys()}
items={menuItems}
onClick={handleMenuClick}
style={{
height: '100%',
borderRight: 0,
background: 'transparent'
}}
/>
</div>
</div>
);
return (
<div style={{ minHeight: '100vh', position: 'relative' }}>
{/* 侧边栏 - 桌面端 */}
{!isMobile && (
<Sider
trigger={null}
collapsible
collapsed={collapsed}
width={240}
style={{
background: token.colorBgContainer,
borderRight: `1px solid ${token.colorBorderSecondary}`,
position: 'fixed',
left: 0,
top: 0, // 从页面顶部开始
bottom: 0,
zIndex: 99,
overflow: 'hidden'
}}
className="admin-sider"
>
{siderContent}
</Sider>
)}
{/* 侧边栏 - 移动端 Drawer */}
<Drawer
title="菜单"
placement="left"
onClose={() => setMobileDrawerVisible(false)}
open={mobileDrawerVisible}
styles={{ body: { padding: 0 } }}
size="default"
>
{siderContent}
</Drawer>
{/* 头部导航栏 - 固定定位 */}
<Header style={{
padding: '0 24px',
background: token.colorBgContainer,
borderBottom: `1px solid ${token.colorBorderSecondary}`,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
position: 'fixed',
top: 0,
left: !isMobile ? (collapsed ? 80 : 240) : 0,
right: 0,
height: '64px',
zIndex: 100,
transition: 'left 0.2s'
}}>
{/* 左侧控制区 */}
<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>
{/* 标签导航 - 固定在头部下方 */}
<div style={{
position: 'fixed',
top: '64px',
left: !isMobile ? (collapsed ? 80 : 240) : 0,
right: 0,
zIndex: 90,
background: token.colorBgContainer,
borderBottom: `1px solid ${token.colorBorderSecondary}`,
transition: 'left 0.2s'
}}>
<TabNavigation />
</div>
{/* 内容区域 */}
<div style={{
padding: '24px',
background: token.colorBgContainer,
minHeight: 'calc(100vh - 112px)',
overflow: 'auto',
marginTop: '112px', // 头部64px + 标签导航48px
marginLeft: !isMobile ? (collapsed ? 80 : 240) : 0,
transition: 'margin-left 0.2s'
}}>
<div style={{
background: token.colorBgContainer,
borderRadius: token.borderRadiusLG,
minHeight: 'calc(100vh - 112px)',
padding: '24px'
}}>
{children}
</div>
</div>
</div>
);
};
export default AdminLayout;

View File

@@ -1,331 +0,0 @@
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { Tabs, Button, Space, Dropdown, theme } from 'antd';
import type { TabsProps } from 'antd';
import {
MoreOutlined,
DashboardOutlined,
ToolOutlined
} 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();
/**
* 路由配置的类型定义
*/
interface RouteConfig {
[path: string]: {
label: string;
icon: React.ReactNode;
closable: boolean;
};
}
/**
* 路由到标签配置的映射
* 只包含实际存在的路由避免404错误
* 使用useMemo避免每次渲染都重新创建对象
*/
const routeConfig: RouteConfig = useMemo(() => ({
'/admin/dashboard': { label: '工作台', icon: <DashboardOutlined />, closable: false },
'/admin/system/config': { label: '系统配置', icon: <ToolOutlined />, closable: true }
}), []);
/**
* 计算当前应该显示的标签项
* 基于当前路径和现有标签计算新标签列表
* 修复React Compiler警告添加tabItems依赖项
*/
const computedTabItems = useMemo(() => {
const currentPath = location.pathname;
const config = routeConfig[currentPath];
// 创建工作台标签(始终存在)
const dashboardItem: TabItem = {
key: '/admin/dashboard',
label: routeConfig['/admin/dashboard'].label,
icon: routeConfig['/admin/dashboard'].icon,
closable: routeConfig['/admin/dashboard'].closable
};
// 特殊处理:工作台页面始终只显示工作台标签
if (currentPath === '/admin/dashboard') {
return [dashboardItem];
}
if (config) {
// 检查当前路径是否已存在
const existingItem = tabItems.find(item => item.key === currentPath);
if (existingItem) {
// 标签已存在,确保工作台在第一位,其他标签保持顺序
const otherItems = tabItems.filter(item =>
item.key !== '/admin/dashboard' &&
item.key !== currentPath &&
routeConfig[item.key] // 确保是配置的路由
);
return [dashboardItem, ...otherItems, existingItem];
} else {
// 添加新标签,确保工作台在第一位
const otherItems = tabItems.filter(item =>
item.key !== '/admin/dashboard' &&
item.key !== currentPath &&
routeConfig[item.key] // 确保是配置的路由
);
const currentItem: TabItem = {
key: currentPath,
label: config.label,
icon: config.icon,
closable: config.closable
};
return [dashboardItem, currentItem, ...otherItems];
}
} else {
// 当前路径不在路由配置中,只显示工作台标签
return [dashboardItem];
}
}, [location.pathname, routeConfig, tabItems]);
/**
* 当computedTabItems变化时更新状态
* 使用setTimeout确保在渲染完成后执行
*/
useEffect(() => {
const timeoutId = setTimeout(() => {
setTabItems(computedTabItems);
setActiveKey(location.pathname);
}, 0);
return () => clearTimeout(timeoutId);
}, [computedTabItems, location.pathname]);
/**
* 关闭标签页
* @param targetKey - 要关闭的标签key
*/
const handleClose = useCallback((targetKey: string) => {
// 工作台标签不能关闭
if (targetKey === '/admin/dashboard') {
return;
}
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) {
// 使用setTimeout确保在渲染完成后执行导航
setTimeout(() => {
setActiveKey(newActiveKey);
navigate(newActiveKey);
}, 0);
}
} else {
// 没有标签了,返回工作台
const dashboardItem = prevItems.find(item => item.key === '/admin/dashboard');
if (dashboardItem) {
setTimeout(() => {
setActiveKey(dashboardItem.key);
navigate(dashboardItem.key);
}, 0);
} else {
setTimeout(() => {
setActiveKey('/admin/dashboard');
navigate('/admin/dashboard');
}, 0);
}
}
}
return newItems;
});
}, [activeKey, navigate]);
/**
* 处理标签点击
* @param key - 标签key
*/
const handleTabClick = useCallback((key: string) => {
if (key !== activeKey) {
setTimeout(() => {
setActiveKey(key);
navigate(key);
}, 0);
}
}, [activeKey, navigate]);
/**
* 获取所有标签项的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 = useCallback(() => {
const currentItem = tabItems.find(item => item.key === activeKey);
if (currentItem && currentItem.closable !== false) {
// 确保工作台始终保留
const dashboardItem = tabItems.find(item => item.key === '/admin/dashboard');
if (dashboardItem && currentItem.key !== '/admin/dashboard') {
setTabItems([dashboardItem, currentItem]);
} else {
setTabItems([currentItem]);
}
}
}, [activeKey, tabItems]);
/**
* 关闭所有标签
*/
const closeAll = useCallback(() => {
const dashboardItem = tabItems.find(item => item.key === '/admin/dashboard');
if (dashboardItem) {
setTabItems([dashboardItem]);
setTimeout(() => {
setActiveKey(dashboardItem.key);
navigate(dashboardItem.key);
}, 0);
} else {
// 如果没有工作台,创建一个
const newDashboardItem: TabItem = {
key: '/admin/dashboard',
label: routeConfig['/admin/dashboard'].label,
icon: routeConfig['/admin/dashboard'].icon,
closable: routeConfig['/admin/dashboard'].closable
};
setTabItems([newDashboardItem]);
setTimeout(() => {
setActiveKey('/admin/dashboard');
navigate('/admin/dashboard');
}, 0);
}
}, [navigate, tabItems, routeConfig]);
/**
* 更多操作下拉菜单
*/
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

@@ -1,11 +0,0 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import 'antd/dist/reset.css'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

@@ -1,441 +0,0 @@
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,195 +0,0 @@
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;
/**
* 运营管理系统后台登录页面
* 路径:/admin
*/
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 - 表单值
*/
const handleLogin = async (values: {
username: string;
password: string;
remember?: boolean;
}) => {
setLoading(true);
try {
const success = await login(values.username, values.password);
if (success) {
navigate('/admin/dashboard', { replace: true });
}
} catch (error) {
// 错误已在AuthContext中处理
} finally {
setLoading(false);
}
};
return (
<div style={{
minHeight: '100vh',
background: 'linear-gradient(135deg, #1e3c72 0%, #2a5298 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '20px'
}}>
<Card
style={{
width: '100%',
maxWidth: '450px',
boxShadow: '0 10px 40px rgba(0, 0, 0, 0.15)',
borderRadius: '16px',
border: '1px solid rgba(255, 255, 255, 0.1)'
}}
styles={{ body: { padding: '48px 40px' } }}
>
<Space orientation="vertical" size="large" style={{ width: '100%' }}>
{/* 系统标题 */}
<div style={{ textAlign: 'center' }}>
<div style={{
width: '72px',
height: '72px',
background: 'linear-gradient(45deg, #1890ff, #722ed1)',
borderRadius: '16px',
margin: '0 auto 20px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '32px',
color: 'white'
}}>
<SafetyOutlined />
</div>
<Title level={2} style={{ margin: 0, color: '#1f2937' }}>
</Title>
<Text type="secondary" style={{ fontSize: '16px' }}>
西
</Text>
</div>
{/* 安全提示 */}
<div style={{
background: '#f6ffed',
border: '1px solid #b7eb8f',
borderRadius: '8px',
padding: '12px 16px',
textAlign: 'center'
}}>
<Text style={{ color: '#52c41a', fontSize: '14px' }}>
🔒 使
</Text>
</div>
{/* 登录表单 */}
<Form
name="admin-login"
onFinish={handleLogin}
autoComplete="off"
size="large"
initialValues={{ remember: false }}
>
<Form.Item
name="username"
rules={[
{ required: true, message: '请输入管理员账号!' },
{ min: 3, message: '账号至少3个字符' }
]}
>
<Input
prefix={<UserOutlined style={{ color: '#8c8c8c' }} />}
placeholder="管理员账号"
style={{
borderRadius: '10px',
border: '1px solid #d9d9d9'
}}
/>
</Form.Item>
<Form.Item
name="password"
rules={[
{ required: true, message: '请输入密码!' },
{ min: 6, message: '密码至少6个字符' }
]}
>
<Input.Password
prefix={<LockOutlined style={{ color: '#8c8c8c' }} />}
placeholder="密码"
style={{
borderRadius: '10px',
border: '1px solid #d9d9d9'
}}
/>
</Form.Item>
<Form.Item>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Form.Item name="remember" valuePropName="checked" style={{ margin: 0 }}>
<Checkbox></Checkbox>
</Form.Item>
<a href="#" style={{ color: '#1890ff' }}>
</a>
</div>
</Form.Item>
<Form.Item>
<Button
type="primary"
htmlType="submit"
loading={loading}
style={{
width: '100%',
height: '48px',
borderRadius: '10px',
fontSize: '16px',
fontWeight: '500',
background: 'linear-gradient(45deg, #1890ff, #722ed1)',
border: 'none',
boxShadow: '0 4px 12px rgba(24, 144, 255, 0.3)'
}}
>
{loading ? '登录中...' : '登录管理后台'}
</Button>
</Form.Item>
</Form>
{/* 底部信息 */}
<div style={{ textAlign: 'center' }}>
<Text type="secondary" style={{ fontSize: '12px' }}>
v1.0.0 |
<a href="#" style={{ color: '#1890ff', marginLeft: '4px' }}>
</a>
</Text>
</div>
</Space>
</Card>
</div>
);
};
export default AdminLogin;

View File

@@ -1,338 +0,0 @@
/**
* 配置历史记录模态框组件
* @author MHXY Development Team
* @version 1.0.0
*/
import React, { useState, useEffect } from 'react';
import { Modal, Table, Tag, Space, Button, Typography, Tooltip, Empty } from 'antd';
import {
HistoryOutlined,
UserOutlined,
ClockCircleOutlined,
EyeOutlined,
InfoCircleOutlined
} from '@ant-design/icons';
// import { ConfigHistory } from '../types/systemConfig';
import systemConfigService from '../services/systemConfigService';
const { Text, Paragraph } = Typography;
const { confirm } = Modal;
interface ConfigHistoryModalProps {
visible: boolean;
configKey?: string;
onClose: () => void;
}
const ConfigHistoryModal: React.FC<ConfigHistoryModalProps> = ({
visible,
configKey,
onClose
}) => {
const [historyData, setHistoryData] = useState<any[]>([]);
const [loading, setLoading] = useState<boolean>(false);
// 加载历史记录
const loadHistory = async () => {
try {
setLoading(true);
const history = await systemConfigService.getConfigHistory(configKey);
setHistoryData(history);
} catch (error) {
console.error('加载配置历史失败:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (visible) {
loadHistory();
}
}, [visible, configKey]);
// 查看配置变更详情
const handleViewDetails = (record: ConfigHistory) => {
confirm({
title: '配置变更详情',
width: 600,
content: (
<div style={{ marginTop: '16px' }}>
<div style={{ marginBottom: '16px' }}>
<Text strong></Text>
<Text code>{record.config_key}</Text>
</div>
<div style={{ marginBottom: '16px' }}>
<Text strong></Text>
<Text>{record.admin_user.real_name} ({record.admin_user.username})</Text>
</div>
<div style={{ marginBottom: '16px' }}>
<Text strong></Text>
<Text>{new Date(record.created_at).toLocaleString('zh-CN')}</Text>
</div>
<div style={{ marginBottom: '16px' }}>
<Text strong></Text>
<Paragraph>{record.changed_reason}</Paragraph>
</div>
<div style={{ marginBottom: '16px' }}>
<Text strong></Text>
<div style={{
marginTop: '8px',
padding: '8px',
background: '#fff2f0',
border: '1px solid #ffccc7',
borderRadius: '4px',
fontFamily: 'monospace',
fontSize: '12px'
}}>
<Paragraph copyable style={{ margin: 0, color: '#cf1322' }}>
{record.old_value}
</Paragraph>
</div>
</div>
<div style={{ marginBottom: '16px' }}>
<Text strong></Text>
<div style={{
marginTop: '8px',
padding: '8px',
background: '#f6ffed',
border: '1px solid #b7eb8f',
borderRadius: '4px',
fontFamily: 'monospace',
fontSize: '12px'
}}>
<Paragraph copyable style={{ margin: 0, color: '#389e0d' }}>
{record.new_value}
</Paragraph>
</div>
</div>
</div>
),
okText: '关闭',
cancelButtonProps: { style: { display: 'none' } }
});
};
// 表格列定义
const columns = [
{
title: '配置项',
dataIndex: 'config_key',
key: 'config_key',
width: 200,
render: (text: string) => (
<Text code style={{ fontSize: '12px' }}>{text}</Text>
)
},
{
title: '变更者',
dataIndex: ['admin_user', 'real_name'],
key: 'changed_by',
width: 120,
render: (text: string, record: ConfigHistory) => (
<div>
<div style={{ fontWeight: 500 }}>{text}</div>
<div style={{ fontSize: '11px', color: '#666' }}>
@{record.admin_user.username}
</div>
</div>
)
},
{
title: '变更内容',
key: 'change_content',
width: 300,
render: (record: ConfigHistory) => (
<div>
<div style={{ marginBottom: '4px' }}>
<Tag color="red" size="small"></Tag>
<Text code style={{ fontSize: '11px' }}>
{record.old_value.length > 20
? `${record.old_value.substring(0, 20)}...`
: record.old_value
}
</Text>
</div>
<div>
<Tag color="green" size="small"></Tag>
<Text code style={{ fontSize: '11px' }}>
{record.new_value.length > 20
? `${record.new_value.substring(0, 20)}...`
: record.new_value
}
</Text>
</div>
</div>
)
},
{
title: '变更原因',
dataIndex: 'changed_reason',
key: 'changed_reason',
width: 200,
render: (text: string) => (
<Paragraph
ellipsis={{ rows: 2, expandable: false }}
style={{ margin: 0, fontSize: '12px' }}
>
{text}
</Paragraph>
)
},
{
title: '变更时间',
dataIndex: 'created_at',
key: 'created_at',
width: 160,
render: (text: string) => (
<div>
<div style={{ fontSize: '12px' }}>
{new Date(text).toLocaleDateString('zh-CN')}
</div>
<div style={{ fontSize: '11px', color: '#666' }}>
{new Date(text).toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})}
</div>
</div>
)
},
{
title: '操作',
key: 'actions',
width: 100,
render: (record: ConfigHistory) => (
<Space>
<Tooltip title="查看详情">
<Button
type="text"
size="small"
icon={<EyeOutlined />}
onClick={() => handleViewDetails(record)}
/>
</Tooltip>
</Space>
)
}
];
// 判断是否为敏感配置
const isSensitiveConfig = (configKey: string) => {
return configKey.includes('secret') || configKey.includes('key') || configKey.includes('psk');
};
return (
<Modal
title={
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<HistoryOutlined />
{configKey && (
<>
<span style={{ color: '#666' }}>-</span>
<Text code>{configKey}</Text>
</>
)}
</div>
}
open={visible}
onCancel={onClose}
width={900}
footer={[
<Button key="close" onClick={onClose}>
</Button>
]}
>
<div style={{ marginBottom: '16px' }}>
{configKey ? (
<div style={{
padding: '12px',
background: '#f0f2f5',
borderRadius: '6px',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
<InfoCircleOutlined style={{ color: '#1890ff' }} />
<span style={{ fontSize: '12px' }}>
<Text code>{configKey}</Text>
</span>
{isSensitiveConfig(configKey) && (
<Tag color="red" size="small"></Tag>
)}
</div>
) : (
<div style={{
padding: '12px',
background: '#f0f2f5',
borderRadius: '6px',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
<InfoCircleOutlined style={{ color: '#1890ff' }} />
<span style={{ fontSize: '12px' }}>
</span>
</div>
)}
</div>
<Table
columns={columns}
dataSource={historyData}
rowKey="id"
loading={loading}
pagination={{
pageSize: 10,
showSizeChanger: false,
showQuickJumper: false,
showTotal: (total) => `${total} 条记录`
}}
locale={{
emptyText: (
<Empty
description="暂无历史记录"
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
)
}}
size="small"
scroll={{ y: 400 }}
/>
{/* 说明信息 */}
<div style={{
marginTop: '16px',
padding: '12px',
background: '#fafafa',
border: '1px solid #d9d9d9',
borderRadius: '6px',
fontSize: '12px',
color: '#666'
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
<InfoCircleOutlined style={{ color: '#1890ff' }} />
<strong></strong>
</div>
<ul style={{ margin: 0, paddingLeft: '20px' }}>
<li></li>
<li></li>
<li></li>
<li></li>
</ul>
</div>
</Modal>
);
};
export default ConfigHistoryModal;

View File

@@ -1,21 +0,0 @@
import React, { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
/**
* 系统首页 - 自动重定向到玩家登录页面
* 路径:/
*/
const Home: React.FC = () => {
const navigate = useNavigate();
useEffect(() => {
// 自动重定向到玩家登录页面
navigate('/player', { replace: true });
}, [navigate]);
// 由于使用了 replace: true这个组件实际上不会渲染任何内容
// 用户会被自动重定向到 /player 页面
return null;
};
export default Home;

View File

@@ -1,153 +0,0 @@
import React, { useState } from 'react';
import { Form, Input, Button, Card, Typography, message, Space } from 'antd';
import { UserOutlined, LockOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
const { Title, Text } = Typography;
/**
* 玩家服务中心登录页面
* 路径:/player
*/
const PlayerLogin: React.FC = () => {
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
/**
* 处理登录表单提交
* 简化版登录无JWT鉴权仅用于演示
* @param values - 表单值
*/
const handleLogin = async (values: { username: string; password: string }) => {
setLoading(true);
try {
// 模拟本地验证无JWT鉴权
await new Promise(resolve => setTimeout(resolve, 500));
// 仅做基础格式验证,无服务端鉴权
if (!values.username || !values.password) {
throw new Error('用户名和密码不能为空');
}
// 模拟登录成功(无真实鉴权)
message.success('登录成功!欢迎回到梦幻西游!');
// 简单的本地状态模拟不涉及JWT
console.log('玩家登录信息:', {
username: values.username,
timestamp: new Date().toISOString(),
authType: 'simple-demo'
});
} catch (error) {
message.error(error instanceof Error ? error.message : '登录失败,请检查用户名和密码!');
} finally {
setLoading(false);
}
};
return (
<div style={{
minHeight: '100vh',
background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '20px'
}}>
<Card
style={{
width: '100%',
maxWidth: '400px',
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
borderRadius: '12px'
}}
styles={{ body: { padding: '40px 32px' } }}
>
<Space orientation="vertical" size="large" style={{ width: '100%', textAlign: 'center' }}>
{/* 游戏Logo和标题 */}
<div>
<div style={{
width: '80px',
height: '80px',
background: 'linear-gradient(45deg, #ff6b6b, #ffd93d)',
borderRadius: '50%',
margin: '0 auto 16px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '32px',
color: 'white'
}}>
🏮
</div>
<Title level={2} style={{ margin: 0, color: '#2c3e50' }}>
西
</Title>
<Text type="secondary"></Text>
</div>
{/* 登录表单 */}
<Form
name="player-login"
onFinish={handleLogin}
autoComplete="off"
size="large"
>
<Form.Item
name="username"
rules={[{ required: true, message: '请输入您的游戏账号!' }]}
>
<Input
prefix={<UserOutlined />}
placeholder="游戏账号"
style={{ borderRadius: '8px' }}
/>
</Form.Item>
<Form.Item
name="password"
rules={[{ required: true, message: '请输入您的密码!' }]}
>
<Input.Password
prefix={<LockOutlined />}
placeholder="密码"
style={{ borderRadius: '8px' }}
/>
</Form.Item>
<Form.Item>
<Button
type="primary"
htmlType="submit"
loading={loading}
style={{
width: '100%',
height: '44px',
borderRadius: '8px',
fontSize: '16px',
background: 'linear-gradient(45deg, #667eea, #764ba2)',
border: 'none'
}}
>
</Button>
</Form.Item>
</Form>
{/* 底部链接 */}
<Space orientation="vertical" size="small">
<Text type="secondary" style={{ fontSize: '12px' }}>
<a href="#" style={{ color: '#667eea' }}></a>
</Text>
<Text type="secondary" style={{ fontSize: '12px' }}>
<a href="#" style={{ color: '#667eea' }}></a>
</Text>
</Space>
</Space>
</Card>
</div>
);
};
export default PlayerLogin;

View File

@@ -1,253 +0,0 @@
/**
* 系统配置页面主组件
* @author MHXY Development Team
* @version 1.0.0
*/
import React, { useState, useEffect } from 'react';
import { Tabs, message, Space } from 'antd';
import {
SettingOutlined,
SecurityScanOutlined,
CloudServerOutlined,
ReloadOutlined
} from '@ant-design/icons';
import { SystemConfig, SaveConfigRequest } from '../types/systemConfig';
import systemConfigService from '../services/systemConfigService';
import BasicConfigTab from './tabs/BasicConfigTab';
import SecurityConfigTab from './tabs/SecurityConfigTab';
import GameConfigTab from './tabs/GameConfigTab';
import ConfigHistoryModal from './ConfigHistoryModal';
const SystemConfigPage: React.FC = () => {
const [configs, setConfigs] = useState<SystemConfig[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [saving, setSaving] = useState<boolean>(false);
const [activeTab, setActiveTab] = useState<string>('basic');
const [historyModalVisible, setHistoryModalVisible] = useState<boolean>(false);
const [selectedConfigKey, setSelectedConfigKey] = useState<string>('');
const [hasUnsavedChanges, setHasUnsavedChanges] = useState<boolean>(false);
// 加载所有配置
const loadConfigs = async () => {
try {
setLoading(true);
const configsData = await systemConfigService.getAllConfigs();
setConfigs(configsData);
setHasUnsavedChanges(false);
} catch (error) {
console.error('加载配置失败:', error);
message.error('加载配置失败,请重试');
} finally {
setLoading(false);
}
};
useEffect(() => {
loadConfigs();
}, []);
// 保存配置
const handleSaveConfigs = async (configRequests: SaveConfigRequest[]) => {
try {
setSaving(true);
const result = await systemConfigService.saveConfigs(configRequests);
if (result.success) {
message.success(result.message);
await loadConfigs();
setHasUnsavedChanges(false);
} else {
message.error(result.message);
if (result.errors) {
console.error('配置保存错误:', result.errors);
}
}
} catch (error) {
console.error('保存配置失败:', error);
message.error('保存配置失败,请重试');
} finally {
setSaving(false);
}
};
// 重置配置为默认值
const handleResetConfig = async (configKey: string) => {
try {
const result = await systemConfigService.resetConfig(configKey);
if (result.success) {
message.success(result.message);
// 更新本地配置
setConfigs(prev => prev.map(config =>
config.config_key === configKey
? { ...config, config_value: result.defaultValue || '' }
: config
));
setHasUnsavedChanges(true);
}
} catch (error) {
console.error('重置配置失败:', error);
message.error('重置配置失败,请重试');
}
};
// 查看配置历史
const handleViewHistory = (configKey: string) => {
setSelectedConfigKey(configKey);
setHistoryModalVisible(true);
};
// 检测未保存的更改
const handleConfigChange = () => {
setHasUnsavedChanges(true);
};
// 获取指定分组的配置
const getConfigsByGroup = (_group: string) => {
return configs.filter(config => config.config_group === _group);
};
const tabItems = [
{
key: 'basic',
label: (
<Space size="small">
<SettingOutlined />
</Space>
),
children: (
<BasicConfigTab
configs={getConfigsByGroup('basic')}
loading={loading}
saving={saving}
onSave={handleSaveConfigs}
onReset={handleResetConfig}
onShowHistory={handleViewHistory}
onConfigChange={handleConfigChange}
/>
)
},
{
key: 'security',
label: (
<Space size="small">
<SecurityScanOutlined />
</Space>
),
children: (
<SecurityConfigTab
configs={getConfigsByGroup('security')}
loading={loading}
saving={saving}
onSave={handleSaveConfigs}
onReset={handleResetConfig}
onShowHistory={handleViewHistory}
onConfigChange={handleConfigChange}
/>
)
},
{
key: 'game',
label: (
<Space size="small">
<CloudServerOutlined />
</Space>
),
children: (
<GameConfigTab
configs={getConfigsByGroup('game')}
loading={loading}
saving={saving}
onSave={handleSaveConfigs}
onReset={handleResetConfig}
onShowHistory={handleViewHistory}
onConfigChange={handleConfigChange}
/>
)
}
];
return (
<div className="system-config-page" style={{ padding: '24px' }}>
{/* 页面头部 */}
<div className="config-page-header" style={{ marginBottom: '24px' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<div>
<h1 style={{ margin: 0, fontSize: '24px', fontWeight: 600 }}>
</h1>
<p style={{ margin: '8px 0 0 0', color: '#666' }}>
</p>
</div>
<div style={{ display: 'flex', gap: '12px' }}>
<button
onClick={loadConfigs}
disabled={loading}
style={{
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '8px 16px',
border: '1px solid #d9d9d9',
borderRadius: '6px',
background: loading ? '#f5f5f5' : '#fff',
color: loading ? '#999' : '#666',
cursor: loading ? 'not-allowed' : 'pointer'
}}
>
<ReloadOutlined />
</button>
</div>
</div>
{hasUnsavedChanges && (
<div style={{
marginTop: '12px',
padding: '12px',
background: '#fff2e8',
border: '1px solid #ffbb96',
borderRadius: '6px',
display: 'flex',
alignItems: 'center',
gap: '8px'
}}>
<span style={{ color: '#d46b08' }}></span>
<span style={{ color: '#d46b08' }}>
</span>
</div>
)}
</div>
{/* 配置选项卡 */}
<div className="config-tabs-container">
<Tabs
activeKey={activeTab}
onChange={setActiveTab}
items={tabItems}
size="large"
type="card"
/>
</div>
{/* 配置历史弹窗 */}
<ConfigHistoryModal
visible={historyModalVisible}
configKey={selectedConfigKey}
onClose={() => {
setHistoryModalVisible(false);
setSelectedConfigKey('');
}}
/>
</div>
);
};
export default SystemConfigPage;

View File

@@ -1,317 +0,0 @@
/**
* 基本配置标签页组件
* @author MHXY Development Team
* @version 1.0.0
*/
import React, { useState, useEffect } from 'react';
import { Form, Input, Switch, Select, Card, Button, Space, Row, Col, Tooltip } from 'antd';
import {
InfoCircleOutlined,
SaveOutlined,
ReloadOutlined,
HistoryOutlined,
SettingOutlined
} from '@ant-design/icons';
import { SystemConfig, SaveConfigRequest } from '../../types/systemConfig';
const { Option } = Select;
const { TextArea } = Input;
interface BasicConfigTabProps {
configs: SystemConfig[];
loading: boolean;
saving: boolean;
onSave: (requests: SaveConfigRequest[]) => void;
onReset: (configKey: string) => void;
onShowHistory: (configKey: string) => void;
onConfigChange: () => void;
}
const BasicConfigTab: React.FC<BasicConfigTabProps> = ({
configs,
// loading,
saving,
onSave,
onReset,
onShowHistory,
onConfigChange
}) => {
const [form] = Form.useForm();
const [formData, setFormData] = useState<Record<string, unknown>>(() => {
// 初始化时设置默认数据
const initialData: Record<string, unknown> = {};
configs.forEach(config => {
initialData[config.config_key] = config.config_value;
});
return initialData;
});
// 初始化表单数据
useEffect(() => {
const initialData: Record<string, unknown> = {};
configs.forEach(config => {
initialData[config.config_key] = config.config_value;
});
form.setFieldsValue(initialData);
}, [configs, form]);
// 处理表单值变化
const handleValuesChange = (_changedValues: Record<string, unknown>, allValues: Record<string, unknown>) => {
setFormData(allValues);
onConfigChange();
};
// 保存配置
const handleSave = () => {
form.validateFields().then(() => {
const saveRequests: SaveConfigRequest[] = configs.map(config => ({
config_key: config.config_key,
config_value: String(formData[config.config_key] || ''),
config_label: config.config_label,
config_group: config.config_group,
config_type: config.config_type
}));
onSave(saveRequests);
}).catch(() => {
// 表单验证失败
});
};
// 重置为默认值
const handleReset = (configKey: string) => {
onReset(configKey);
};
// 显示配置历史
const handleShowHistory = (configKey: string) => {
onShowHistory(configKey);
};
// 语言选项
const languageOptions = [
{ value: 'zh-CN', label: '简体中文' },
{ value: 'zh-TW', label: '繁体中文' },
{ value: 'en-US', label: 'English' },
{ value: 'ja-JP', label: '日本語' }
];
// 版本选项
const versionOptions = [
{ value: '1.0.0', label: 'v1.0.0' },
{ value: '1.1.0', label: 'v1.1.0' },
{ value: '2.0.0', label: 'v2.0.0' }
];
const configItems = [
{
key: 'site_name',
title: '网站名称',
description: '系统显示名称,将在页面标题和系统信息中显示',
type: 'input',
placeholder: '请输入网站名称',
required: true
},
{
key: 'site_version',
title: '系统版本',
description: '当前系统版本号,用于显示和版本管理',
type: 'select',
options: versionOptions,
required: true
},
{
key: 'site_description',
title: '系统描述',
description: '系统描述信息,简要介绍系统功能和用途',
type: 'textarea',
placeholder: '请输入系统描述',
rows: 3
},
{
key: 'admin_email',
title: '管理员邮箱',
description: '系统管理员联系邮箱,用于接收系统通知',
type: 'input',
placeholder: '请输入管理员邮箱',
required: true
},
{
key: 'maintenance_mode',
title: '维护模式',
description: '开启后用户无法正常访问系统,仅管理员可以登录',
type: 'switch'
},
{
key: 'default_language',
title: '默认语言',
description: '系统默认语言设置,影响界面显示语言',
type: 'select',
options: languageOptions,
required: true
}
];
return (
<div className="basic-config-tab" style={{ padding: '0' }}>
{/* 配置表单 */}
<Form
form={form}
layout="vertical"
onValuesChange={handleValuesChange}
style={{ marginBottom: '24px' }}
>
<Row gutter={[24, 0]}>
{configItems.map((item) => {
const config = configs.find(c => c.config_key === item.key);
if (!config) return null;
return (
<Col span={12} key={item.key}>
<Card
size="small"
style={{ marginBottom: '16px' }}
title={
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<SettingOutlined />
{item.title}
{config.config_type === 'boolean' && (
<Switch
size="small"
checked={formData[item.key] === '1' || formData[item.key] === true}
onChange={(checked) => {
const newValue = checked ? '1' : '0';
form.setFieldValue(item.key, newValue);
onConfigChange();
}}
style={{ marginLeft: '8px' }}
/>
)}
</div>
}
extra={
<Space>
<Tooltip title="查看历史记录">
<Button
type="text"
size="small"
icon={<HistoryOutlined />}
onClick={() => handleShowHistory(item.key)}
/>
</Tooltip>
<Tooltip title="重置为默认值">
<Button
type="text"
size="small"
icon={<ReloadOutlined />}
onClick={() => handleReset(item.key)}
/>
</Tooltip>
</Space>
}
>
<div style={{ marginBottom: '8px' }}>
<div style={{
fontSize: '12px',
color: '#666',
display: 'flex',
alignItems: 'center',
gap: '4px'
}}>
<InfoCircleOutlined />
{item.description}
</div>
</div>
{/* 输入控件 */}
<Form.Item
rules={[
{ required: item.required, message: `请输入${item.title}` },
...(item.key === 'admin_email' ? [
{ type: 'email' as const, message: '请输入有效的邮箱地址' }
] : [])
]}
>
{item.type === 'input' && (
<Input
placeholder={item.placeholder}
disabled={config.config_type === 'boolean'}
value={String(formData[item.key] || '')}
onChange={(e) => {
form.setFieldValue(item.key, e.target.value);
onConfigChange();
}}
/>
)}
{item.type === 'textarea' && (
<TextArea
placeholder={item.placeholder}
rows={item.rows || 2}
value={String(formData[item.key] || '')}
onChange={(e) => {
form.setFieldValue(item.key, e.target.value);
onConfigChange();
}}
/>
)}
{item.type === 'select' && (
<Select
placeholder={item.placeholder}
value={String(formData[item.key] || '')}
onChange={(value) => {
form.setFieldValue(item.key, value);
onConfigChange();
}}
>
{item.options?.map(option => (
<Option key={option.value} value={option.value}>
{option.label}
</Option>
))}
</Select>
)}
</Form.Item>
</Card>
</Col>
);
})}
</Row>
</Form>
{/* 保存按钮 */}
<div style={{
display: 'flex',
justifyContent: 'center',
padding: '24px 0',
borderTop: '1px solid #f0f0f0'
}}>
<Button
type="primary"
size="large"
icon={<SaveOutlined />}
loading={saving}
onClick={handleSave}
style={{ minWidth: '120px' }}
>
</Button>
</div>
{/* 配置信息 */}
<Card size="small" style={{ background: '#fafafa' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
<InfoCircleOutlined style={{ color: '#1890ff' }} />
<strong></strong>
</div>
<ul style={{ margin: 0, paddingLeft: '20px', fontSize: '12px', color: '#666' }}>
<li></li>
<li>访</li>
<li></li>
<li></li>
</ul>
</Card>
</div>
);
};
export default BasicConfigTab;

View File

@@ -1,503 +0,0 @@
/**
* 游戏通信配置标签页组件
* @author MHXY Development Team
* @version 1.0.0
*/
import React, { useState, useEffect } from 'react';
import { Form, Input, InputNumber, Switch, Select, Card, Button, Space, Row, Col, Tooltip, Alert } from 'antd';
import {
InfoCircleOutlined,
SaveOutlined,
ReloadOutlined,
HistoryOutlined,
CloudServerOutlined,
ApiOutlined,
ClockCircleOutlined,
UserAddOutlined,
FileTextOutlined
} from '@ant-design/icons';
import { SystemConfig, SaveConfigRequest } from '../../types/systemConfig';
const { TextArea } = Input;
interface GameConfigTabProps {
configs: SystemConfig[];
loading?: boolean;
saving?: boolean;
onSave: (requests: SaveConfigRequest[]) => void;
onReset: (configKey: string) => void;
onShowHistory: (configKey: string) => void;
onConfigChange: () => void;
}
interface ConfigItem {
key: string;
title: string;
description: string;
type: 'input' | 'textarea' | 'inputnumber' | 'switch' | 'select';
placeholder?: string;
required?: boolean;
min?: number;
max?: number;
suffix?: string;
rows?: number;
sensitive?: boolean;
icon?: React.ReactElement;
options?: Array<{ value: string; label: string }>;
}
// 定义表单数据的类型
interface FormDataType {
[key: string]: string | boolean | number | null | undefined;
}
// 定义变化的值的类型
interface ChangedValuesType {
[key: string]: string | boolean | number | null | undefined;
}
const GameConfigTab: React.FC<GameConfigTabProps> = ({
configs,
loading,
saving,
onSave,
onReset,
onShowHistory,
onConfigChange
}) => {
const [form] = Form.useForm();
const [formData, setFormData] = useState<FormDataType>({});
// 初始化表单数据
useEffect(() => {
const initialData: FormDataType = {};
configs.forEach(config => {
if (config.config_type === 'boolean') {
initialData[config.config_key] = config.config_value === '1' || config.config_value === 'true';
} else {
initialData[config.config_key] = config.config_value;
}
});
// 延迟设置表单值,避免在渲染期间直接修改
const timer = setTimeout(() => {
setFormData(initialData);
form.setFieldsValue(initialData);
}, 0);
return () => clearTimeout(timer);
}, [configs, form]);
// 处理表单值变化
const handleValuesChange = (changedValues: ChangedValuesType, allValues: FormDataType) => {
setFormData(allValues);
// 使用changedValues参数避免未使用的警告
if (Object.keys(changedValues).length > 0) {
onConfigChange();
}
};
// 保存配置
const handleSave = () => {
form.validateFields().then(() => {
const saveRequests: SaveConfigRequest[] = configs.map(config => ({
config_key: config.config_key,
config_value: config.config_type === 'boolean'
? (formData[config.config_key] ? '1' : '0')
: String(formData[config.config_key] || ''),
config_type: config.config_type,
config_label: config.config_label,
config_group: config.config_group
}));
onSave(saveRequests);
}).catch(() => {
// 表单验证失败
});
};
// 重置为默认值
const handleReset = (configKey: string) => {
onReset(configKey);
};
// 显示配置历史
const handleShowHistory = (configKey: string) => {
onShowHistory(configKey);
};
// 检查API地址格式
const validateApiUrl = (url: string) => {
const urlStr = String(url || '');
if (!urlStr) return { valid: false, message: 'API地址不能为空' };
if (!/^https?:\/\/.+/.test(urlStr)) {
return { valid: false, message: '请输入有效的HTTP/HTTPS地址' };
}
return { valid: true, message: 'API地址格式正确' };
};
// 检查PSK密钥强度
const checkPskStrength = (psk: string) => {
const pskStr = String(psk || '');
if (!pskStr) return { level: 'weak', message: 'PSK密钥不能为空' };
if (pskStr.length < 8) return { level: 'weak', message: 'PSK密钥长度不足8位' };
if (pskStr.length < 32) return { level: 'medium', message: 'PSK密钥长度不足32位建议使用更强的密钥' };
if (!/[!@#$%^&*(),.?":{}|<>]/.test(pskStr)) return { level: 'medium', message: 'PSK密钥缺少特殊字符建议添加特殊字符' };
return { level: 'strong', message: 'PSK密钥强度符合要求' };
};
const apiUrlValidation = validateApiUrl(String(formData.game_server_api || ''));
const pskStrength = checkPskStrength(String(formData.game_server_psk || ''));
const configItems: ConfigItem[] = [
{
key: 'game_server_api',
title: '游戏服务端API',
description: '游戏服务端HTTP接口地址用于与游戏服务端进行数据交互',
type: 'input' as const,
placeholder: 'http://127.0.0.1:8080/tool/http',
required: true,
icon: <ApiOutlined />,
sensitive: false
},
{
key: 'game_server_psk',
title: '游戏服务端PSK',
description: '游戏服务端预共享密钥用于API认证安全性至关重要',
type: 'textarea' as const,
placeholder: '请输入PSK密钥建议32位以上',
required: true,
rows: 3,
icon: <ApiOutlined />,
sensitive: true
},
{
key: 'game_server_timeout',
title: '请求超时时间',
description: '与游戏服务端通信的超时时间,单位:秒',
type: 'inputnumber' as const,
min: 5,
max: 120,
suffix: '秒',
required: true,
icon: <ClockCircleOutlined />
},
{
key: 'game_server_retry_count',
title: '重试次数',
description: 'API请求失败时的重试次数',
type: 'inputnumber' as const,
min: 1,
max: 10,
suffix: '次',
required: true,
icon: <CloudServerOutlined />
},
{
key: 'player_auto_register',
title: '玩家自动注册',
description: '新玩家是否自动创建账号',
type: 'switch' as const,
icon: <UserAddOutlined />
},
{
key: 'game_log_level',
title: '游戏日志级别',
description: '游戏相关操作的日志记录级别',
type: 'select' as const,
required: true,
icon: <FileTextOutlined />,
options: [
{ value: 'error', label: '仅错误' },
{ value: 'warn', label: '警告及以上' },
{ value: 'info', label: '信息及以上' },
{ value: 'debug', label: '调试及以上' }
]
}
];
return (
<div className="game-config-tab" style={{ padding: '0' }}>
{/* 游戏通信警告 */}
<Alert
title="游戏通信配置警告"
description="错误的游戏通信配置可能导致无法与游戏服务端正常交互请确保API地址和密钥配置正确。"
type="warning"
showIcon
style={{ marginBottom: '24px' }}
action={
<Button size="small" danger disabled={loading}>
</Button>
}
/>
{/* API地址验证提示 */}
{formData.game_server_api && (
<Alert
title={`API地址验证: ${apiUrlValidation.valid ? '✅ 格式正确' : '❌ 格式错误'}`}
description={apiUrlValidation.message}
type={apiUrlValidation.valid ? 'success' : 'error'}
showIcon
style={{ marginBottom: '24px' }}
/>
)}
{/* PSK密钥强度提示 */}
{formData.game_server_psk && (
<Alert
title={`PSK密钥强度: ${pskStrength.level.toUpperCase()}`}
description={pskStrength.message}
type={pskStrength.level === 'strong' ? 'success' : pskStrength.level === 'medium' ? 'warning' : 'error'}
showIcon
style={{ marginBottom: '24px' }}
/>
)}
{/* 配置表单 */}
<Form
form={form}
layout="vertical"
onValuesChange={handleValuesChange}
style={{ marginBottom: '24px' }}
>
<Row gutter={[24, 0]}>
{configItems.map((item: ConfigItem) => {
const config = configs.find(c => c.config_key === item.key);
if (!config) return null;
return (
<Col span={12} key={item.key}>
<Card
size="small"
style={{ marginBottom: '16px' }}
title={
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
{item.icon || <CloudServerOutlined />}
{item.title}
{item.sensitive && (
<span style={{ color: '#ff4d4f', fontSize: '12px' }}>
</span>
)}
</div>
}
extra={
<Space>
<Tooltip title="查看历史记录">
<Button
type="text"
size="small"
icon={<HistoryOutlined />}
onClick={() => handleShowHistory(item.key)}
/>
</Tooltip>
<Tooltip title="重置为默认值">
<Button
type="text"
size="small"
icon={<ReloadOutlined />}
onClick={() => handleReset(item.key)}
/>
</Tooltip>
</Space>
}
>
<div style={{ marginBottom: '8px' }}>
<div style={{
fontSize: '12px',
color: '#666',
display: 'flex',
alignItems: 'center',
gap: '4px'
}}>
<InfoCircleOutlined />
{item.description}
</div>
</div>
{/* 输入控件 */}
{item.type === 'switch' ? (
// Switch组件不需要表单验证直接渲染
<div style={{ marginTop: '8px' }}>
<Switch
checked={Boolean(formData[item.key])}
onChange={(checked) => {
form.setFieldValue(item.key, checked);
onConfigChange();
}}
/>
<span style={{ marginLeft: '8px', fontSize: '12px', color: '#666' }}>
{String(formData[item.key] || '') ? '已启用' : '未启用'}
</span>
</div>
) : (
// 其他输入控件使用Form.Item进行布局但使用value/onChange进行状态管理
<Form.Item
rules={[
{
required: item.required,
message: `请输入${item.title}`
},
...(item.key === 'game_server_api' ? [
{
validator: (_: unknown, value: string) => {
if (!value || !/^https?:\/\/.+/.test(value)) {
return Promise.reject(new Error('请输入有效的API地址'));
}
return Promise.resolve();
}
}
] : []),
...(item.key === 'game_server_psk' ? [
{
min: 32,
message: 'PSK密钥至少需要32位字符'
}
] : [])
]}
>
{item.type === 'input' && (
<Input
placeholder={item.placeholder}
disabled={config.config_type === 'boolean'}
value={String(formData[item.key] || '')}
onChange={(e) => {
form.setFieldValue(item.key, e.target.value);
onConfigChange();
}}
/>
)}
{item.type === 'textarea' && (
<TextArea
placeholder={item.placeholder}
rows={item.rows || 3}
maxLength={500}
showCount={!item.sensitive}
autoSize={{ minRows: item.rows || 3, maxRows: 6 }}
style={{ fontFamily: item.sensitive ? 'monospace' : 'inherit' }}
value={String(formData[item.key] || '')}
onChange={(e) => {
form.setFieldValue(item.key, e.target.value);
onConfigChange();
}}
/>
)}
{item.type === 'inputnumber' && (
<InputNumber
style={{ width: '100%' }}
min={item.min}
max={item.max}
placeholder={item.placeholder}
suffix={item.suffix}
value={formData[item.key] as number | string | null}
onChange={(value) => {
form.setFieldValue(item.key, value);
onConfigChange();
}}
/>
)}
{item.type === 'select' && (
<Select
placeholder={item.placeholder}
value={String(formData[item.key] || '')}
onChange={(value) => {
form.setFieldValue(item.key, value);
onConfigChange();
}}
>
{item.options?.map((option, optionIndex) => (
<Select.Option key={`${item.key}-${option.value}-${optionIndex}`} value={option.value}>
{option.label}
</Select.Option>
))}
</Select>
)}
</Form.Item>
)}
{/* 特殊字段验证信息 */}
{item.key === 'game_server_api' && formData.game_server_api && (
<div style={{
marginTop: '8px',
padding: '8px',
background: apiUrlValidation.valid ? '#f6ffed' : '#fff2f0',
borderRadius: '4px',
fontSize: '12px',
border: `1px solid ${apiUrlValidation.valid ? '#b7eb8f' : '#ffccc7'}`
}}>
<div>
<strong></strong>
{apiUrlValidation.valid ? '✅ 格式正确' : '❌ 格式错误'}
</div>
<div>
{String(formData.game_server_api || '').startsWith('https') ? 'HTTPS' : 'HTTP'}
</div>
</div>
)}
{item.key === 'game_server_psk' && formData.game_server_psk && (
<div style={{
marginTop: '8px',
padding: '8px',
background: '#f6f8fa',
borderRadius: '4px',
fontSize: '12px'
}}>
<strong></strong>
<div style={{ marginTop: '4px' }}>
: {String(formData.game_server_psk || '').length}
{String(formData.game_server_psk || '').length >= 32 && ' ✅'}
</div>
<div>
: {/[!@#$%^&*(),.?":{}|<>]/.test(String(formData.game_server_psk || '')) ? '✅' : '❌'}
</div>
</div>
)}
</Card>
</Col>
);
})}
</Row>
</Form>
{/* 保存按钮 */}
<div style={{
display: 'flex',
justifyContent: 'center',
padding: '24px 0',
borderTop: '1px solid #f0f0f0'
}}>
<Button
type="primary"
size="large"
icon={<SaveOutlined />}
loading={saving}
onClick={handleSave}
style={{ minWidth: '120px' }}
danger={!apiUrlValidation.valid || pskStrength.level === 'weak'}
>
</Button>
</div>
{/* 配置说明 */}
<Card size="small" style={{ background: '#fafafa' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
<InfoCircleOutlined style={{ color: '#1890ff' }} />
<strong></strong>
</div>
<ul style={{ margin: 0, paddingLeft: '20px', fontSize: '12px', color: '#666' }}>
<li>API地址必须正确配置</li>
<li>PSK密钥是API认证的重要凭据</li>
<li>30</li>
<li>3</li>
<li>'信息'便</li>
<li></li>
</ul>
</Card>
</div>
);
};
export default GameConfigTab;

View File

@@ -1,428 +0,0 @@
/**
* 安全配置标签页组件
* @author MHXY Development Team
* @version 1.0.0
*/
import React, { useState, useEffect } from 'react';
import { Form, Input, InputNumber, Switch, Card, Button, Space, Row, Col, Tooltip, Alert } from 'antd';
import {
InfoCircleOutlined,
SaveOutlined,
ReloadOutlined,
HistoryOutlined,
SecurityScanOutlined,
KeyOutlined,
ClockCircleOutlined,
SafetyCertificateOutlined
} from '@ant-design/icons';
import { SystemConfig, SaveConfigRequest } from '../../types/systemConfig';
const { TextArea } = Input;
interface SecurityConfigTabProps {
configs: SystemConfig[];
loading: boolean;
saving: boolean;
onSave: (requests: SaveConfigRequest[]) => void;
onReset: (configKey: string) => void;
onShowHistory: (configKey: string) => void;
onConfigChange: () => void;
}
// 定义表单数据类型
type FormFieldValue = string | number | boolean;
type FormData = Record<string, FormFieldValue>;
// 配置项接口定义
interface ConfigItem {
key: string;
title: string;
description: string;
type: 'input' | 'textarea' | 'inputnumber' | 'switch' | 'select';
required: boolean;
sensitive?: boolean;
placeholder?: string;
min?: number;
max?: number;
rows?: number;
suffix?: string;
icon?: React.ReactNode;
options?: Array<{ value: string; label: string }>;
}
const SecurityConfigTab: React.FC<SecurityConfigTabProps> = ({
configs,
// loading,
saving,
onSave,
onReset,
onShowHistory,
onConfigChange
}) => {
const [form] = Form.useForm();
const [formData, setFormData] = useState<FormData>({});
// 初始化表单数据
useEffect(() => {
const initialData: FormData = {};
configs.forEach(config => {
if (config.config_type === 'boolean') {
initialData[config.config_key] = config.config_value === '1' || config.config_value === 'true';
} else {
initialData[config.config_key] = config.config_value;
}
});
// 直接设置表单字段值,避免级联渲染
form.setFieldsValue(initialData);
}, [configs, form]);
// 处理表单值变化
const handleValuesChange = (changedValues: Partial<FormData>, allValues: FormData) => {
setFormData(allValues);
onConfigChange();
};
// 保存配置
const handleSave = () => {
form.validateFields().then(() => {
const saveRequests: SaveConfigRequest[] = configs.map(config => ({
config_key: config.config_key,
config_value: config.config_type === 'boolean'
? (formData[config.config_key] ? '1' : '0')
: String(formData[config.config_key] || ''),
config_type: config.config_type,
changed_reason: '安全配置更新'
}));
onSave(saveRequests);
}).catch(() => {
// 表单验证失败
});
};
// 重置为默认值
const handleReset = (configKey: string) => {
onReset(configKey);
};
// 显示配置历史
const handleShowHistory = (configKey: string) => {
onShowHistory(configKey);
};
// 检查JWT密钥强度
const checkJwtSecretStrength = (secret: string) => {
if (!secret) return { level: 'weak', message: 'JWT密钥不能为空' };
if (secret.length < 32) return { level: 'weak', message: 'JWT密钥至少需要32位字符' };
if (secret.length < 64) return { level: 'medium', message: 'JWT密钥长度适中建议使用更长的密钥' };
return { level: 'strong', message: 'JWT密钥强度良好' };
};
// 安全地获取JWT密钥字符串
const getJwtSecretString = (): string => {
const secret = formData.jwt_secret;
return typeof secret === 'string' ? secret : '';
};
const jwtSecretStrength = checkJwtSecretStrength(getJwtSecretString());
const configItems: ConfigItem[] = [
{
key: 'jwt_secret',
title: 'JWT密钥',
description: '用于JWT令牌签名的密钥安全性至关重要建议32位以上随机字符',
type: 'textarea',
placeholder: '请输入JWT密钥建议32位以上',
required: true,
rows: 3,
icon: <KeyOutlined />,
sensitive: true
},
{
key: 'jwt_expires_in',
title: 'JWT过期时间',
description: 'JWT访问令牌的有效期单位小时建议24-72小时',
type: 'inputnumber',
min: 1,
max: 168,
suffix: '小时',
required: true,
icon: <ClockCircleOutlined />
},
{
key: 'jwt_refresh_expires_in',
title: 'JWT刷新令牌过期时间',
description: 'JWT刷新令牌的有效期单位小时建议7天168小时',
type: 'inputnumber',
min: 24,
max: 720,
suffix: '小时',
required: true,
icon: <ClockCircleOutlined />
},
{
key: 'login_attempt_limit',
title: '登录尝试次数限制',
description: '连续登录失败次数限制,超过后临时锁定账户',
type: 'inputnumber',
min: 3,
max: 10,
suffix: '次',
required: true,
icon: <SecurityScanOutlined />
},
{
key: 'session_timeout',
title: '会话超时时间',
description: '用户会话超时时间,单位:分钟',
type: 'inputnumber',
min: 15,
max: 1440,
suffix: '分钟',
required: true,
icon: <ClockCircleOutlined />
},
{
key: 'password_min_length',
title: '密码最小长度',
description: '用户密码最小长度要求',
type: 'inputnumber',
min: 6,
max: 20,
suffix: '位',
required: true,
icon: <KeyOutlined />
},
{
key: 'enable_2fa',
title: '启用双因子认证',
description: '是否启用双因子认证功能,提供额外的安全保障',
type: 'switch',
icon: <SafetyCertificateOutlined />,
required: false
}
];
return (
<div className="security-config-tab" style={{ padding: '0' }}>
{/* 安全警告 */}
<Alert
title="安全配置警告"
description="安全配置直接影响系统安全性,请谨慎修改。错误的配置可能导致系统无法正常运行。"
type="warning"
showIcon
style={{ marginBottom: '24px' }}
action={
<Button size="small" danger>
</Button>
}
/>
{/* JWT密钥强度提示 */}
{formData.jwt_secret && (
<Alert
title={`JWT密钥强度: ${jwtSecretStrength.level.toUpperCase()}`}
description={jwtSecretStrength.message}
type={jwtSecretStrength.level === 'strong' ? 'success' : jwtSecretStrength.level === 'medium' ? 'warning' : 'error'}
showIcon
style={{ marginBottom: '24px' }}
/>
)}
{/* 配置表单 */}
<Form
form={form}
layout="vertical"
onValuesChange={handleValuesChange}
style={{ marginBottom: '24px' }}
>
<Row gutter={[24, 0]}>
{configItems.map((item) => {
const config = configs.find(c => c.config_key === item.key);
if (!config) return null;
return (
<Col span={12} key={item.key}>
<Card
size="small"
style={{ marginBottom: '16px' }}
title={
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
{item.icon || <SecurityScanOutlined />}
{item.title}
{item.sensitive && (
<span style={{ color: '#ff4d4f', fontSize: '12px' }}>
</span>
)}
</div>
}
extra={
<Space>
<Tooltip title="查看历史记录">
<Button
type="text"
size="small"
icon={<HistoryOutlined />}
onClick={() => handleShowHistory(item.key)}
/>
</Tooltip>
<Tooltip title="重置为默认值">
<Button
type="text"
size="small"
icon={<ReloadOutlined />}
onClick={() => handleReset(item.key)}
/>
</Tooltip>
</Space>
}
>
<div style={{ marginBottom: '8px' }}>
<div style={{
fontSize: '12px',
color: '#666',
display: 'flex',
alignItems: 'center',
gap: '4px'
}}>
<InfoCircleOutlined />
{item.description}
</div>
</div>
{/* 输入控件 */}
{item.type === 'switch' ? (
// Switch组件不需要表单验证直接渲染
<div style={{ marginTop: '8px' }}>
<Switch
checked={Boolean(formData[item.key])}
onChange={(checked) => {
form.setFieldValue(item.key, checked);
onConfigChange();
}}
/>
<span style={{ marginLeft: '8px', fontSize: '12px', color: '#666' }}>
{String(formData[item.key] || '') ? '已启用' : '未启用'}
</span>
</div>
) : (
// 其他输入控件使用Form.Item进行布局但使用value/onChange进行状态管理
<Form.Item
rules={[
{ required: item.required, message: `请输入${item.title}` },
...(item.key === 'jwt_secret' ? [
{ min: 32 as const, message: 'JWT密钥至少需要32位字符' }
] : [])
]}
>
{item.type === 'input' && (
<Input.Password
placeholder={item.placeholder}
disabled={config.config_type === 'boolean'}
visibilityToggle={!item.sensitive}
value={formData[item.key]}
onChange={(e) => {
form.setFieldValue(item.key, e.target.value);
onConfigChange();
}}
/>
)}
{item.type === 'textarea' && (
<TextArea
placeholder={item.placeholder}
rows={item.rows || 3}
maxLength={500}
showCount={!item.sensitive}
autoSize={{ minRows: item.rows || 3, maxRows: 6 }}
style={{ fontFamily: item.sensitive ? 'monospace' : 'inherit' }}
value={formData[item.key]}
onChange={(e) => {
form.setFieldValue(item.key, e.target.value);
onConfigChange();
}}
/>
)}
{item.type === 'inputnumber' && (
<InputNumber
style={{ width: '100%' }}
min={item.min}
max={item.max}
placeholder={item.placeholder}
suffix={item.suffix}
value={formData[item.key]}
onChange={(value) => {
form.setFieldValue(item.key, value);
onConfigChange();
}}
/>
)}
</Form.Item>
)}
{/* JWT密钥特殊提示 */}
{item.key === 'jwt_secret' && formData.jwt_secret && (
<div style={{
marginTop: '8px',
padding: '8px',
background: '#f6f8fa',
borderRadius: '4px',
fontSize: '12px'
}}>
<strong></strong>
<div style={{ marginTop: '4px' }}>
: {getJwtSecretString().length}
{getJwtSecretString().length >= 32 && ' ✅'}
</div>
<div>
: {/[!@#$%^&*(),.?":{}|<>]/.test(getJwtSecretString()) ? '✅' : '❌'}
</div>
</div>
)}
</Card>
</Col>
);
})}
</Row>
</Form>
{/* 保存按钮 */}
<div style={{
display: 'flex',
justifyContent: 'center',
padding: '24px 0',
borderTop: '1px solid #f0f0f0'
}}>
<Button
type="primary"
size="large"
icon={<SaveOutlined />}
loading={saving}
onClick={handleSave}
style={{ minWidth: '120px' }}
danger={jwtSecretStrength.level === 'weak'}
>
</Button>
</div>
{/* 配置说明 */}
<Card size="small" style={{ background: '#fafafa' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
<InfoCircleOutlined style={{ color: '#1890ff' }} />
<strong></strong>
</div>
<ul style={{ margin: 0, paddingLeft: '20px', fontSize: '12px', color: '#666' }}>
<li>JWT密钥是系统安全的核心使</li>
<li>15-30便</li>
<li>3-5</li>
<li></li>
<li></li>
</ul>
</Card>
</div>
);
};
export default SecurityConfigTab;

View File

@@ -1,230 +0,0 @@
/**
* 系统配置API服务
* @author MHXY Development Team
* @version 1.0.0
*/
import {
SystemConfig,
SaveConfigRequest
} from '../types/systemConfig';
const API_BASE_URL = '/api/system-config';
class SystemConfigService {
/**
* 获取所有系统配置
*/
async getAllConfigs(): Promise<SystemConfig[]> {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 300));
// 返回模拟数据
return [
// 基本配置
{ id: 1, config_key: 'site_name', config_value: '梦幻西游一站式运营管理系统', config_type: 'string', config_group: 'basic', config_label: '网站名称', config_description: '系统显示名称', sort_order: 1 },
{ id: 2, config_key: 'site_version', config_value: '1.0.0', config_type: 'string', config_group: 'basic', config_label: '系统版本', config_description: '当前系统版本号', sort_order: 2 },
{ id: 3, config_key: 'site_description', config_value: '专业的游戏运营管理平台', config_type: 'string', config_group: 'basic', config_label: '系统描述', config_description: '系统描述信息', sort_order: 3 },
{ id: 4, config_key: 'admin_email', config_value: 'admin@mhxy.com', config_type: 'string', config_group: 'basic', config_label: '管理员邮箱', config_description: '系统管理员联系邮箱', sort_order: 4 },
{ id: 5, config_key: 'maintenance_mode', config_value: '0', config_type: 'boolean', config_group: 'basic', config_label: '维护模式', config_description: '开启后用户无法正常访问系统', sort_order: 5 },
{ id: 6, config_key: 'default_language', config_value: 'zh-CN', config_type: 'string', config_group: 'basic', config_label: '默认语言', config_description: '系统默认语言设置', sort_order: 6 },
// 安全配置
{ id: 7, config_key: 'jwt_secret', config_value: 'JWT_SECRET_32_BYTE_RANDOM_STRING_2025', config_type: 'string', config_group: 'security', config_label: 'JWT密钥', config_description: '用于JWT令牌签名的密钥建议32位字符', sort_order: 1 },
{ id: 8, config_key: 'jwt_expires_in', config_value: '24', config_type: 'number', config_group: 'security', config_label: 'JWT过期时间(小时)', config_description: 'JWT访问令牌的有效期单位小时', sort_order: 2 },
{ id: 9, config_key: 'jwt_refresh_expires_in', config_value: '168', config_type: 'number', config_group: 'security', config_label: 'JWT刷新令牌过期时间(小时)', config_description: 'JWT刷新令牌的有效期单位小时', sort_order: 3 },
{ id: 10, config_key: 'login_attempt_limit', config_value: '5', config_type: 'number', config_group: 'security', config_label: '登录尝试次数限制', config_description: '连续登录失败次数限制', sort_order: 4 },
{ id: 11, config_key: 'session_timeout', config_value: '30', config_type: 'number', config_group: 'security', config_label: '会话超时时间(分钟)', config_description: '用户会话超时时间', sort_order: 5 },
{ id: 12, config_key: 'password_min_length', config_value: '6', config_type: 'number', config_group: 'security', config_label: '密码最小长度', config_description: '用户密码最小长度要求', sort_order: 6 },
{ id: 13, config_key: 'enable_2fa', config_value: '0', config_type: 'boolean', config_group: 'security', config_label: '启用双因子认证', config_description: '是否启用双因子认证功能', sort_order: 7 },
// 游戏通信配置
{ id: 14, config_key: 'game_server_api', config_value: 'http://127.0.0.1:8080/tool/http', config_type: 'string', config_group: 'game', config_label: '游戏服务端API', config_description: '游戏服务端HTTP接口地址', sort_order: 1 },
{ id: 15, config_key: 'game_server_psk', config_value: 'THIS_IS_A_32_BYTE_FIXED_PSK!!!!!', config_type: 'string', config_group: 'game', config_label: '游戏服务端PSK', config_description: '游戏服务端预共享密钥用于API认证', sort_order: 2 },
{ id: 16, config_key: 'game_server_timeout', config_value: '30', config_type: 'number', config_group: 'game', config_label: '请求超时时间(秒)', config_description: '与游戏服务端通信的超时时间', sort_order: 3 },
{ id: 17, config_key: 'game_server_retry_count', config_value: '3', config_type: 'number', config_group: 'game', config_label: '重试次数', config_description: 'API请求失败时的重试次数', sort_order: 4 },
{ id: 18, config_key: 'player_auto_register', config_value: '1', config_type: 'boolean', config_group: 'game', config_label: '玩家自动注册', config_description: '新玩家是否自动创建账号', sort_order: 5 },
{ id: 19, config_key: 'game_log_level', config_value: 'info', config_type: 'string', config_group: 'game', config_label: '游戏日志级别', config_description: '游戏相关操作的日志记录级别', sort_order: 6 }
];
}
/**
* 按分组获取配置
*/
async getConfigsByGroup(group: string): Promise<SystemConfig[]> {
const allConfigs = await this.getAllConfigs();
return allConfigs.filter(config => config.config_group === group);
}
/**
* 保存单个配置项
*/
async saveConfig(request: SaveConfigRequest): Promise<{ success: boolean; message: string }> {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 500));
// 模拟验证逻辑
if (!request.config_key || !request.config_value) {
throw new Error('配置键名和值不能为空');
}
// 特殊验证
if (request.config_key === 'jwt_secret' && request.config_value.length < 32) {
throw new Error('JWT密钥至少需要32位字符');
}
if (request.config_key === 'game_server_api' && !/^https?:\/\/.+/.test(request.config_value)) {
throw new Error('游戏服务端API必须是有效的HTTP/HTTPS地址');
}
return {
success: true,
message: '配置保存成功'
};
}
/**
* 批量保存配置
*/
async saveConfigs(requests: SaveConfigRequest[]): Promise<{ success: boolean; message: string; errors?: string[] }> {
const errors: string[] = [];
for (const request of requests) {
try {
await this.saveConfig(request);
} catch (error) {
errors.push(`${request.config_key}: ${error instanceof Error ? error.message : '未知错误'}`);
}
}
return {
success: errors.length === 0,
message: errors.length === 0 ? '所有配置保存成功' : '部分配置保存失败',
errors: errors.length > 0 ? errors : undefined
};
}
/**
* 获取配置历史记录
*/
async getConfigHistory(configKey?: string, limit: number = 50): Promise<any[]> {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 300));
const mockHistory: any[] = [
{
id: 1,
config_key: 'jwt_secret',
old_value: 'OLD_JWT_SECRET_32_BYTE_STRING_2024',
new_value: 'JWT_SECRET_32_BYTE_RANDOM_STRING_2025',
changed_by: 1,
changed_reason: '更新JWT密钥以提升安全性',
created_at: '2025-12-12T10:30:00.000Z',
admin_user: {
username: 'admin',
real_name: '系统管理员'
}
},
{
id: 2,
config_key: 'game_server_api',
old_value: 'http://192.168.1.100:8080/tool/http',
new_value: 'http://127.0.0.1:8080/tool/http',
changed_by: 1,
changed_reason: '更新为本地开发环境地址',
created_at: '2025-12-12T09:15:00.000Z',
admin_user: {
username: 'admin',
real_name: '系统管理员'
}
}
];
return configKey
? mockHistory.filter(item => item.config_key === configKey)
: mockHistory;
}
/**
* 同步配置到.env文件
*/
async syncToEnvFile(configs: EnvConfig): Promise<{ success: boolean; message: string }> {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000));
// 模拟文件写入操作
console.log('同步配置到.env文件:', configs);
return {
success: true,
message: '配置已成功同步到.env文件'
};
}
/**
* 重置指定配置项为默认值
*/
async resetConfig(configKey: string): Promise<{ success: boolean; message: string; defaultValue?: string }> {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 300));
const defaultValues: Record<string, string> = {
'jwt_secret': 'JWT_SECRET_32_BYTE_RANDOM_STRING_2025',
'game_server_api': 'http://127.0.0.1:8080/tool/http',
'game_server_psk': 'THIS_IS_A_32_BYTE_FIXED_PSK!!!!!',
'jwt_expires_in': '24',
'login_attempt_limit': '5',
'session_timeout': '30'
};
const defaultValue = defaultValues[configKey];
if (!defaultValue) {
throw new Error('未找到该配置项的默认值');
}
return {
success: true,
message: '配置已重置为默认值',
defaultValue
};
}
/**
* 验证配置值
*/
validateConfigValue(configKey: string, value: any): { valid: boolean; error?: string } {
switch (configKey) {
case 'jwt_secret':
if (typeof value !== 'string' || value.length < 32) {
return { valid: false, error: 'JWT密钥至少需要32位字符' };
}
break;
case 'game_server_api':
if (!/^https?:\/\/.+/.test(value)) {
return { valid: false, error: '请输入有效的HTTP/HTTPS地址' };
}
break;
case 'game_server_psk':
if (typeof value !== 'string' || value.length < 32) {
return { valid: false, error: 'PSK密钥至少需要32位字符' };
}
break;
case 'jwt_expires_in':
case 'login_attempt_limit':
case 'session_timeout':
const num = Number(value);
if (isNaN(num) || num <= 0) {
return { valid: false, error: '请输入有效的正整数' };
}
break;
}
return { valid: true };
}
}
export const systemConfigService = new SystemConfigService();
export default systemConfigService;

View File

@@ -1,135 +0,0 @@
/**
* 系统配置相关类型定义
* @author MHXY Development Team
* @version 1.0.0
*/
import React from 'react';
export interface SystemConfig {
id?: number;
config_key: string;
config_value: string;
config_type: 'string' | 'number' | 'boolean' | 'json';
config_group: 'basic' | 'security' | 'game';
config_label: string;
config_description?: string;
is_encrypted?: boolean;
is_system?: boolean;
sort_order?: number;
created_at?: string;
updated_at?: string;
}
export interface ConfigGroup {
key: 'basic' | 'security' | 'game';
label: string;
description: string;
icon?: React.ReactNode;
}
export interface ConfigFormData {
[key: string]: string | number | boolean | object;
}
export interface EnvConfig {
JWT_SECRET: string;
JWT_EXPIRES_IN: string;
JWT_REFRESH_EXPIRES_IN: string;
GAME_SERVER_API: string;
GAME_SERVER_PSK: string;
SITE_NAME: string;
SITE_VERSION: string;
MAINTENANCE_MODE: string;
}
export interface ConfigHistory {
id: number;
config_key: string;
old_value: string;
new_value: string;
changed_by: number;
changed_reason?: string;
created_at: string;
admin_user?: {
username: string;
real_name: string;
};
}
export interface SaveConfigRequest {
config_key: string;
config_value: string;
config_type: string;
changed_reason?: string;
}
export interface ConfigValidationRule {
config_key: string;
required: boolean;
minLength?: number;
maxLength?: number;
pattern?: RegExp;
customValidator?: (value: any) => string | null;
}
export const CONFIG_GROUPS: ConfigGroup[] = [
{
key: 'basic',
label: '基本配置',
description: '系统基本设置和显示配置'
},
{
key: 'security',
label: '安全配置',
description: 'JWT认证、安全策略等安全相关配置'
},
{
key: 'game',
label: '游戏通信配置',
description: '游戏服务端通信和玩家相关配置'
}
];
export const CONFIG_VALIDATION_RULES: Record<string, ConfigValidationRule> = {
jwt_secret: {
config_key: 'jwt_secret',
required: true,
minLength: 32,
maxLength: 64,
pattern: /^[A-Za-z0-9_\-!@#$%^&*()_+\[\]{};':"\\|,.<>\/?]{32,64}$/
},
game_server_api: {
config_key: 'game_server_api',
required: true,
pattern: /^https?:\/\/.+/
},
game_server_psk: {
config_key: 'game_server_psk',
required: true,
minLength: 32,
maxLength: 64
},
jwt_expires_in: {
config_key: 'jwt_expires_in',
required: true,
customValidator: (value: any) => {
const num = Number(value);
if (isNaN(num) || num < 1 || num > 168) {
return 'JWT过期时间必须在1-168小时之间';
}
return null;
}
},
password_min_length: {
config_key: 'password_min_length',
required: true,
customValidator: (value: any) => {
const num = Number(value);
if (isNaN(num) || num < 6 || num > 32) {
return '密码最小长度必须在6-32位之间';
}
return null;
}
}
};

View File

@@ -1,27 +0,0 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}

View File

@@ -1,7 +0,0 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@@ -1,26 +0,0 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -1,13 +0,0 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
esbuild: {
target: 'es2020'
},
optimizeDeps: {
include: ['react', 'react-dom', 'antd']
}
})