From 2d8566132ef594f6227406881f1f63a3e3009b6d Mon Sep 17 00:00:00 2001 From: Stev_Wang <304865932@qq.com> Date: Sat, 27 Dec 2025 20:17:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E2=9C=A8=20=E5=89=8D=E7=AB=AF=EF=BC=9A?= =?UTF-8?q?=E7=8E=A9=E5=AE=B6=E6=9C=8D=E5=8A=A1=E5=B9=B3=E5=8F=B0=E5=92=8C?= =?UTF-8?q?=E8=BF=90=E8=90=A5=E7=AE=A1=E7=90=86=E7=B3=BB=E7=BB=9F=E5=90=8E?= =?UTF-8?q?=E5=8F=B0=E5=88=9D=E5=A7=8B=E5=8C=96=E5=8F=8A=E6=A1=86=E6=9E=B6?= =?UTF-8?q?=E6=90=AD=E5=BB=BA=EF=BC=8C=E5=90=8E=E7=AB=AF=EF=BC=9A=E5=AE=8C?= =?UTF-8?q?=E6=88=90=E5=9F=BA=E7=A1=80=E5=8A=9F=E8=83=BD=E6=90=AD=E5=BB=BA?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .cz-config.js | 107 ++++++++++++++ .czrc | 75 ++++++++++ README.md | 130 +++++++++++++++++ arco-design-pro/package.json | 5 - backend/.prettierrc | 4 + backend/README.md | 98 +++++++++++++ backend/database/init.sql | 30 ++++ backend/database/update-admin-password.sql | 10 ++ backend/eslint.config.mjs | 35 +++++ backend/nest-cli.json | 8 ++ backend/package.json | 86 +++++++++++ backend/src/app.controller.ts | 12 ++ backend/src/app.module.ts | 33 +++++ backend/src/app.service.ts | 8 ++ backend/src/auth/auth.controller.ts | 21 +++ backend/src/auth/auth.module.ts | 22 +++ backend/src/auth/auth.service.ts | 50 +++++++ backend/src/auth/dto/login-response.dto.ts | 9 ++ backend/src/auth/dto/login.dto.ts | 12 ++ .../common/filters/http-exception.filter.ts | 31 ++++ .../interceptors/transform.interceptor.ts | 34 +++++ backend/src/common/pipes/validation.pipe.ts | 22 +++ backend/src/config/database.config.ts | 22 +++ backend/src/entities/admin-user.entity.ts | 34 +++++ backend/src/main.ts | 22 +++ backend/src/player/player.controller.ts | 21 +++ backend/src/player/player.module.ts | 15 ++ backend/src/player/player.service.ts | 61 ++++++++ backend/test/jest-e2e.json | 9 ++ backend/tsconfig.build.json | 4 + backend/tsconfig.json | 25 ++++ frontend/.gitignore | 24 ++++ frontend/README.md | 73 ++++++++++ frontend/eslint.config.js | 23 +++ frontend/index.html | 13 ++ frontend/package.json | 34 +++++ frontend/public/vite.svg | 1 + frontend/src/App.css | 42 ++++++ frontend/src/App.tsx | 35 +++++ frontend/src/assets/react.svg | 1 + frontend/src/components/AdminAuthRoute.tsx | 14 ++ frontend/src/components/AdminLayout.tsx | 135 ++++++++++++++++++ frontend/src/components/PlayerAuthRoute.tsx | 14 ++ frontend/src/components/PlayerLayout.tsx | 116 +++++++++++++++ frontend/src/index.css | 68 +++++++++ frontend/src/main.tsx | 15 ++ frontend/src/pages/admin/AdminDashboard.tsx | 64 +++++++++ frontend/src/pages/admin/AdminLogin.tsx | 95 ++++++++++++ frontend/src/pages/player/PlayerDashboard.tsx | 122 ++++++++++++++++ frontend/src/pages/player/PlayerLogin.tsx | 92 ++++++++++++ frontend/src/router/index.tsx | 72 ++++++++++ frontend/src/services/adminAuthService.ts | 25 ++++ frontend/src/services/playerAuthService.ts | 25 ++++ frontend/src/stores/authStore.ts | 33 +++++ frontend/src/utils/api.ts | 49 +++++++ frontend/tsconfig.app.json | 28 ++++ frontend/tsconfig.json | 7 + frontend/tsconfig.node.json | 26 ++++ frontend/vite.config.ts | 15 ++ package.json | 19 +++ 60 files changed, 2330 insertions(+), 5 deletions(-) create mode 100644 .cz-config.js create mode 100644 .czrc create mode 100644 README.md delete mode 100644 arco-design-pro/package.json create mode 100644 backend/.prettierrc create mode 100644 backend/README.md create mode 100644 backend/database/init.sql create mode 100644 backend/database/update-admin-password.sql create mode 100644 backend/eslint.config.mjs create mode 100644 backend/nest-cli.json create mode 100644 backend/package.json create mode 100644 backend/src/app.controller.ts create mode 100644 backend/src/app.module.ts create mode 100644 backend/src/app.service.ts create mode 100644 backend/src/auth/auth.controller.ts create mode 100644 backend/src/auth/auth.module.ts create mode 100644 backend/src/auth/auth.service.ts create mode 100644 backend/src/auth/dto/login-response.dto.ts create mode 100644 backend/src/auth/dto/login.dto.ts create mode 100644 backend/src/common/filters/http-exception.filter.ts create mode 100644 backend/src/common/interceptors/transform.interceptor.ts create mode 100644 backend/src/common/pipes/validation.pipe.ts create mode 100644 backend/src/config/database.config.ts create mode 100644 backend/src/entities/admin-user.entity.ts create mode 100644 backend/src/main.ts create mode 100644 backend/src/player/player.controller.ts create mode 100644 backend/src/player/player.module.ts create mode 100644 backend/src/player/player.service.ts create mode 100644 backend/test/jest-e2e.json create mode 100644 backend/tsconfig.build.json create mode 100644 backend/tsconfig.json create mode 100644 frontend/.gitignore create mode 100644 frontend/README.md create mode 100644 frontend/eslint.config.js create mode 100644 frontend/index.html create mode 100644 frontend/package.json create mode 100644 frontend/public/vite.svg create mode 100644 frontend/src/App.css create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/assets/react.svg create mode 100644 frontend/src/components/AdminAuthRoute.tsx create mode 100644 frontend/src/components/AdminLayout.tsx create mode 100644 frontend/src/components/PlayerAuthRoute.tsx create mode 100644 frontend/src/components/PlayerLayout.tsx create mode 100644 frontend/src/index.css create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/pages/admin/AdminDashboard.tsx create mode 100644 frontend/src/pages/admin/AdminLogin.tsx create mode 100644 frontend/src/pages/player/PlayerDashboard.tsx create mode 100644 frontend/src/pages/player/PlayerLogin.tsx create mode 100644 frontend/src/router/index.tsx create mode 100644 frontend/src/services/adminAuthService.ts create mode 100644 frontend/src/services/playerAuthService.ts create mode 100644 frontend/src/stores/authStore.ts create mode 100644 frontend/src/utils/api.ts create mode 100644 frontend/tsconfig.app.json create mode 100644 frontend/tsconfig.json create mode 100644 frontend/tsconfig.node.json create mode 100644 frontend/vite.config.ts create mode 100644 package.json 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 @@ +

+ Nest Logo +

+ +[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.

+

+NPM Version +Package License +NPM Downloads +CircleCI +Discord +Backers on Open Collective +Sponsors on Open Collective + Donate us + Support us + Follow us on Twitter +

+ + +## 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 ? '运营' : '运营管理系统'} +
+ navigate(key)} + /> + + +
+ + +
+ + + +
+ 梦幻西游一站式运营管理平台 ©2025 Created by JGE +
+
+ + ); +}; + +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 ( + +
+
+ 梦幻西游玩家服务中心 +
+ navigate(key)} + /> + + + +
+ +
+ +
+
+
+ 梦幻西游一站式运营管理平台 ©2025 Created by JGE +
+
+ ); +}; + +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" + } +}