diff --git a/.cz-config.js b/.cz-config.js
new file mode 100644
index 0000000..2665458
--- /dev/null
+++ b/.cz-config.js
@@ -0,0 +1,107 @@
+module.exports = {
+ 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",
+ themeColorCode: "",
+ scopes: [],
+ allowCustomScopes: true,
+ allowEmptyScopes: true,
+ customScopesAlign: "bottom",
+ customScopesAlias: "custom",
+ emptyScopesAlias: "empty",
+ upperCaseSubject: false,
+ allowBreakingChanges: ["feat", "fix"],
+ breaklineNumber: 100,
+ breaklineChar: "|",
+ skipQuestions: [],
+ issuePrefixes: [
+ {
+ value: "#",
+ name: "#: 关联issue"
+ }
+ ],
+ customIssuePrefixAlign: "top",
+ emptyIssuePrefixAlias: "skip",
+ allowCustomIssuePrefix: true,
+ allowEmptyIssuePrefix: true,
+ confirmColorize: true,
+ maxHeaderLength: 100,
+ maxSubjectLength: 100,
+ minSubjectLength: 0,
+ scopeOverrides: {
+ feat: [],
+ fix: []
+ },
+ defaultBody: "",
+ defaultIssues: "",
+ defaultScope: "",
+ defaultType: ""
+};
\ No newline at end of file
diff --git a/.czrc b/.czrc
new file mode 100644
index 0000000..33f51da
--- /dev/null
+++ b/.czrc
@@ -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"
+}
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8faae5b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,130 @@
+# 梦幻西游一站式运营管理平台
+
+## 项目结构
+
+```
+JGE-RS-SL-WEB/
+├── backend/ # NestJS后端
+│ ├── src/
+│ │ ├── auth/ # 后台认证模块
+│ │ ├── player/ # 玩家服务模块
+│ │ ├── common/ # 公共模块(过滤器、拦截器、管道)
+│ │ ├── entities/ # 数据库实体
+│ │ └── config/ # 配置文件
+│ └── database/ # 数据库初始化脚本
+└── frontend/ # React前端
+ ├── src/
+ │ ├── pages/ # 页面组件
+ │ ├── components/ # 公共组件
+ │ ├── services/ # API服务
+ │ ├── stores/ # 状态管理
+ │ └── router/ # 路由配置
+```
+
+## 环境要求
+
+- Node.js 22.x
+- MySQL 8.4
+- npm 10.x
+
+## 快速开始
+
+### 1. 数据库初始化
+
+1. 创建数据库:
+```sql
+CREATE DATABASE jge_rs_sl_web DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+```
+
+2. 导入初始化脚本:
+```bash
+mysql -u root -p jge_rs_sl_web < backend/database/init.sql
+```
+
+3. 默认管理员账号:
+ - 用户名:admin
+ - 密码:admin123
+
+### 2. 后端启动
+
+1. 配置环境变量:
+编辑 `backend/.env` 文件,修改数据库连接信息:
+```
+DB_HOST=localhost
+DB_PORT=3306
+DB_USERNAME=root
+DB_PASSWORD=your_password
+DB_DATABASE=jge_rs_sl_web
+JWT_SECRET=your_jwt_secret_key_change_in_production
+JWT_EXPIRES_IN=24h
+PORT=3000
+NODE_ENV=development
+```
+
+2. 安装依赖并启动:
+```bash
+cd backend
+npm install
+npm run start:dev
+```
+
+后端服务将在 http://localhost:3000 启动
+
+### 3. 前端启动
+
+1. 安装依赖并启动:
+```bash
+cd frontend
+npm install
+npm run dev
+```
+
+前端服务将在 http://localhost:5173 启动
+
+## 功能模块
+
+### 玩家服务中心 (/player)
+- 登录页面
+- 控制台布局(Header、Content、Footer)
+- 控制台主页
+
+### 运营管理系统后台 (/admin)
+- 登录页面(JWT认证)
+- 后台布局(Header、Sider、Content、Footer)
+- 工作台页面
+
+## 技术栈
+
+### 后端
+- NestJS 11.x
+- TypeORM
+- MySQL 8.4
+- JWT认证
+- Passport
+- Bcrypt密码加密
+
+### 前端
+- React 19.x
+- TypeScript
+- Vite
+- Ant Design 6.1.2
+- React Router
+- Zustand状态管理
+- Axios
+
+## API接口
+
+### 后台认证
+- POST /admin/auth/login - 后台登录
+- POST /admin/auth/logout - 后台登出
+
+### 玩家认证
+- POST /player/auth/login - 玩家登录
+- POST /player/auth/logout - 玩家登出
+
+## 开发说明
+
+- 后端代码遵循NestJS最佳实践
+- 前端代码严格遵循Ant Design官方标准
+- JWT Token存储在内存中,不使用localStorage/sessionStorage
+- 所有API响应格式统一
diff --git a/arco-design-pro/package.json b/arco-design-pro/package.json
deleted file mode 100644
index 2d1b186..0000000
--- a/arco-design-pro/package.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "dependencies": {
- "arco-cli": "^1.27.5"
- }
-}
diff --git a/backend/.prettierrc b/backend/.prettierrc
new file mode 100644
index 0000000..38bbcbd
--- /dev/null
+++ b/backend/.prettierrc
@@ -0,0 +1,4 @@
+{
+ "singleQuote": true,
+ "trailingComma": "all"
+}
diff --git a/backend/README.md b/backend/README.md
new file mode 100644
index 0000000..098a4bb
--- /dev/null
+++ b/backend/README.md
@@ -0,0 +1,98 @@
+
+
+
+
+[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
+[circleci-url]: https://circleci.com/gh/nestjs/nest
+
+ A progressive Node.js framework for building efficient and scalable server-side applications.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Description
+
+[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
+
+## Project setup
+
+```bash
+$ npm install
+```
+
+## Compile and run the project
+
+```bash
+# development
+$ npm run start
+
+# watch mode
+$ npm run start:dev
+
+# production mode
+$ npm run start:prod
+```
+
+## Run tests
+
+```bash
+# unit tests
+$ npm run test
+
+# e2e tests
+$ npm run test:e2e
+
+# test coverage
+$ npm run test:cov
+```
+
+## Deployment
+
+When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
+
+If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
+
+```bash
+$ npm install -g @nestjs/mau
+$ mau deploy
+```
+
+With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
+
+## Resources
+
+Check out a few resources that may come in handy when working with NestJS:
+
+- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
+- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
+- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
+- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
+- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
+- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
+- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
+- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
+
+## Support
+
+Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
+
+## Stay in touch
+
+- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
+- Website - [https://nestjs.com](https://nestjs.com/)
+- Twitter - [@nestframework](https://twitter.com/nestframework)
+
+## License
+
+Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
diff --git a/backend/database/init.sql b/backend/database/init.sql
new file mode 100644
index 0000000..1b9d42e
--- /dev/null
+++ b/backend/database/init.sql
@@ -0,0 +1,30 @@
+-- 梦幻西游一站式运营管理平台 - 数据库初始化脚本
+-- MySQL 8.4 版本
+-- 创建时间:2025-12-27
+
+-- 设置字符集
+SET NAMES utf8mb4;
+SET FOREIGN_KEY_CHECKS = 0;
+
+-- 后台用户表
+CREATE TABLE IF NOT EXISTS admin_users (
+ id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID',
+ username VARCHAR(50) UNIQUE NOT NULL COMMENT '用户名',
+ password_hash CHAR(60) NOT NULL COMMENT '密码哈希(bcrypt)',
+ role ENUM('super_admin', 'operator', 'viewer') NOT NULL DEFAULT 'viewer' COMMENT '角色:super_admin-超级管理员, operator-操作员, viewer-查看者',
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+ INDEX idx_username (username)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='后台用户表';
+
+-- 插入默认超级管理员账号
+-- 用户名:admin
+-- 密码:admin123
+-- 注意:密码哈希需要使用bcrypt生成,以下是admin123的bcrypt哈希值(10轮)
+INSERT INTO admin_users (username, password_hash, role) VALUES
+('admin', '$2b$10$Q1RH29Lsi4y/uq9ZIej1a.nRUv/7gNgdnga.tVStXGARE/J0rrF5K', 'super_admin');
+
+-- 验证插入结果
+SELECT * FROM admin_users;
+
+SET FOREIGN_KEY_CHECKS = 1;
diff --git a/backend/database/update-admin-password.sql b/backend/database/update-admin-password.sql
new file mode 100644
index 0000000..b20658b
--- /dev/null
+++ b/backend/database/update-admin-password.sql
@@ -0,0 +1,10 @@
+-- 更新管理员密码
+-- 用户名:admin
+-- 密码:admin123456
+-- 新的bcrypt哈希值(10轮)
+UPDATE admin_users
+SET password_hash = '$2b$10$t1yFXiPAfHUJONTjxnFYae3Q4petGiD3swtAJC2mtYNJcujxu0raa'
+WHERE username = 'admin';
+
+-- 验证更新结果
+SELECT id, username, password_hash, role FROM admin_users;
diff --git a/backend/eslint.config.mjs b/backend/eslint.config.mjs
new file mode 100644
index 0000000..7aff321
--- /dev/null
+++ b/backend/eslint.config.mjs
@@ -0,0 +1,35 @@
+// @ts-check
+import eslint from '@eslint/js';
+import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
+import globals from 'globals';
+import tseslint from 'typescript-eslint';
+
+export default tseslint.config(
+ {
+ ignores: ['eslint.config.mjs'],
+ },
+ eslint.configs.recommended,
+ ...tseslint.configs.recommendedTypeChecked,
+ eslintPluginPrettierRecommended,
+ {
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ ...globals.jest,
+ },
+ sourceType: 'commonjs',
+ parserOptions: {
+ projectService: true,
+ tsconfigRootDir: import.meta.dirname,
+ },
+ },
+ },
+ {
+ rules: {
+ '@typescript-eslint/no-explicit-any': 'off',
+ '@typescript-eslint/no-floating-promises': 'warn',
+ '@typescript-eslint/no-unsafe-argument': 'warn',
+ "prettier/prettier": ["error", { endOfLine: "auto" }],
+ },
+ },
+);
diff --git a/backend/nest-cli.json b/backend/nest-cli.json
new file mode 100644
index 0000000..a8170d1
--- /dev/null
+++ b/backend/nest-cli.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "https://json.schemastore.org/nest-cli",
+ "collection": "@nestjs/schematics",
+ "sourceRoot": "src",
+ "compilerOptions": {
+ "deleteOutDir": true
+ }
+}
diff --git a/backend/package.json b/backend/package.json
new file mode 100644
index 0000000..284da8c
--- /dev/null
+++ b/backend/package.json
@@ -0,0 +1,86 @@
+{
+ "name": "backend",
+ "version": "0.0.1",
+ "description": "",
+ "author": "",
+ "private": true,
+ "license": "UNLICENSED",
+ "scripts": {
+ "build": "nest build",
+ "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
+ "start": "nest start",
+ "start:dev": "nest start --watch",
+ "start:debug": "nest start --debug --watch",
+ "start:prod": "node dist/main",
+ "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
+ "test": "jest",
+ "test:watch": "jest --watch",
+ "test:cov": "jest --coverage",
+ "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
+ "test:e2e": "jest --config ./test/jest-e2e.json"
+ },
+ "dependencies": {
+ "@nestjs/axios": "^4.0.1",
+ "@nestjs/common": "^11.0.1",
+ "@nestjs/config": "^4.0.2",
+ "@nestjs/core": "^11.0.1",
+ "@nestjs/jwt": "^11.0.2",
+ "@nestjs/passport": "^11.0.5",
+ "@nestjs/platform-express": "^11.0.1",
+ "@nestjs/typeorm": "^11.0.0",
+ "axios": "^1.13.2",
+ "bcrypt": "^6.0.0",
+ "class-transformer": "^0.5.1",
+ "class-validator": "^0.14.3",
+ "mysql2": "^3.16.0",
+ "passport": "^0.7.0",
+ "passport-jwt": "^4.0.1",
+ "reflect-metadata": "^0.2.2",
+ "rxjs": "^7.8.1",
+ "typeorm": "^0.3.28"
+ },
+ "devDependencies": {
+ "@eslint/eslintrc": "^3.2.0",
+ "@eslint/js": "^9.18.0",
+ "@nestjs/cli": "^11.0.0",
+ "@nestjs/schematics": "^11.0.0",
+ "@nestjs/testing": "^11.0.1",
+ "@types/bcrypt": "^6.0.0",
+ "@types/express": "^5.0.0",
+ "@types/jest": "^30.0.0",
+ "@types/node": "^22.10.7",
+ "@types/passport-jwt": "^4.0.1",
+ "@types/supertest": "^6.0.2",
+ "eslint": "^9.18.0",
+ "eslint-config-prettier": "^10.0.1",
+ "eslint-plugin-prettier": "^5.2.2",
+ "globals": "^16.0.0",
+ "jest": "^30.0.0",
+ "prettier": "^3.4.2",
+ "source-map-support": "^0.5.21",
+ "supertest": "^7.0.0",
+ "ts-jest": "^29.2.5",
+ "ts-loader": "^9.5.2",
+ "ts-node": "^10.9.2",
+ "tsconfig-paths": "^4.2.0",
+ "typescript": "^5.7.3",
+ "typescript-eslint": "^8.20.0"
+ },
+ "jest": {
+ "moduleFileExtensions": [
+ "js",
+ "json",
+ "ts"
+ ],
+ "rootDir": "src",
+ "testRegex": ".*\\.spec\\.ts$",
+ "transform": {
+ "^.+\\.(t|j)s$": "ts-jest"
+ },
+ "collectCoverageFrom": [
+ "**/*.(t|j)s"
+ ],
+ "coverageDirectory": "../coverage",
+ "testEnvironment": "node"
+ }
+}
diff --git a/backend/src/app.controller.ts b/backend/src/app.controller.ts
new file mode 100644
index 0000000..ca5eede
--- /dev/null
+++ b/backend/src/app.controller.ts
@@ -0,0 +1,12 @@
+import { Controller, Get } from '@nestjs/common';
+import { AppService } from './app.service';
+
+@Controller()
+export class AppController {
+ constructor(private readonly appService: AppService) {}
+
+ @Get()
+ getHello(): string {
+ return this.appService.getHello();
+ }
+}
diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts
new file mode 100644
index 0000000..f0b811d
--- /dev/null
+++ b/backend/src/app.module.ts
@@ -0,0 +1,33 @@
+import { Module } from '@nestjs/common';
+import { ConfigModule } from '@nestjs/config';
+import { TypeOrmModule } from '@nestjs/typeorm';
+import { AppController } from './app.controller';
+import { AppService } from './app.service';
+import { AuthModule } from './auth/auth.module';
+import { PlayerModule } from './player/player.module';
+
+@Module({
+ imports: [
+ ConfigModule.forRoot({
+ isGlobal: true,
+ envFilePath: '.env',
+ }),
+ TypeOrmModule.forRoot({
+ type: 'mysql',
+ host: process.env.DB_HOST || 'localhost',
+ port: parseInt(process.env.DB_PORT || '3306', 10),
+ username: process.env.DB_USERNAME || 'root',
+ password: process.env.DB_PASSWORD || '',
+ database: process.env.DB_DATABASE || 'jge_rs_sl_web',
+ entities: [__dirname + '/**/*.entity{.ts,.js}'],
+ synchronize: process.env.NODE_ENV === 'development',
+ logging: process.env.NODE_ENV === 'development',
+ charset: 'utf8mb4',
+ }),
+ AuthModule,
+ PlayerModule,
+ ],
+ controllers: [AppController],
+ providers: [AppService],
+})
+export class AppModule {}
diff --git a/backend/src/app.service.ts b/backend/src/app.service.ts
new file mode 100644
index 0000000..d12de69
--- /dev/null
+++ b/backend/src/app.service.ts
@@ -0,0 +1,8 @@
+import { Injectable } from '@nestjs/common';
+
+@Injectable()
+export class AppService {
+ getHello(): string {
+ return 'Hello World!';
+ }
+}
diff --git a/backend/src/auth/auth.controller.ts b/backend/src/auth/auth.controller.ts
new file mode 100644
index 0000000..5a3d2f2
--- /dev/null
+++ b/backend/src/auth/auth.controller.ts
@@ -0,0 +1,21 @@
+import { Controller, Post, Body, HttpCode, HttpStatus } from '@nestjs/common';
+import { AuthService } from './auth.service';
+import { LoginDto } from './dto/login.dto';
+import { LoginResponseDto } from './dto/login-response.dto';
+
+@Controller('admin/auth')
+export class AuthController {
+ constructor(private authService: AuthService) {}
+
+ @Post('login')
+ @HttpCode(HttpStatus.OK)
+ async login(@Body() loginDto: LoginDto): Promise {
+ return this.authService.login(loginDto);
+ }
+
+ @Post('logout')
+ @HttpCode(HttpStatus.OK)
+ async logout(): Promise<{ message: string }> {
+ return { message: '登出成功' };
+ }
+}
diff --git a/backend/src/auth/auth.module.ts b/backend/src/auth/auth.module.ts
new file mode 100644
index 0000000..c51e8d9
--- /dev/null
+++ b/backend/src/auth/auth.module.ts
@@ -0,0 +1,22 @@
+import { Module } from '@nestjs/common';
+import { JwtModule } from '@nestjs/jwt';
+import { TypeOrmModule } from '@nestjs/typeorm';
+import { PassportModule } from '@nestjs/passport';
+import { AuthService } from './auth.service';
+import { AuthController } from './auth.controller';
+import { AdminUser } from '../entities/admin-user.entity';
+
+@Module({
+ imports: [
+ TypeOrmModule.forFeature([AdminUser]),
+ PassportModule,
+ JwtModule.register({
+ secret: process.env.JWT_SECRET || 'your_jwt_secret_key_change_in_production',
+ signOptions: { expiresIn: process.env.JWT_EXPIRES_IN || '24h' } as any,
+ }),
+ ],
+ controllers: [AuthController],
+ providers: [AuthService],
+ exports: [AuthService],
+})
+export class AuthModule {}
diff --git a/backend/src/auth/auth.service.ts b/backend/src/auth/auth.service.ts
new file mode 100644
index 0000000..51d8c2f
--- /dev/null
+++ b/backend/src/auth/auth.service.ts
@@ -0,0 +1,50 @@
+import { Injectable, UnauthorizedException } from '@nestjs/common';
+import { JwtService } from '@nestjs/jwt';
+import { InjectRepository } from '@nestjs/typeorm';
+import { Repository } from 'typeorm';
+import * as bcrypt from 'bcrypt';
+import { AdminUser } from '../entities/admin-user.entity';
+import { LoginDto } from './dto/login.dto';
+import { LoginResponseDto } from './dto/login-response.dto';
+
+@Injectable()
+export class AuthService {
+ constructor(
+ @InjectRepository(AdminUser)
+ private adminUserRepository: Repository,
+ private jwtService: JwtService,
+ ) {}
+
+ async login(loginDto: LoginDto): Promise {
+ const { username, password } = loginDto;
+
+ const user = await this.adminUserRepository.findOne({
+ where: { username },
+ });
+
+ if (!user) {
+ throw new UnauthorizedException('用户名或密码错误');
+ }
+
+ const isPasswordValid = await bcrypt.compare(password, user.passwordHash);
+
+ if (!isPasswordValid) {
+ throw new UnauthorizedException('用户名或密码错误');
+ }
+
+ const payload = { sub: user.id, username: user.username, role: user.role };
+
+ return {
+ accessToken: this.jwtService.sign(payload),
+ userId: user.id,
+ username: user.username,
+ role: user.role,
+ };
+ }
+
+ async validateUser(userId: number): Promise {
+ return this.adminUserRepository.findOne({
+ where: { id: userId },
+ });
+ }
+}
diff --git a/backend/src/auth/dto/login-response.dto.ts b/backend/src/auth/dto/login-response.dto.ts
new file mode 100644
index 0000000..75d0492
--- /dev/null
+++ b/backend/src/auth/dto/login-response.dto.ts
@@ -0,0 +1,9 @@
+export class LoginResponseDto {
+ accessToken: string;
+
+ userId: number;
+
+ username: string;
+
+ role: string;
+}
diff --git a/backend/src/auth/dto/login.dto.ts b/backend/src/auth/dto/login.dto.ts
new file mode 100644
index 0000000..052e706
--- /dev/null
+++ b/backend/src/auth/dto/login.dto.ts
@@ -0,0 +1,12 @@
+import { IsNotEmpty, IsString, MinLength } from 'class-validator';
+
+export class LoginDto {
+ @IsNotEmpty({ message: '用户名不能为空' })
+ @IsString({ message: '用户名必须是字符串' })
+ username: string;
+
+ @IsNotEmpty({ message: '密码不能为空' })
+ @IsString({ message: '密码必须是字符串' })
+ @MinLength(6, { message: '密码长度至少为6位' })
+ password: string;
+}
diff --git a/backend/src/common/filters/http-exception.filter.ts b/backend/src/common/filters/http-exception.filter.ts
new file mode 100644
index 0000000..f6f572c
--- /dev/null
+++ b/backend/src/common/filters/http-exception.filter.ts
@@ -0,0 +1,31 @@
+import {
+ ExceptionFilter,
+ Catch,
+ ArgumentsHost,
+ HttpException,
+ HttpStatus,
+} from '@nestjs/common';
+import { Request, Response } from 'express';
+
+@Catch(HttpException)
+export class HttpExceptionFilter implements ExceptionFilter {
+ catch(exception: HttpException, host: ArgumentsHost) {
+ const ctx = host.switchToHttp();
+ const response = ctx.getResponse();
+ const request = ctx.getRequest();
+ const status = exception.getStatus();
+ const exceptionResponse = exception.getResponse();
+
+ const errorResponse = {
+ statusCode: status,
+ timestamp: new Date().toISOString(),
+ path: request.url,
+ message:
+ typeof exceptionResponse === 'string'
+ ? exceptionResponse
+ : (exceptionResponse as any).message || exception.message,
+ };
+
+ response.status(status).json(errorResponse);
+ }
+}
diff --git a/backend/src/common/interceptors/transform.interceptor.ts b/backend/src/common/interceptors/transform.interceptor.ts
new file mode 100644
index 0000000..02d592f
--- /dev/null
+++ b/backend/src/common/interceptors/transform.interceptor.ts
@@ -0,0 +1,34 @@
+import {
+ Injectable,
+ NestInterceptor,
+ ExecutionContext,
+ CallHandler,
+} from '@nestjs/common';
+import { Observable } from 'rxjs';
+import { map } from 'rxjs/operators';
+
+export interface Response {
+ statusCode: number;
+ message: string;
+ data: T;
+ timestamp: string;
+}
+
+@Injectable()
+export class TransformInterceptor
+ implements NestInterceptor>
+{
+ intercept(
+ context: ExecutionContext,
+ next: CallHandler,
+ ): Observable> {
+ return next.handle().pipe(
+ map((data) => ({
+ statusCode: context.switchToHttp().getResponse().statusCode,
+ message: 'success',
+ data,
+ timestamp: new Date().toISOString(),
+ })),
+ );
+ }
+}
diff --git a/backend/src/common/pipes/validation.pipe.ts b/backend/src/common/pipes/validation.pipe.ts
new file mode 100644
index 0000000..a27b32d
--- /dev/null
+++ b/backend/src/common/pipes/validation.pipe.ts
@@ -0,0 +1,22 @@
+import { ValidationPipe, ValidationError } from '@nestjs/common';
+import { BadRequestException } from '@nestjs/common';
+
+export class CustomValidationPipe extends ValidationPipe {
+ constructor() {
+ super({
+ whitelist: true,
+ forbidNonWhitelisted: true,
+ transform: true,
+ exceptionFactory: (errors: ValidationError[]) => {
+ const messages = errors.map((error) => {
+ return Object.values(error.constraints || {}).join(', ');
+ });
+ return new BadRequestException({
+ statusCode: 400,
+ message: messages.join('; '),
+ error: 'Bad Request',
+ });
+ },
+ });
+ }
+}
diff --git a/backend/src/config/database.config.ts b/backend/src/config/database.config.ts
new file mode 100644
index 0000000..68a34a4
--- /dev/null
+++ b/backend/src/config/database.config.ts
@@ -0,0 +1,22 @@
+import { DataSource, DataSourceOptions } from 'typeorm';
+import { config } from 'dotenv';
+
+config();
+
+export const dataSourceOptions: DataSourceOptions = {
+ type: 'mysql',
+ host: process.env.DB_HOST || 'localhost',
+ port: parseInt(process.env.DB_PORT || '3306', 10),
+ username: process.env.DB_USERNAME || 'root',
+ password: process.env.DB_PASSWORD || '',
+ database: process.env.DB_DATABASE || 'jge_rs_sl_web',
+ entities: ['dist/**/*.entity{.ts,.js}'],
+ migrations: ['dist/migrations/*{.ts,.js}'],
+ synchronize: process.env.NODE_ENV === 'development',
+ logging: process.env.NODE_ENV === 'development',
+ charset: 'utf8mb4',
+};
+
+const dataSource = new DataSource(dataSourceOptions);
+
+export default dataSource;
diff --git a/backend/src/entities/admin-user.entity.ts b/backend/src/entities/admin-user.entity.ts
new file mode 100644
index 0000000..2c28387
--- /dev/null
+++ b/backend/src/entities/admin-user.entity.ts
@@ -0,0 +1,34 @@
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, Index } from 'typeorm';
+
+export enum AdminRole {
+ SUPER_ADMIN = 'super_admin',
+ OPERATOR = 'operator',
+ VIEWER = 'viewer',
+}
+
+@Entity('admin_users')
+export class AdminUser {
+ @PrimaryGeneratedColumn({ type: 'bigint', comment: '用户ID' })
+ id: number;
+
+ @Column({ type: 'varchar', length: 50, unique: true, comment: '用户名' })
+ @Index()
+ username: string;
+
+ @Column({ type: 'char', length: 60, comment: '密码哈希(bcrypt)' })
+ passwordHash: string;
+
+ @Column({
+ type: 'enum',
+ enum: AdminRole,
+ default: AdminRole.VIEWER,
+ comment: '角色:super_admin-超级管理员, operator-操作员, viewer-查看者',
+ })
+ role: AdminRole;
+
+ @CreateDateColumn({ type: 'datetime', comment: '创建时间' })
+ createdAt: Date;
+
+ @UpdateDateColumn({ type: 'datetime', comment: '更新时间' })
+ updatedAt: Date;
+}
diff --git a/backend/src/main.ts b/backend/src/main.ts
new file mode 100644
index 0000000..80cf487
--- /dev/null
+++ b/backend/src/main.ts
@@ -0,0 +1,22 @@
+import { NestFactory } from '@nestjs/core';
+import { ValidationPipe } from '@nestjs/common';
+import { AppModule } from './app.module';
+import { HttpExceptionFilter } from './common/filters/http-exception.filter';
+import { TransformInterceptor } from './common/interceptors/transform.interceptor';
+import { CustomValidationPipe } from './common/pipes/validation.pipe';
+
+async function bootstrap() {
+ const app = await NestFactory.create(AppModule);
+
+ app.enableCors({
+ origin: true,
+ credentials: true,
+ });
+
+ app.useGlobalPipes(new CustomValidationPipe());
+ app.useGlobalFilters(new HttpExceptionFilter());
+ app.useGlobalInterceptors(new TransformInterceptor());
+
+ await app.listen(process.env.PORT ?? 3000);
+}
+bootstrap();
diff --git a/backend/src/player/player.controller.ts b/backend/src/player/player.controller.ts
new file mode 100644
index 0000000..d6f580c
--- /dev/null
+++ b/backend/src/player/player.controller.ts
@@ -0,0 +1,21 @@
+import { Controller, Post, Body, HttpCode, HttpStatus, Headers } from '@nestjs/common';
+import { PlayerService } from './player.service';
+import { LoginDto } from '../auth/dto/login.dto';
+
+@Controller('player/auth')
+export class PlayerController {
+ constructor(private playerService: PlayerService) {}
+
+ @Post('login')
+ @HttpCode(HttpStatus.OK)
+ async login(@Body() loginDto: LoginDto): Promise {
+ return this.playerService.login(loginDto);
+ }
+
+ @Post('logout')
+ @HttpCode(HttpStatus.OK)
+ async logout(@Headers('authorization') authHeader: string): Promise {
+ const token = authHeader?.replace('Bearer ', '');
+ return this.playerService.logout(token);
+ }
+}
diff --git a/backend/src/player/player.module.ts b/backend/src/player/player.module.ts
new file mode 100644
index 0000000..9dc18f8
--- /dev/null
+++ b/backend/src/player/player.module.ts
@@ -0,0 +1,15 @@
+import { Module } from '@nestjs/common';
+import { HttpModule } from '@nestjs/axios';
+import { PlayerController } from './player.controller';
+import { PlayerService } from './player.service';
+
+@Module({
+ imports: [
+ HttpModule.register({
+ proxy: false,
+ }),
+ ],
+ controllers: [PlayerController],
+ providers: [PlayerService],
+})
+export class PlayerModule {}
diff --git a/backend/src/player/player.service.ts b/backend/src/player/player.service.ts
new file mode 100644
index 0000000..b4cc452
--- /dev/null
+++ b/backend/src/player/player.service.ts
@@ -0,0 +1,61 @@
+import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
+import { HttpService } from '@nestjs/axios';
+import { firstValueFrom } from 'rxjs';
+import { LoginDto } from '../auth/dto/login.dto';
+
+@Injectable()
+export class PlayerService {
+ private readonly officialApiBaseUrl = process.env.GAME_API_BASE_URL || 'https://api.example.com';
+
+ constructor(private readonly httpService: HttpService) {}
+
+ async login(loginDto: LoginDto): Promise {
+ try {
+ const response = await firstValueFrom(
+ this.httpService.post(`${this.officialApiBaseUrl}?code=auth/login`, loginDto),
+ );
+ const data = (response as any).data;
+
+ if (!data.success) {
+ throw new HttpException(
+ data.message || '登录失败',
+ HttpStatus.UNAUTHORIZED,
+ );
+ }
+
+ return {
+ accessToken: data.data.token,
+ userId: data.data.userId,
+ username: data.data.username,
+ role: 'player',
+ };
+ } catch (error) {
+ console.error('Player login error:', error);
+ if (error instanceof HttpException) {
+ throw error;
+ }
+ throw new HttpException(
+ (error as any).response?.data?.message || (error as any).response?.data || '登录失败',
+ (error as any).response?.status || HttpStatus.INTERNAL_SERVER_ERROR,
+ );
+ }
+ }
+
+ async logout(token: string): Promise {
+ try {
+ const response = await firstValueFrom(
+ this.httpService.post(`${this.officialApiBaseUrl}?code=auth/out_login`, {}, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ }),
+ );
+ return (response as any).data;
+ } catch (error) {
+ throw new HttpException(
+ (error as any).response?.data || '登出失败',
+ (error as any).response?.status || HttpStatus.INTERNAL_SERVER_ERROR,
+ );
+ }
+ }
+}
diff --git a/backend/test/jest-e2e.json b/backend/test/jest-e2e.json
new file mode 100644
index 0000000..bb66802
--- /dev/null
+++ b/backend/test/jest-e2e.json
@@ -0,0 +1,9 @@
+{
+ "moduleFileExtensions": ["js", "json", "ts"],
+ "rootDir": ".",
+ "testEnvironment": "node",
+ "testRegex": ".e2e-spec.ts$",
+ "transform": {
+ "^.+\\.(t|j)s$": "ts-jest"
+ }
+}
diff --git a/backend/tsconfig.build.json b/backend/tsconfig.build.json
new file mode 100644
index 0000000..1d7acd8
--- /dev/null
+++ b/backend/tsconfig.build.json
@@ -0,0 +1,4 @@
+{
+ "extends": "./tsconfig.json",
+ "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
+}
diff --git a/backend/tsconfig.json b/backend/tsconfig.json
new file mode 100644
index 0000000..89eb88f
--- /dev/null
+++ b/backend/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "module": "nodenext",
+ "moduleResolution": "nodenext",
+ "resolvePackageJsonExports": true,
+ "esModuleInterop": true,
+ "isolatedModules": true,
+ "declaration": true,
+ "removeComments": true,
+ "emitDecoratorMetadata": true,
+ "experimentalDecorators": true,
+ "allowSyntheticDefaultImports": true,
+ "target": "ES2023",
+ "sourceMap": true,
+ "outDir": "./dist",
+ "baseUrl": "./",
+ "incremental": true,
+ "skipLibCheck": true,
+ "strictNullChecks": true,
+ "forceConsistentCasingInFileNames": true,
+ "noImplicitAny": false,
+ "strictBindCallApply": false,
+ "noFallthroughCasesInSwitch": false
+ }
+}
diff --git a/frontend/.gitignore b/frontend/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/frontend/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/frontend/README.md b/frontend/README.md
new file mode 100644
index 0000000..d2e7761
--- /dev/null
+++ b/frontend/README.md
@@ -0,0 +1,73 @@
+# 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...
+ },
+ },
+])
+```
diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js
new file mode 100644
index 0000000..5e6b472
--- /dev/null
+++ b/frontend/eslint.config.js
@@ -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,
+ },
+ },
+])
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..072a57e
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ frontend
+
+
+
+
+
+
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..4a717bb
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,34 @@
+{
+ "name": "frontend",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b && vite build",
+ "lint": "eslint .",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "antd": "^6.1.2",
+ "axios": "^1.13.2",
+ "react": "^19.2.0",
+ "react-dom": "^19.2.0",
+ "react-router-dom": "^7.11.0",
+ "zustand": "^5.0.9"
+ },
+ "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",
+ "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"
+ }
+}
diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg
new file mode 100644
index 0000000..e7b8dfb
--- /dev/null
+++ b/frontend/public/vite.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/src/App.css b/frontend/src/App.css
new file mode 100644
index 0000000..cf67b17
--- /dev/null
+++ b/frontend/src/App.css
@@ -0,0 +1,42 @@
+#root {
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ padding: 0;
+}
+
+.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;
+}
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
new file mode 100644
index 0000000..3d7ded3
--- /dev/null
+++ b/frontend/src/App.tsx
@@ -0,0 +1,35 @@
+import { useState } from 'react'
+import reactLogo from './assets/react.svg'
+import viteLogo from '/vite.svg'
+import './App.css'
+
+function App() {
+ const [count, setCount] = useState(0)
+
+ return (
+ <>
+
+ Vite + React
+
+
+
+ Edit src/App.tsx and save to test HMR
+
+
+
+ Click on the Vite and React logos to learn more
+
+ >
+ )
+}
+
+export default App
diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg
new file mode 100644
index 0000000..6c87de9
--- /dev/null
+++ b/frontend/src/assets/react.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/src/components/AdminAuthRoute.tsx b/frontend/src/components/AdminAuthRoute.tsx
new file mode 100644
index 0000000..21d4b8e
--- /dev/null
+++ b/frontend/src/components/AdminAuthRoute.tsx
@@ -0,0 +1,14 @@
+import { Navigate, Outlet } from 'react-router-dom';
+import { useAuthStore } from '../stores/authStore';
+
+const AdminAuthRoute = () => {
+ const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
+
+ if (!isAuthenticated) {
+ return ;
+ }
+
+ return ;
+};
+
+export default AdminAuthRoute;
diff --git a/frontend/src/components/AdminLayout.tsx b/frontend/src/components/AdminLayout.tsx
new file mode 100644
index 0000000..28ae72c
--- /dev/null
+++ b/frontend/src/components/AdminLayout.tsx
@@ -0,0 +1,135 @@
+import { useState } from 'react';
+import { Layout, Menu, Button, theme } from 'antd';
+import {
+ MenuFoldOutlined,
+ MenuUnfoldOutlined,
+ DashboardOutlined,
+ UserOutlined,
+ LogoutOutlined,
+} from '@ant-design/icons';
+import { Outlet, useNavigate, useLocation } from 'react-router-dom';
+import { useAuthStore } from '../stores/authStore';
+import { adminAuthService } from '../services/adminAuthService';
+import { message } from 'antd';
+
+const { Header, Sider, Content, Footer } = Layout;
+
+const AdminLayout = () => {
+ const [collapsed, setCollapsed] = useState(false);
+ const {
+ token: { colorBgContainer, borderRadiusLG },
+ } = theme.useToken();
+ const navigate = useNavigate();
+ const location = useLocation();
+ const adminUser = useAuthStore((state) => state.adminUser);
+ const logout = useAuthStore((state) => state.logout);
+
+ const handleLogout = async () => {
+ try {
+ await adminAuthService.logout();
+ localStorage.removeItem('adminToken');
+ logout();
+ message.success('登出成功');
+ navigate('/admin/login');
+ } catch (error) {
+ message.error('登出失败');
+ }
+ };
+
+ const menuItems = [
+ {
+ key: '/admin/dashboard',
+ icon: ,
+ label: '工作台',
+ },
+ {
+ key: 'user',
+ icon: ,
+ label: '用户管理',
+ children: [
+ {
+ key: '/admin/users',
+ label: '用户列表',
+ },
+ ],
+ },
+ ];
+
+ return (
+
+
+
+ {collapsed ? '运营' : '运营管理系统'}
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default AdminLayout;
diff --git a/frontend/src/components/PlayerAuthRoute.tsx b/frontend/src/components/PlayerAuthRoute.tsx
new file mode 100644
index 0000000..826b99b
--- /dev/null
+++ b/frontend/src/components/PlayerAuthRoute.tsx
@@ -0,0 +1,14 @@
+import { Navigate, Outlet } from 'react-router-dom';
+import { useAuthStore } from '../stores/authStore';
+
+const PlayerAuthRoute = () => {
+ const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
+
+ if (!isAuthenticated) {
+ return ;
+ }
+
+ return ;
+};
+
+export default PlayerAuthRoute;
diff --git a/frontend/src/components/PlayerLayout.tsx b/frontend/src/components/PlayerLayout.tsx
new file mode 100644
index 0000000..e157099
--- /dev/null
+++ b/frontend/src/components/PlayerLayout.tsx
@@ -0,0 +1,116 @@
+import { Layout, Menu, Button, Dropdown } from 'antd';
+import {
+ HomeOutlined,
+ UserOutlined,
+ LogoutOutlined,
+ DownOutlined,
+} from '@ant-design/icons';
+import { Outlet, useNavigate, useLocation } from 'react-router-dom';
+import { message } from 'antd';
+import { playerAuthService } from '../services/playerAuthService';
+
+const { Header, Content, Footer } = Layout;
+
+const PlayerLayout = () => {
+ const navigate = useNavigate();
+ const location = useLocation();
+
+ const handleLogout = async () => {
+ try {
+ await playerAuthService.logout();
+ localStorage.removeItem('playerToken');
+ message.success('登出成功');
+ navigate('/player/login');
+ } catch (error) {
+ message.error('登出失败');
+ }
+ };
+
+ const menuItems = [
+ {
+ key: '/player/dashboard',
+ icon: ,
+ label: '首页',
+ },
+ {
+ key: '/player/profile',
+ icon: ,
+ label: '个人中心',
+ },
+ ];
+
+ const userMenuItems = [
+ {
+ key: 'logout',
+ icon: ,
+ label: '退出登录',
+ onClick: handleLogout,
+ },
+ ];
+
+ return (
+
+
+
+ 梦幻西游玩家服务中心
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default PlayerLayout;
diff --git a/frontend/src/index.css b/frontend/src/index.css
new file mode 100644
index 0000000..020a94d
--- /dev/null
+++ b/frontend/src/index.css
@@ -0,0 +1,68 @@
+: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;
+}
+
+html, body {
+ margin: 0;
+ padding: 0;
+ width: 100%;
+ height: 100%;
+ overflow-x: hidden;
+}
+
+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;
+ }
+}
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
new file mode 100644
index 0000000..c7a3572
--- /dev/null
+++ b/frontend/src/main.tsx
@@ -0,0 +1,15 @@
+import { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import { RouterProvider } from 'react-router-dom';
+import { ConfigProvider } from 'antd';
+import zhCN from 'antd/locale/zh_CN';
+import './index.css';
+import router from './router';
+
+createRoot(document.getElementById('root')!).render(
+
+
+
+
+ ,
+);
diff --git a/frontend/src/pages/admin/AdminDashboard.tsx b/frontend/src/pages/admin/AdminDashboard.tsx
new file mode 100644
index 0000000..6fe69ca
--- /dev/null
+++ b/frontend/src/pages/admin/AdminDashboard.tsx
@@ -0,0 +1,64 @@
+import { Card, Row, Col, Statistic } from 'antd';
+import {
+ UserOutlined,
+ ShoppingOutlined,
+ DollarOutlined,
+ TrophyOutlined,
+} from '@ant-design/icons';
+
+const AdminDashboard = () => {
+ return (
+
+
工作台
+
+
+
+ }
+ valueStyle={{ color: '#3f8600' }}
+ />
+
+
+
+
+ }
+ valueStyle={{ color: '#cf1322' }}
+ />
+
+
+
+
+ }
+ precision={2}
+ valueStyle={{ color: '#1890ff' }}
+ />
+
+
+
+
+ }
+ valueStyle={{ color: '#722ed1' }}
+ />
+
+
+
+
+ 系统运行正常,所有服务均在线。
+ 最后更新时间:2025-12-27 12:00:00
+
+
+ );
+};
+
+export default AdminDashboard;
diff --git a/frontend/src/pages/admin/AdminLogin.tsx b/frontend/src/pages/admin/AdminLogin.tsx
new file mode 100644
index 0000000..02eae1e
--- /dev/null
+++ b/frontend/src/pages/admin/AdminLogin.tsx
@@ -0,0 +1,95 @@
+import { useState } from 'react';
+import { Form, Input, Button, Card, message } from 'antd';
+import { UserOutlined, LockOutlined } from '@ant-design/icons';
+import { useNavigate } from 'react-router-dom';
+import { adminAuthService, type LoginRequest } from '../../services/adminAuthService';
+import { useAuthStore } from '../../stores/authStore';
+
+const AdminLogin = () => {
+ const [loading, setLoading] = useState(false);
+ const navigate = useNavigate();
+ const setAuth = useAuthStore((state) => state.setAuth);
+
+ const onFinish = async (values: LoginRequest) => {
+ setLoading(true);
+ try {
+ const response = await adminAuthService.login(values);
+ setAuth(
+ {
+ id: response.userId,
+ username: response.username,
+ role: response.role,
+ },
+ response.accessToken
+ );
+ localStorage.setItem('adminToken', response.accessToken);
+ message.success('登录成功');
+ navigate('/admin/dashboard');
+ } catch (error) {
+ message.error(error instanceof Error ? error.message : '登录失败');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+ }
+ placeholder="用户名"
+ />
+
+
+
+ }
+ placeholder="密码"
+ />
+
+
+
+
+
+
+
+
+ );
+};
+
+export default AdminLogin;
diff --git a/frontend/src/pages/player/PlayerDashboard.tsx b/frontend/src/pages/player/PlayerDashboard.tsx
new file mode 100644
index 0000000..79a528e
--- /dev/null
+++ b/frontend/src/pages/player/PlayerDashboard.tsx
@@ -0,0 +1,122 @@
+import { Card, Row, Col, Statistic, List, Avatar, Button } from 'antd';
+import {
+ TrophyOutlined,
+ GiftOutlined,
+ ClockCircleOutlined,
+ StarOutlined,
+} from '@ant-design/icons';
+
+const PlayerDashboard = () => {
+ const notices = [
+ {
+ title: '系统维护通知',
+ description: '系统将于2025-12-28 02:00-04:00进行维护,请提前做好准备。',
+ time: '2025-12-27 10:00',
+ },
+ {
+ title: '新年活动开启',
+ description: '新年活动已正式开启,参与活动可获得丰厚奖励!',
+ time: '2025-12-26 18:00',
+ },
+ {
+ title: '版本更新公告',
+ description: 'v2.5.0版本已更新,新增多项功能优化。',
+ time: '2025-12-25 09:00',
+ },
+ ];
+
+ return (
+
+
欢迎来到玩家服务中心
+
+
+
+ }
+ valueStyle={{ color: '#3f8600' }}
+ />
+
+
+
+
+ }
+ precision={2}
+ valueStyle={{ color: '#cf1322' }}
+ />
+
+
+
+
+ }
+ valueStyle={{ color: '#1890ff' }}
+ />
+
+
+
+
+ }
+ valueStyle={{ color: '#722ed1' }}
+ />
+
+
+
+
+
+
+ (
+
+ } />}
+ title={item.title}
+ description={
+
+
{item.description}
+
{item.time}
+
+ }
+ />
+
+ )}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default PlayerDashboard;
diff --git a/frontend/src/pages/player/PlayerLogin.tsx b/frontend/src/pages/player/PlayerLogin.tsx
new file mode 100644
index 0000000..2811bcb
--- /dev/null
+++ b/frontend/src/pages/player/PlayerLogin.tsx
@@ -0,0 +1,92 @@
+import { useState } from 'react';
+import { Form, Input, Button, Card, message } from 'antd';
+import { UserOutlined, LockOutlined } from '@ant-design/icons';
+import { useNavigate } from 'react-router-dom';
+import { playerAuthService, type LoginRequest } from '../../services/playerAuthService';
+import { useAuthStore } from '../../stores/authStore';
+
+const PlayerLogin = () => {
+ const [loading, setLoading] = useState(false);
+ const navigate = useNavigate();
+ const setAuth = useAuthStore((state) => state.setAuth);
+
+ const onFinish = async (values: LoginRequest) => {
+ setLoading(true);
+ try {
+ const response = await playerAuthService.login(values);
+ localStorage.setItem('playerToken', response.accessToken);
+ setAuth({
+ id: response.userId,
+ username: response.username,
+ role: response.role,
+ }, response.accessToken);
+ message.success('登录成功');
+ navigate('/player/dashboard');
+ } catch (error) {
+ message.error(error instanceof Error ? error.message : '登录失败');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+ }
+ placeholder="用户名"
+ />
+
+
+
+ }
+ placeholder="密码"
+ />
+
+
+
+
+
+
+
+
+ );
+};
+
+export default PlayerLogin;
diff --git a/frontend/src/router/index.tsx b/frontend/src/router/index.tsx
new file mode 100644
index 0000000..aa8defe
--- /dev/null
+++ b/frontend/src/router/index.tsx
@@ -0,0 +1,72 @@
+import { createBrowserRouter, Navigate } from 'react-router-dom';
+import AdminLayout from '../components/AdminLayout';
+import PlayerLayout from '../components/PlayerLayout';
+import AdminAuthRoute from '../components/AdminAuthRoute';
+import PlayerAuthRoute from '../components/PlayerAuthRoute';
+import AdminLogin from '../pages/admin/AdminLogin';
+import AdminDashboard from '../pages/admin/AdminDashboard';
+import PlayerLogin from '../pages/player/PlayerLogin';
+import PlayerDashboard from '../pages/player/PlayerDashboard';
+
+const router = createBrowserRouter([
+ {
+ path: '/',
+ element: ,
+ },
+ {
+ path: '/admin',
+ children: [
+ {
+ path: 'login',
+ element: ,
+ },
+ {
+ element: ,
+ children: [
+ {
+ element: ,
+ children: [
+ {
+ path: 'dashboard',
+ element: ,
+ },
+ {
+ index: true,
+ element: ,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ {
+ path: '/player',
+ children: [
+ {
+ path: 'login',
+ element: ,
+ },
+ {
+ element: ,
+ children: [
+ {
+ element: ,
+ children: [
+ {
+ path: 'dashboard',
+ element: ,
+ },
+ {
+ index: true,
+ element: ,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+]);
+
+export default router;
diff --git a/frontend/src/services/adminAuthService.ts b/frontend/src/services/adminAuthService.ts
new file mode 100644
index 0000000..7e6e01c
--- /dev/null
+++ b/frontend/src/services/adminAuthService.ts
@@ -0,0 +1,25 @@
+import api from '../utils/api';
+
+export interface LoginRequest {
+ username: string;
+ password: string;
+}
+
+export interface LoginResponse {
+ accessToken: string;
+ userId: number;
+ username: string;
+ role: string;
+}
+
+export const adminAuthService = {
+ async login(data: LoginRequest): Promise {
+ const response = await api.post('/admin/auth/login', data);
+ return response.data;
+ },
+
+ async logout(): Promise<{ message: string }> {
+ const response = await api.post<{ message: string }>('/admin/auth/logout');
+ return response.data;
+ },
+};
diff --git a/frontend/src/services/playerAuthService.ts b/frontend/src/services/playerAuthService.ts
new file mode 100644
index 0000000..be7d5ef
--- /dev/null
+++ b/frontend/src/services/playerAuthService.ts
@@ -0,0 +1,25 @@
+import api from '../utils/api';
+
+export interface LoginRequest {
+ username: string;
+ password: string;
+}
+
+export interface LoginResponse {
+ accessToken: string;
+ userId: number;
+ username: string;
+ role: string;
+}
+
+export const playerAuthService = {
+ async login(data: LoginRequest): Promise {
+ const response = await api.post('/player/auth/login', data);
+ return response.data;
+ },
+
+ async logout(): Promise<{ message: string }> {
+ const response = await api.post<{ message: string }>('/player/auth/logout');
+ return response.data;
+ },
+};
diff --git a/frontend/src/stores/authStore.ts b/frontend/src/stores/authStore.ts
new file mode 100644
index 0000000..cf946ca
--- /dev/null
+++ b/frontend/src/stores/authStore.ts
@@ -0,0 +1,33 @@
+import { create } from 'zustand';
+
+interface AdminUser {
+ id: number;
+ username: string;
+ role: string;
+}
+
+interface AuthState {
+ isAuthenticated: boolean;
+ adminUser: AdminUser | null;
+ token: string | null;
+ setAuth: (user: AdminUser, token: string) => void;
+ logout: () => void;
+}
+
+export const useAuthStore = create((set) => ({
+ isAuthenticated: false,
+ adminUser: null,
+ token: null,
+ setAuth: (user, token) =>
+ set({
+ isAuthenticated: true,
+ adminUser: user,
+ token,
+ }),
+ logout: () =>
+ set({
+ isAuthenticated: false,
+ adminUser: null,
+ token: null,
+ }),
+}));
diff --git a/frontend/src/utils/api.ts b/frontend/src/utils/api.ts
new file mode 100644
index 0000000..55ca59b
--- /dev/null
+++ b/frontend/src/utils/api.ts
@@ -0,0 +1,49 @@
+import axios from 'axios';
+
+const api = axios.create({
+ baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000',
+ timeout: 10000,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+});
+
+api.interceptors.request.use(
+ (config) => {
+ const adminToken = localStorage.getItem('adminToken');
+ const playerToken = localStorage.getItem('playerToken');
+ const token = adminToken || playerToken;
+ if (token) {
+ config.headers.Authorization = `Bearer ${token}`;
+ }
+ return config;
+ },
+ (error) => {
+ return Promise.reject(error);
+ }
+);
+
+api.interceptors.response.use(
+ (response) => {
+ return response.data;
+ },
+ (error) => {
+ console.error('API Error:', error);
+ let message = '请求失败';
+
+ if (error.response) {
+ const data = error.response.data;
+ if (data) {
+ message = data.message || data.error || message;
+ }
+ } else if (error.request) {
+ message = '网络连接失败,请检查后端服务是否启动';
+ } else {
+ message = error.message || message;
+ }
+
+ return Promise.reject(new Error(message));
+ }
+);
+
+export default api;
diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json
new file mode 100644
index 0000000..a9b5a59
--- /dev/null
+++ b/frontend/tsconfig.app.json
@@ -0,0 +1,28 @@
+{
+ "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,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["src"]
+}
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644
index 0000000..1ffef60
--- /dev/null
+++ b/frontend/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json
new file mode 100644
index 0000000..8a67f62
--- /dev/null
+++ b/frontend/tsconfig.node.json
@@ -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"]
+}
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
new file mode 100644
index 0000000..23a33e4
--- /dev/null
+++ b/frontend/vite.config.ts
@@ -0,0 +1,15 @@
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
+
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ proxy: {
+ '/api': {
+ target: 'http://localhost:3000',
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/api/, ''),
+ },
+ },
+ },
+});
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..43750b8
--- /dev/null
+++ b/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "jge-rs-sl-web",
+ "version": "1.0.0",
+ "description": "梦幻西游一站式运营管理平台",
+ "scripts": {
+ "cz": "git-cz",
+ "commit": "git-cz"
+ },
+ "config": {
+ "commitizen": {
+ "path": "node_modules/cz-git",
+ "useConfig": "./.cz-config.js"
+ }
+ },
+ "devDependencies": {
+ "commitizen": "^4.3.1",
+ "cz-git": "^1.11.0"
+ }
+}