Compare commits
12 Commits
209f50e4c4
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c55f57409 | ||
|
|
9d7bc5508a | ||
| a5f7d06ded | |||
| 59281f76fb | |||
|
|
429883b0bf | ||
|
|
5b2c2d35bc | ||
|
|
c4ec174828 | ||
|
|
2ca4cd60f6 | ||
|
|
fb51d51215 | ||
|
|
5a3d3918ba | ||
|
|
874b613d85 | ||
|
|
f5906044d8 |
75
.czrc
Normal file
75
.czrc
Normal file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"$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"
|
||||
}
|
||||
95
.czrc.json
Normal file
95
.czrc.json
Normal file
@@ -0,0 +1,95 @@
|
||||
{
|
||||
"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": ""
|
||||
}
|
||||
}
|
||||
143
.gitignore
vendored
143
.gitignore
vendored
@@ -1,138 +1,27 @@
|
||||
# ---> Node
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
.nuxt
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
.temp
|
||||
.cache
|
||||
|
||||
# vitepress build output
|
||||
**/.vitepress/dist
|
||||
|
||||
# vitepress cache directory
|
||||
**/.vitepress/cache
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# documentation
|
||||
docs
|
||||
|
||||
73
README.md
73
README.md
@@ -1,2 +1,73 @@
|
||||
# MYXY_Web
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||
|
||||
```js
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
|
||||
// Remove tseslint.configs.recommended and replace with this
|
||||
tseslint.configs.recommendedTypeChecked,
|
||||
// Alternatively, use this for stricter rules
|
||||
tseslint.configs.strictTypeChecked,
|
||||
// Optionally, add this for stylistic rules
|
||||
tseslint.configs.stylisticTypeChecked,
|
||||
|
||||
// Other configs...
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactX from 'eslint-plugin-react-x'
|
||||
import reactDom from 'eslint-plugin-react-dom'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
// Enable lint rules for React
|
||||
reactX.configs['recommended-typescript'],
|
||||
// Enable lint rules for React DOM
|
||||
reactDom.configs.recommended,
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
23
eslint.config.js
Normal file
23
eslint.config.js
Normal file
@@ -0,0 +1,23 @@
|
||||
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,
|
||||
},
|
||||
},
|
||||
])
|
||||
13
index.html
Normal file
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!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
Normal file
6089
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
40
package.json
Normal file
40
package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
96
sql/init_admin_users.sql
Normal file
96
sql/init_admin_users.sql
Normal file
@@ -0,0 +1,96 @@
|
||||
-- =====================================================
|
||||
-- 梦幻西游一站式运营管理系统 - 后台用户表初始化脚本
|
||||
-- 适用于 MYSQL 8.4
|
||||
-- 创建日期: 2025-12-12
|
||||
-- 描述: 创建后台管理员用户相关数据表
|
||||
-- =====================================================
|
||||
|
||||
-- 选择数据库
|
||||
USE mhxy_web;
|
||||
|
||||
-- 创建后台管理员用户表
|
||||
CREATE TABLE IF NOT EXISTS admin_users (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '管理员ID',
|
||||
username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',
|
||||
password VARCHAR(255) NOT NULL COMMENT '密码(加密存储)',
|
||||
real_name VARCHAR(100) COMMENT '真实姓名',
|
||||
email VARCHAR(100) COMMENT '邮箱地址',
|
||||
phone VARCHAR(20) COMMENT '联系电话',
|
||||
status TINYINT DEFAULT 1 COMMENT '账号状态: 0-禁用, 1-启用',
|
||||
is_super_admin TINYINT DEFAULT 0 COMMENT '是否超级管理员: 0-否, 1-是',
|
||||
last_login_time DATETIME COMMENT '最后登录时间',
|
||||
last_login_ip VARCHAR(45) COMMENT '最后登录IP',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
|
||||
-- 创建索引
|
||||
INDEX idx_username (username),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_created_at (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='后台管理员用户表';
|
||||
|
||||
-- 创建管理员操作日志表
|
||||
CREATE TABLE IF NOT EXISTS admin_operation_logs (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '日志ID',
|
||||
admin_user_id INT NOT NULL COMMENT '管理员ID',
|
||||
operation_type VARCHAR(50) NOT NULL COMMENT '操作类型',
|
||||
operation_desc TEXT COMMENT '操作描述',
|
||||
request_method VARCHAR(10) COMMENT '请求方法',
|
||||
request_url VARCHAR(255) COMMENT '请求URL',
|
||||
request_params TEXT COMMENT '请求参数',
|
||||
response_status INT COMMENT '响应状态码',
|
||||
ip_address VARCHAR(45) COMMENT 'IP地址',
|
||||
user_agent TEXT COMMENT '用户代理',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
|
||||
-- 创建外键约束
|
||||
FOREIGN KEY (admin_user_id) REFERENCES admin_users(id) ON DELETE CASCADE,
|
||||
|
||||
-- 创建索引
|
||||
INDEX idx_admin_user_id (admin_user_id),
|
||||
INDEX idx_operation_type (operation_type),
|
||||
INDEX idx_created_at (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='管理员操作日志表';
|
||||
|
||||
-- 创建系统配置表
|
||||
CREATE TABLE IF NOT EXISTS system_config (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '配置ID',
|
||||
config_key VARCHAR(100) NOT NULL UNIQUE COMMENT '配置键',
|
||||
config_value TEXT COMMENT '配置值',
|
||||
config_desc VARCHAR(255) COMMENT '配置描述',
|
||||
config_group VARCHAR(50) DEFAULT 'default' COMMENT '配置分组',
|
||||
is_editable TINYINT DEFAULT 1 COMMENT '是否可编辑: 0-否, 1-是',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
|
||||
-- 创建索引
|
||||
INDEX idx_config_key (config_key),
|
||||
INDEX idx_config_group (config_group)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统配置表';
|
||||
|
||||
-- 插入初始超级管理员账号(密码为: admin123,实际部署时需要加密)
|
||||
INSERT INTO admin_users (username, password, real_name, email, status, is_super_admin) VALUES
|
||||
('admin', '$2b$10$rOzK5V4yR7YqE6XoV4Wz9ODz8rN2L5A3C7B1F9H6I4J2K3L5M6N7O8P', '系统管理员', 'admin@mhxy.com', 1, 1);
|
||||
|
||||
-- 说明: 上面插入的密码是 'admin123' 的BCrypt加密结果
|
||||
-- 生产环境中应该使用更复杂的密码
|
||||
-- 加密密码生成示例(实际使用时需要在应用中处理):
|
||||
-- const bcrypt = require('bcrypt');
|
||||
-- const hashedPassword = await bcrypt.hash('admin123', 10);
|
||||
|
||||
-- 插入系统默认配置
|
||||
INSERT INTO system_config (config_key, config_value, config_desc, config_group) VALUES
|
||||
('site_name', '梦幻西游一站式运营管理系统', '网站名称', 'basic'),
|
||||
('site_description', '梦幻西游游戏运营管理后台系统', '网站描述', 'basic'),
|
||||
('admin_session_timeout', '7200', '管理员会话超时时间(秒)', 'security'),
|
||||
('max_login_attempts', '5', '最大登录尝试次数', 'security'),
|
||||
('enable_captcha', '1', '是否启用验证码', 'security');
|
||||
|
||||
-- =====================================================
|
||||
-- 使用说明:
|
||||
-- 1. 在MySQL 8.4中执行此脚本
|
||||
-- 2. 确保数据库存在: CREATE DATABASE IF NOT EXISTS mhxy_web;
|
||||
-- 3. 执行脚本: source /path/to/init_admin_users.sql;
|
||||
-- 4. 初始管理员账号: admin / admin123
|
||||
-- 5. 部署到生产环境时,请修改默认密码
|
||||
-- =====================================================
|
||||
98
sql/insert_config_data_compatible.sql
Normal file
98
sql/insert_config_data_compatible.sql
Normal file
@@ -0,0 +1,98 @@
|
||||
-- =============================================
|
||||
-- 梦幻西游运营管理系统 - 系统配置数据插入脚本 (兼容版)
|
||||
-- 版本: 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;
|
||||
40
src/App.css
Normal file
40
src/App.css
Normal file
@@ -0,0 +1,40 @@
|
||||
#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;
|
||||
}
|
||||
61
src/App.tsx
Normal file
61
src/App.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
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;
|
||||
1
src/assets/react.svg
Normal file
1
src/assets/react.svg
Normal file
@@ -0,0 +1 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
186
src/contexts/AuthContext.tsx
Normal file
186
src/contexts/AuthContext.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||
import { message } from 'antd';
|
||||
|
||||
// 管理员用户接口
|
||||
export interface AdminUser {
|
||||
id: number;
|
||||
username: string;
|
||||
real_name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
status: number;
|
||||
is_super_admin: number;
|
||||
last_login_time: string;
|
||||
last_login_ip: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// 认证上下文接口
|
||||
interface AuthContextType {
|
||||
user: AdminUser | null;
|
||||
isLoading: boolean;
|
||||
isAuthenticated: boolean;
|
||||
login: (username: string, password: string) => Promise<boolean>;
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
// 创建认证上下文
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
// 模拟API调用
|
||||
const mockApiCall = {
|
||||
login: async (username: string, password: string): Promise<AdminUser> => {
|
||||
// 模拟网络延迟
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// 模拟验证逻辑
|
||||
if (username === 'admin' && password === 'admin123') {
|
||||
const mockUser: AdminUser = {
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
real_name: '系统管理员',
|
||||
email: 'admin@mhxy.com',
|
||||
phone: '13800138000',
|
||||
status: 1,
|
||||
is_super_admin: 1,
|
||||
last_login_time: new Date().toISOString(),
|
||||
last_login_ip: '127.0.0.1',
|
||||
created_at: '2025-12-12T00:00:00.000Z',
|
||||
updated_at: '2025-12-12T00:00:00.000Z'
|
||||
};
|
||||
return mockUser;
|
||||
} else {
|
||||
throw new Error('用户名或密码错误');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 认证提供者组件
|
||||
* @param children - 子组件
|
||||
*/
|
||||
export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const [user, setUser] = useState<AdminUser | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// 检查本地存储中的用户信息
|
||||
useEffect(() => {
|
||||
const checkAuthStatus = () => {
|
||||
try {
|
||||
const storedUser = localStorage.getItem('admin_user');
|
||||
const token = localStorage.getItem('admin_token');
|
||||
|
||||
if (storedUser && token) {
|
||||
setUser(JSON.parse(storedUser));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查认证状态失败:', error);
|
||||
// 清除无效的本地存储
|
||||
localStorage.removeItem('admin_user');
|
||||
localStorage.removeItem('admin_token');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkAuthStatus();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 登录方法
|
||||
* @param username - 用户名
|
||||
* @param password - 密码
|
||||
* @returns Promise<boolean> - 登录是否成功
|
||||
*/
|
||||
const login = async (username: string, password: string): Promise<boolean> => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const loggedInUser = await mockApiCall.login(username, password);
|
||||
|
||||
// 存储用户信息和token
|
||||
const token = `mock_token_${loggedInUser.id}_${Date.now()}`;
|
||||
localStorage.setItem('admin_user', JSON.stringify(loggedInUser));
|
||||
localStorage.setItem('admin_token', token);
|
||||
|
||||
setUser(loggedInUser);
|
||||
message.success(`欢迎回来,${loggedInUser.real_name}!`);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '登录失败,请重试');
|
||||
return false;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 登出方法
|
||||
*/
|
||||
const logout = () => {
|
||||
localStorage.removeItem('admin_user');
|
||||
localStorage.removeItem('admin_token');
|
||||
setUser(null);
|
||||
message.info('已安全登出');
|
||||
};
|
||||
|
||||
const value: AuthContextType = {
|
||||
user,
|
||||
isLoading,
|
||||
isAuthenticated: !!user,
|
||||
login,
|
||||
logout
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={value}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 使用认证上下文的Hook
|
||||
* @returns AuthContextType
|
||||
*/
|
||||
export const useAuth = (): AuthContextType => {
|
||||
const context = useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useAuth 必须在 AuthProvider 内部使用');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
/**
|
||||
* 管理员权限验证组件
|
||||
* @param children - 子组件
|
||||
* @param fallback - 未认证时的回退组件
|
||||
*/
|
||||
export const AdminProtectedRoute: React.FC<{
|
||||
children: ReactNode;
|
||||
fallback?: ReactNode;
|
||||
}> = ({ children, fallback }) => {
|
||||
const { isAuthenticated, isLoading } = useAuth();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '100vh'
|
||||
}}>
|
||||
正在验证身份...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return fallback || <div>请先登录</div>;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export default AuthContext;
|
||||
67
src/index.css
Normal file
67
src/index.css
Normal file
@@ -0,0 +1,67 @@
|
||||
: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;
|
||||
}
|
||||
}
|
||||
494
src/layouts/AdminLayout.tsx
Normal file
494
src/layouts/AdminLayout.tsx
Normal file
@@ -0,0 +1,494 @@
|
||||
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;
|
||||
331
src/layouts/TabNavigation.tsx
Normal file
331
src/layouts/TabNavigation.tsx
Normal file
@@ -0,0 +1,331 @@
|
||||
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;
|
||||
11
src/main.tsx
Normal file
11
src/main.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
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>,
|
||||
)
|
||||
441
src/pages/AdminDashboard.tsx
Normal file
441
src/pages/AdminDashboard.tsx
Normal file
@@ -0,0 +1,441 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Card,
|
||||
Row,
|
||||
Col,
|
||||
Statistic,
|
||||
Progress,
|
||||
Typography,
|
||||
Space,
|
||||
Button,
|
||||
Tag,
|
||||
theme
|
||||
} from 'antd';
|
||||
import {
|
||||
UserOutlined,
|
||||
DollarOutlined,
|
||||
TeamOutlined,
|
||||
WarningOutlined,
|
||||
CheckCircleOutlined,
|
||||
CloseCircleOutlined,
|
||||
ClockCircleOutlined,
|
||||
TrophyOutlined,
|
||||
FireOutlined,
|
||||
ShoppingCartOutlined,
|
||||
NotificationOutlined,
|
||||
RiseOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
/**
|
||||
* 模拟数据接口
|
||||
*/
|
||||
interface DashboardStats {
|
||||
totalUsers: number;
|
||||
activeUsers: number;
|
||||
todayRevenue: number;
|
||||
monthlyRevenue: number;
|
||||
totalServers: number;
|
||||
onlineServers: number;
|
||||
}
|
||||
|
||||
interface QuickAction {
|
||||
title: string;
|
||||
icon: React.ReactNode;
|
||||
color: string;
|
||||
action: () => void;
|
||||
}
|
||||
|
||||
interface RecentActivity {
|
||||
id: string;
|
||||
user: string;
|
||||
action: string;
|
||||
time: string;
|
||||
status: 'success' | 'warning' | 'error' | 'info';
|
||||
}
|
||||
|
||||
interface SystemAlert {
|
||||
id: string;
|
||||
type: 'info' | 'warning' | 'error';
|
||||
message: string;
|
||||
time: string;
|
||||
resolved: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认工作台页面
|
||||
* 管理员登录后的首页
|
||||
*/
|
||||
const AdminDashboard: React.FC = () => {
|
||||
const { user } = useAuth();
|
||||
const { token } = theme.useToken();
|
||||
|
||||
// 模拟数据
|
||||
const dashboardStats: DashboardStats = {
|
||||
totalUsers: 125840,
|
||||
activeUsers: 8756,
|
||||
todayRevenue: 125680.50,
|
||||
monthlyRevenue: 3756000.00,
|
||||
totalServers: 24,
|
||||
onlineServers: 23
|
||||
};
|
||||
|
||||
// 快速操作
|
||||
const quickActions: QuickAction[] = [
|
||||
{
|
||||
title: '用户管理',
|
||||
icon: <UserOutlined />,
|
||||
color: '#1890ff',
|
||||
action: () => console.log('跳转到用户管理')
|
||||
},
|
||||
{
|
||||
title: '服务器监控',
|
||||
icon: <FireOutlined />,
|
||||
color: '#fa541c',
|
||||
action: () => console.log('跳转到服务器监控')
|
||||
},
|
||||
{
|
||||
title: '订单处理',
|
||||
icon: <ShoppingCartOutlined />,
|
||||
color: '#52c41a',
|
||||
action: () => console.log('跳转到订单处理')
|
||||
},
|
||||
{
|
||||
title: '系统公告',
|
||||
icon: <NotificationOutlined />,
|
||||
color: '#722ed1',
|
||||
action: () => console.log('跳转到系统公告')
|
||||
}
|
||||
];
|
||||
|
||||
// 最近活动数据
|
||||
const recentActivities: RecentActivity[] = [
|
||||
{
|
||||
id: '1',
|
||||
user: '张三',
|
||||
action: '完成了用户充值审核',
|
||||
time: '2分钟前',
|
||||
status: 'success'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
user: '李四',
|
||||
action: '提交了服务器维护申请',
|
||||
time: '5分钟前',
|
||||
status: 'warning'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
user: '王五',
|
||||
action: '处理了玩家投诉',
|
||||
time: '10分钟前',
|
||||
status: 'info'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
user: '赵六',
|
||||
action: '更新了游戏道具信息',
|
||||
time: '15分钟前',
|
||||
status: 'success'
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
user: '孙七',
|
||||
action: '删除了异常订单',
|
||||
time: '20分钟前',
|
||||
status: 'error'
|
||||
}
|
||||
];
|
||||
|
||||
// 系统告警数据
|
||||
const systemAlerts: SystemAlert[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'warning',
|
||||
message: '服务器 CPU 使用率超过 80%',
|
||||
time: '5分钟前',
|
||||
resolved: false
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'info',
|
||||
message: '今日玩家活跃度较昨日增长 15%',
|
||||
time: '10分钟前',
|
||||
resolved: false
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'error',
|
||||
message: '支付接口响应异常',
|
||||
time: '30分钟前',
|
||||
resolved: true
|
||||
}
|
||||
];
|
||||
|
||||
// 获取状态图标
|
||||
const getStatusIcon = (status: RecentActivity['status']) => {
|
||||
switch (status) {
|
||||
case 'success':
|
||||
return <CheckCircleOutlined style={{ color: '#52c41a' }} />;
|
||||
case 'warning':
|
||||
return <WarningOutlined style={{ color: '#faad14' }} />;
|
||||
case 'error':
|
||||
return <CloseCircleOutlined style={{ color: '#ff4d4f' }} />;
|
||||
case 'info':
|
||||
default:
|
||||
return <ClockCircleOutlined style={{ color: '#1890ff' }} />;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取告警标签颜色
|
||||
const getAlertColor = (type: SystemAlert['type']) => {
|
||||
switch (type) {
|
||||
case 'error':
|
||||
return 'error';
|
||||
case 'warning':
|
||||
return 'warning';
|
||||
case 'info':
|
||||
default:
|
||||
return 'processing';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '0' }}>
|
||||
{/* 欢迎标题 */}
|
||||
<div style={{ marginBottom: '24px' }}>
|
||||
<Title level={2} style={{ margin: 0, color: token.colorText }}>
|
||||
欢迎回来,{user?.real_name || user?.username}!
|
||||
</Title>
|
||||
<Text type="secondary">
|
||||
今天是 {new Date().toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
weekday: 'long'
|
||||
})}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<Row gutter={[16, 16]} style={{ marginBottom: '24px' }}>
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="总用户数"
|
||||
value={dashboardStats.totalUsers}
|
||||
prefix={<UserOutlined style={{ color: '#1890ff' }} />}
|
||||
suffix="人"
|
||||
styles={{ content: { color: '#1890ff' } }}
|
||||
/>
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
较上月增长 <Text type="success">+12.5%</Text>
|
||||
</Text>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="在线用户"
|
||||
value={dashboardStats.activeUsers}
|
||||
prefix={<TeamOutlined style={{ color: '#52c41a' }} />}
|
||||
suffix="人"
|
||||
styles={{ content: { color: '#52c41a' } }}
|
||||
/>
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Progress
|
||||
percent={Math.round((dashboardStats.activeUsers / dashboardStats.totalUsers) * 100)}
|
||||
size="small"
|
||||
showInfo={false}
|
||||
strokeColor="#52c41a"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="今日收入"
|
||||
value={dashboardStats.todayRevenue}
|
||||
prefix={<DollarOutlined style={{ color: '#fa8c16' }} />}
|
||||
suffix="元"
|
||||
precision={2}
|
||||
styles={{ content: { color: '#fa8c16' } }}
|
||||
/>
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
较昨日 <Text type="success">+8.3%</Text>
|
||||
</Text>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="月度收入"
|
||||
value={dashboardStats.monthlyRevenue}
|
||||
prefix={<RiseOutlined style={{ color: '#722ed1' }} />}
|
||||
suffix="元"
|
||||
precision={2}
|
||||
styles={{ content: { color: '#722ed1' } }}
|
||||
/>
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
目标完成度 <Text type="success">85.6%</Text>
|
||||
</Text>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 主要内容区域 */}
|
||||
<Row gutter={[16, 16]}>
|
||||
{/* 快速操作 */}
|
||||
<Col xs={24} lg={8}>
|
||||
<Card title="快速操作" style={{ height: '100%' }}>
|
||||
<Row gutter={[16, 16]}>
|
||||
{quickActions.map((action, index) => (
|
||||
<Col xs={12} key={index}>
|
||||
<Button
|
||||
type="default"
|
||||
icon={action.icon}
|
||||
style={{
|
||||
height: '80px',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderColor: action.color,
|
||||
color: action.color
|
||||
}}
|
||||
onClick={action.action}
|
||||
>
|
||||
<div style={{ fontSize: '18px', marginBottom: '4px' }}>
|
||||
{action.icon}
|
||||
</div>
|
||||
<span style={{ fontSize: '12px' }}>{action.title}</span>
|
||||
</Button>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* 最近活动 */}
|
||||
<Col xs={24} lg={8}>
|
||||
<Card title="最近活动" style={{ height: '100%' }}>
|
||||
<div style={{ maxHeight: '300px', overflow: 'auto' }}>
|
||||
{recentActivities.map((item) => (
|
||||
<div key={item.id} style={{
|
||||
padding: '12px 0',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<div style={{ marginTop: '2px' }}>
|
||||
{getStatusIcon(item.status)}
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: '4px'
|
||||
}}>
|
||||
<Text strong style={{ fontSize: '13px' }}>{item.user}</Text>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>{item.time}</Text>
|
||||
</div>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
{item.action}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ textAlign: 'center', marginTop: '16px' }}>
|
||||
<Button type="link" size="small">查看全部</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* 系统状态 */}
|
||||
<Col xs={24} lg={8}>
|
||||
<Card title="系统状态" style={{ height: '100%' }}>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
|
||||
<Text>服务器在线率</Text>
|
||||
<Text strong>
|
||||
{Math.round((dashboardStats.onlineServers / dashboardStats.totalServers) * 100)}%
|
||||
</Text>
|
||||
</div>
|
||||
<Progress
|
||||
percent={Math.round((dashboardStats.onlineServers / dashboardStats.totalServers) * 100)}
|
||||
strokeColor={dashboardStats.onlineServers === dashboardStats.totalServers ? '#52c41a' : '#faad14'}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<Text strong style={{ fontSize: '13px' }}>系统告警</Text>
|
||||
</div>
|
||||
|
||||
<div style={{ maxHeight: '200px', overflow: 'auto' }}>
|
||||
{systemAlerts.map((alert) => (
|
||||
<div key={alert.id} style={{ padding: '8px 0', display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<Tag color={getAlertColor(alert.type)} style={{ margin: 0 }}>
|
||||
{alert.type === 'error' ? '错误' : alert.type === 'warning' ? '警告' : '信息'}
|
||||
</Tag>
|
||||
<Text style={{ fontSize: '12px', flex: 1 }}>
|
||||
{alert.message}
|
||||
</Text>
|
||||
<Text type="secondary" style={{ fontSize: '11px' }}>
|
||||
{alert.time}
|
||||
</Text>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ textAlign: 'center', marginTop: '16px' }}>
|
||||
<Button type="link" size="small">查看全部告警</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 底部信息 */}
|
||||
<Row style={{ marginTop: '24px' }}>
|
||||
<Col span={24}>
|
||||
<Card>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Space>
|
||||
<TrophyOutlined style={{ color: '#faad14', fontSize: '18px' }} />
|
||||
<div>
|
||||
<Text strong>本月运营之星</Text>
|
||||
<br />
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
李明 - 完成订单处理 1,256 单
|
||||
</Text>
|
||||
</div>
|
||||
</Space>
|
||||
|
||||
<Space>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
系统版本:v1.0.0 | 最后更新:2025-12-12
|
||||
</Text>
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminDashboard;
|
||||
195
src/pages/AdminLogin.tsx
Normal file
195
src/pages/AdminLogin.tsx
Normal file
@@ -0,0 +1,195 @@
|
||||
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;
|
||||
338
src/pages/ConfigHistoryModal.tsx
Normal file
338
src/pages/ConfigHistoryModal.tsx
Normal file
@@ -0,0 +1,338 @@
|
||||
/**
|
||||
* 配置历史记录模态框组件
|
||||
* @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;
|
||||
21
src/pages/Home.tsx
Normal file
21
src/pages/Home.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
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;
|
||||
153
src/pages/PlayerLogin.tsx
Normal file
153
src/pages/PlayerLogin.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
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;
|
||||
253
src/pages/SystemConfigPage.tsx
Normal file
253
src/pages/SystemConfigPage.tsx
Normal file
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* 系统配置页面主组件
|
||||
* @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;
|
||||
317
src/pages/tabs/BasicConfigTab.tsx
Normal file
317
src/pages/tabs/BasicConfigTab.tsx
Normal file
@@ -0,0 +1,317 @@
|
||||
/**
|
||||
* 基本配置标签页组件
|
||||
* @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;
|
||||
503
src/pages/tabs/GameConfigTab.tsx
Normal file
503
src/pages/tabs/GameConfigTab.tsx
Normal file
@@ -0,0 +1,503 @@
|
||||
/**
|
||||
* 游戏通信配置标签页组件
|
||||
* @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;
|
||||
428
src/pages/tabs/SecurityConfigTab.tsx
Normal file
428
src/pages/tabs/SecurityConfigTab.tsx
Normal file
@@ -0,0 +1,428 @@
|
||||
/**
|
||||
* 安全配置标签页组件
|
||||
* @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;
|
||||
230
src/services/systemConfigService.ts
Normal file
230
src/services/systemConfigService.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* 系统配置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;
|
||||
135
src/types/systemConfig.ts
Normal file
135
src/types/systemConfig.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* 系统配置相关类型定义
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
};
|
||||
27
tsconfig.app.json
Normal file
27
tsconfig.app.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
||||
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
26
tsconfig.node.json
Normal file
26
tsconfig.node.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
||||
13
vite.config.ts
Normal file
13
vite.config.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
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']
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user