项目初始化
This commit is contained in:
3
.commitlintrc.js
Normal file
3
.commitlintrc.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: ['@commitlint/config-conventional']
|
||||||
|
};
|
||||||
107
.cz-config.js
Normal file
107
.cz-config.js
Normal file
@@ -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: ""
|
||||||
|
};
|
||||||
75
.czrc
Normal file
75
.czrc
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://cdn.jsdelivr.net/npm/cz-git@1.12.0/schema/cz-git.json",
|
||||||
|
"path": "node_modules/cz-git",
|
||||||
|
"messages": {
|
||||||
|
"type": "选择你要提交的类型 :",
|
||||||
|
"scope": "选择一个提交范围(可选):",
|
||||||
|
"customScope": "请输入自定义的提交范围 :",
|
||||||
|
"subject": "填写简短精炼的变更描述 :",
|
||||||
|
"body": "填写详细描述(可选)。使用 '|' 换行 :",
|
||||||
|
"breaking": "列出任何突破性变更(可选)。使用 '|' 换行 :",
|
||||||
|
"footerPrefixesSelect": "选择关联issue前缀(可选):",
|
||||||
|
"customFooterPrefix": "输入自定义issue前缀 :",
|
||||||
|
"footer": "填写关联issue (可选) 例如: #123, #456 :",
|
||||||
|
"confirmCommit": "确认提交?"
|
||||||
|
},
|
||||||
|
"types": [
|
||||||
|
{
|
||||||
|
"value": "feat",
|
||||||
|
"name": "feat: 新增功能",
|
||||||
|
"emoji": "✨"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "fix",
|
||||||
|
"name": "fix: 修复缺陷",
|
||||||
|
"emoji": "🐛"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "docs",
|
||||||
|
"name": "docs: 文档更新",
|
||||||
|
"emoji": "📝"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "style",
|
||||||
|
"name": "style: 代码格式",
|
||||||
|
"emoji": "💄"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "refactor",
|
||||||
|
"name": "refactor: 代码重构",
|
||||||
|
"emoji": "♻️"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "perf",
|
||||||
|
"name": "perf: 性能优化",
|
||||||
|
"emoji": "⚡️"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "test",
|
||||||
|
"name": "test: 测试相关",
|
||||||
|
"emoji": "🧪"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "build",
|
||||||
|
"name": "build: 构建相关",
|
||||||
|
"emoji": "🏗️"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "ci",
|
||||||
|
"name": "ci: 持续集成",
|
||||||
|
"emoji": "🔧"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "chore",
|
||||||
|
"name": "chore: 其他修改",
|
||||||
|
"emoji": "📌"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value": "revert",
|
||||||
|
"name": "revert: 回退代码",
|
||||||
|
"emoji": "⏪️"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"useEmoji": true,
|
||||||
|
"emojiAlign": "center"
|
||||||
|
}
|
||||||
90
.gitignore
vendored
Normal file
90
.gitignore
vendored
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
.env.development
|
||||||
|
.env.test
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
.trae/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
coverage/
|
||||||
|
.nyc_output/
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# Database
|
||||||
|
*.dump
|
||||||
|
*.sqlite
|
||||||
|
pgdata/
|
||||||
|
# Don't ignore SQL script files in the sql directory
|
||||||
|
!sql/*.sql
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
.docker/
|
||||||
|
*.dockerignore
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
.cache/
|
||||||
|
|
||||||
|
# Frontend specific
|
||||||
|
frontend/dist/
|
||||||
|
frontend/node_modules/
|
||||||
|
frontend/.env
|
||||||
|
frontend/.env.local
|
||||||
|
frontend/.env.*.local
|
||||||
|
|
||||||
|
# Backend specific
|
||||||
|
backend/dist/
|
||||||
|
backend/node_modules/
|
||||||
|
backend/.env
|
||||||
|
backend/.env.local
|
||||||
|
backend/.env.*.local
|
||||||
|
backend/test-game-server.js
|
||||||
|
|
||||||
|
# Package manager files
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
pnpm-lock.yaml
|
||||||
|
|
||||||
|
# TypeScript
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
*.bak
|
||||||
|
*.old
|
||||||
|
*.backup
|
||||||
|
|
||||||
|
# Docs
|
||||||
|
docs/
|
||||||
174
README.md
Normal file
174
README.md
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
# 一体化游戏运营平台
|
||||||
|
|
||||||
|
## 项目概述
|
||||||
|
|
||||||
|
一体化游戏运营平台是一个基于 Vue 3 + Node.js + MySQL 的全栈项目,用于管理游戏运营相关的功能。
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
### 前端
|
||||||
|
- Vue 3 + TypeScript
|
||||||
|
- Vite
|
||||||
|
- Element Plus
|
||||||
|
|
||||||
|
### 后端
|
||||||
|
- Node.js + Express
|
||||||
|
- TypeScript
|
||||||
|
- TypeORM
|
||||||
|
- MySQL 8.4
|
||||||
|
|
||||||
|
### 容器化
|
||||||
|
- Docker
|
||||||
|
- Docker Compose
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
├── backend/ # 后端代码
|
||||||
|
├── frontend/ # 前端代码
|
||||||
|
├── sql/ # 数据库脚本
|
||||||
|
├── docs/ # 项目文档
|
||||||
|
├── docker/ # Docker 配置
|
||||||
|
└── docker-compose.yml # Docker Compose 配置
|
||||||
|
```
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 1. 克隆项目
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <项目地址>
|
||||||
|
cd MHXY_Web
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 配置环境变量
|
||||||
|
|
||||||
|
#### 后端环境变量
|
||||||
|
|
||||||
|
在 `backend` 目录下创建 `.env` 文件:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# 服务器配置
|
||||||
|
PORT=3000
|
||||||
|
HOST=0.0.0.0
|
||||||
|
|
||||||
|
# 数据库配置
|
||||||
|
DB_HOST=database
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_NAME=mhxy_web
|
||||||
|
DB_USER=root
|
||||||
|
DB_PASSWORD=password
|
||||||
|
|
||||||
|
# JWT配置
|
||||||
|
JWT_SECRET=your_jwt_secret_key_here
|
||||||
|
JWT_EXPIRES_IN=24h
|
||||||
|
JWT_REFRESH_SECRET=your_jwt_refresh_secret_key_here
|
||||||
|
JWT_REFRESH_EXPIRES_IN=7d
|
||||||
|
|
||||||
|
# 游戏服务端API配置
|
||||||
|
GAME_API_URL=http://your_game_server_url/tool/http
|
||||||
|
GAME_PSK=THIS_IS_A_32_BYTE_FIXED_PSK!!!!! # 必须是32字节固定长度
|
||||||
|
|
||||||
|
# 日志配置
|
||||||
|
LOG_LEVEL=info
|
||||||
|
|
||||||
|
# 环境配置
|
||||||
|
NODE_ENV=production
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 前端环境变量
|
||||||
|
|
||||||
|
在 `frontend` 目录下创建 `.env` 文件:
|
||||||
|
|
||||||
|
```env
|
||||||
|
VITE_API_BASE_URL=http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 使用 Docker 启动项目
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 直接启动项目
|
||||||
|
|
||||||
|
#### 后端启动
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 前端启动
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## 数据库配置
|
||||||
|
|
||||||
|
项目使用 MySQL 8.4 数据库,主要配置文件:
|
||||||
|
|
||||||
|
- `sql/init_mysql.sql` - 数据库初始化脚本
|
||||||
|
- `backend/src/config/typeorm.config.ts` - TypeORM 配置
|
||||||
|
|
||||||
|
## 开发指南
|
||||||
|
|
||||||
|
### 代码规范
|
||||||
|
|
||||||
|
- 使用 TypeScript 编写代码
|
||||||
|
- 遵循 ESLint 和 Prettier 配置
|
||||||
|
- 提交代码前运行 `npm run lint` 检查代码规范
|
||||||
|
|
||||||
|
### 数据库迁移
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
npm run typeorm migration:generate -- -n <迁移名称>
|
||||||
|
npm run typeorm migration:run
|
||||||
|
```
|
||||||
|
|
||||||
|
## 部署指南
|
||||||
|
|
||||||
|
### 使用 Docker Compose 部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 手动部署
|
||||||
|
|
||||||
|
1. 构建前端代码
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 构建后端代码
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 启动服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node dist/index.js
|
||||||
|
```
|
||||||
|
|
||||||
|
## 文档
|
||||||
|
|
||||||
|
- `docs/数据库迁移/MySQL_迁移说明.md` - MySQL 迁移说明
|
||||||
|
- `docs/开发指南/` - 开发相关文档
|
||||||
|
- `docs/部署指南/` - 部署相关文档
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
MIT
|
||||||
26
backend/.env.example
Normal file
26
backend/.env.example
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# 服务器配置
|
||||||
|
PORT=3000
|
||||||
|
HOST=0.0.0.0
|
||||||
|
|
||||||
|
# 数据库配置
|
||||||
|
DB_HOST=database
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_NAME=mhxy_web
|
||||||
|
DB_USER=root
|
||||||
|
DB_PASSWORD=password
|
||||||
|
|
||||||
|
# JWT配置
|
||||||
|
JWT_SECRET=your_jwt_secret_key_here
|
||||||
|
JWT_EXPIRES_IN=24h
|
||||||
|
JWT_REFRESH_SECRET=your_jwt_refresh_secret_key_here
|
||||||
|
JWT_REFRESH_EXPIRES_IN=7d
|
||||||
|
|
||||||
|
# 游戏服务端API配置
|
||||||
|
GAME_API_URL=http://your_game_server_url/tool/http
|
||||||
|
GAME_PSK=THIS_IS_A_32_BYTE_FIXED_PSK!!!!! # 必须是32字节固定长度
|
||||||
|
|
||||||
|
# 日志配置
|
||||||
|
LOG_LEVEL=info
|
||||||
|
|
||||||
|
# 环境配置
|
||||||
|
NODE_ENV=production
|
||||||
43
backend/Dockerfile
Normal file
43
backend/Dockerfile
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# 多阶段构建:构建阶段
|
||||||
|
FROM node:18-alpine AS build-stage
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 设置国内镜像源以加速依赖安装(可选,根据实际情况调整)
|
||||||
|
# RUN npm config set registry https://registry.npmmirror.com
|
||||||
|
|
||||||
|
# 复制package.json和package-lock.json
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# 安装所有依赖(包括开发依赖)
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# 复制源代码
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# 编译TypeScript代码
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# 多阶段构建:运行阶段
|
||||||
|
FROM node:18-alpine AS production-stage
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 设置时区(可选,根据需要调整)
|
||||||
|
# RUN apk --no-cache add tzdata && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
|
||||||
|
|
||||||
|
# 复制package.json和package-lock.json
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# 安装仅生产依赖
|
||||||
|
RUN npm ci --only=production
|
||||||
|
|
||||||
|
# 复制编译结果
|
||||||
|
COPY --from=build-stage /app/dist ./dist
|
||||||
|
|
||||||
|
# 复制环境变量示例文件(如果存在)
|
||||||
|
COPY .env.example .env
|
||||||
|
|
||||||
|
# 暴露端口
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# 启动应用
|
||||||
|
CMD ["node", "dist/index.js"]
|
||||||
48
backend/package.json
Normal file
48
backend/package.json
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"name": "game-operation-platform-backend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Game operation platform backend API",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"dev": "concurrently \"tsc --watch\" \"nodemon -q dist/index.js\"",
|
||||||
|
"typeorm": "ts-node ./node_modules/typeorm/cli.js",
|
||||||
|
"init:admin": "ts-node src/scripts/initAdmin.ts"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"game",
|
||||||
|
"operation",
|
||||||
|
"platform",
|
||||||
|
"backend"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.13.2",
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-validator": "^0.14.1",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^16.3.1",
|
||||||
|
"express": "^4.18.2",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"mysql2": "^3.9.7",
|
||||||
|
"typeorm": "^0.3.17"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
|
"@types/chai": "^5.2.3",
|
||||||
|
"@types/cors": "^2.8.17",
|
||||||
|
"@types/deep-eql": "^4.0.2",
|
||||||
|
"@types/estree": "^1.0.8",
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/jsonwebtoken": "^9.0.5",
|
||||||
|
"@types/node": "^20.10.5",
|
||||||
|
"concurrently": "^9.2.1",
|
||||||
|
"nodemon": "^3.1.11",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"ts-node-dev": "^2.0.0",
|
||||||
|
"typescript": "^5.3.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
37
backend/src/config/typeorm.config.ts
Normal file
37
backend/src/config/typeorm.config.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { DataSource } from 'typeorm'
|
||||||
|
import * as dotenv from 'dotenv'
|
||||||
|
|
||||||
|
// 加载环境变量
|
||||||
|
dotenv.config()
|
||||||
|
|
||||||
|
export const AppDataSource = new DataSource({
|
||||||
|
type: 'mysql',
|
||||||
|
host: process.env.DB_HOST || 'localhost',
|
||||||
|
port: parseInt(process.env.DB_PORT || '3306'),
|
||||||
|
username: process.env.DB_USER || 'root',
|
||||||
|
password: process.env.DB_PASSWORD || 'password',
|
||||||
|
database: process.env.DB_NAME || 'mhxy_web',
|
||||||
|
entities: [__dirname + '/../models/*.{ts,js}'],
|
||||||
|
migrations: [__dirname + '/../../migrations/*.{ts,js}'],
|
||||||
|
subscribers: [],
|
||||||
|
synchronize: true, // 生产环境建议设置为false
|
||||||
|
logging: ['error', 'warn', 'schema'], // 只记录错误、警告和架构变更,不记录查询日志
|
||||||
|
charset: 'utf8mb4_unicode_ci'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 初始化数据库连接
|
||||||
|
export const initializeDatabase = async () => {
|
||||||
|
try {
|
||||||
|
console.log(`🔄 正在连接数据库 ${process.env.DB_HOST || 'localhost'}:${process.env.DB_PORT || '3306'}/${process.env.DB_NAME || 'mhxy_web'}...`)
|
||||||
|
await AppDataSource.initialize()
|
||||||
|
console.log('✅ 数据库连接成功')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 数据库连接失败:', error)
|
||||||
|
console.error('📋 请检查以下配置:')
|
||||||
|
console.error(' • 数据库主机地址、端口是否正确')
|
||||||
|
console.error(' • 数据库用户名、密码是否正确')
|
||||||
|
console.error(' • 数据库服务是否正常运行')
|
||||||
|
console.error(' • 数据库是否已创建')
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
138
backend/src/index.ts
Normal file
138
backend/src/index.ts
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import * as dotenv from 'dotenv'
|
||||||
|
import express from 'express'
|
||||||
|
import cors from 'cors'
|
||||||
|
import { initializeDatabase, AppDataSource } from './config/typeorm.config'
|
||||||
|
import authRoutes from './routes/auth.routes'
|
||||||
|
import userRoutes from './routes/user.routes'
|
||||||
|
import configRoutes from './routes/config.routes'
|
||||||
|
import gameRoutes from './routes/game.routes'
|
||||||
|
import errorHandler from './middleware/errorHandler'
|
||||||
|
import { Config } from './models/Config'
|
||||||
|
import { configService } from './services/ConfigService'
|
||||||
|
|
||||||
|
// 加载环境变量
|
||||||
|
dotenv.config()
|
||||||
|
|
||||||
|
// 创建Express应用
|
||||||
|
const app = express()
|
||||||
|
|
||||||
|
// 配置中间件
|
||||||
|
app.use(cors())
|
||||||
|
app.use(express.json())
|
||||||
|
app.use(express.urlencoded({ extended: true }))
|
||||||
|
|
||||||
|
// 配置路由
|
||||||
|
app.use('/api/auth', authRoutes)
|
||||||
|
app.use('/api/users', userRoutes)
|
||||||
|
app.use('/api/config', configRoutes)
|
||||||
|
app.use('/api/game', gameRoutes)
|
||||||
|
|
||||||
|
// 错误处理中间件
|
||||||
|
app.use(errorHandler)
|
||||||
|
|
||||||
|
// 启动信息
|
||||||
|
console.log('🚀 正在启动梦幻西游Web管理系统后端服务...')
|
||||||
|
console.log(`📋 环境模式: ${process.env.NODE_ENV || 'development'}`)
|
||||||
|
console.log(`🔧 服务器配置: ${process.env.HOST || 'localhost'}:${process.env.PORT || 3001}`)
|
||||||
|
|
||||||
|
// 启动服务器
|
||||||
|
let PORT = process.env.PORT || 3001
|
||||||
|
let HOST = process.env.HOST || 'localhost'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将.env文件中的配置同步到数据库中
|
||||||
|
* 仅在数据库中没有对应配置时才插入,保留数据库中已有的配置
|
||||||
|
*/
|
||||||
|
const syncEnvToDatabase = async () => {
|
||||||
|
try {
|
||||||
|
const configRepository = AppDataSource.getRepository(Config)
|
||||||
|
|
||||||
|
// 需要同步的配置项映射:.env键名 → 数据库键名
|
||||||
|
const envToDbMapping = {
|
||||||
|
'HOST': { key: 'server_host', description: '后端服务器主机地址' },
|
||||||
|
'PORT': { key: 'server_port', description: '后端服务器端口' },
|
||||||
|
'GAME_API_URL': { key: 'game_api_url', description: '游戏服务API地址' },
|
||||||
|
'GAME_PSK': { key: 'game_psk', description: '游戏服务端的PSK' },
|
||||||
|
'JWT_SECRET': { key: 'jwt_secret', description: 'JWT密钥' },
|
||||||
|
'JWT_EXPIRES_IN': { key: 'jwt_expires_in', description: 'JWT过期时间' },
|
||||||
|
'JWT_REFRESH_SECRET': { key: 'jwt_refresh_secret', description: 'JWT刷新令牌密钥' },
|
||||||
|
'JWT_REFRESH_EXPIRES_IN': { key: 'jwt_refresh_expires_in', description: 'JWT刷新令牌过期时间' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取数据库中已有的配置
|
||||||
|
const existingConfigs = await configRepository.find()
|
||||||
|
const existingConfigKeys = existingConfigs.map(config => config.key)
|
||||||
|
|
||||||
|
// 遍历需要同步的配置项,仅插入不存在的配置
|
||||||
|
let syncCount = 0
|
||||||
|
for (const [envKey, dbConfig] of Object.entries(envToDbMapping)) {
|
||||||
|
if (!existingConfigKeys.includes(dbConfig.key)) {
|
||||||
|
// 数据库中不存在该配置,从.env插入
|
||||||
|
await configRepository.insert({
|
||||||
|
key: dbConfig.key,
|
||||||
|
value: process.env[envKey] || '',
|
||||||
|
description: dbConfig.description
|
||||||
|
})
|
||||||
|
console.log(`✅ 从.env同步配置: ${envKey} → ${dbConfig.key}`)
|
||||||
|
syncCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (syncCount > 0) {
|
||||||
|
console.log(`📊 成功同步 ${syncCount} 个配置项到数据库`)
|
||||||
|
} else {
|
||||||
|
console.log('📊 数据库配置已是最新,无需同步')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('同步环境变量到数据库失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const startServer = async () => {
|
||||||
|
try {
|
||||||
|
console.log('🔄 正在初始化数据库连接...')
|
||||||
|
// 初始化数据库连接
|
||||||
|
await initializeDatabase()
|
||||||
|
|
||||||
|
console.log('🔄 正在同步环境变量到数据库...')
|
||||||
|
// 将.env文件中的配置同步到数据库
|
||||||
|
await syncEnvToDatabase()
|
||||||
|
|
||||||
|
console.log('🔄 正在初始化配置服务...')
|
||||||
|
// 初始化配置服务,从数据库加载配置
|
||||||
|
await configService.initialize()
|
||||||
|
|
||||||
|
// 将数据库配置应用到环境变量,覆盖.env文件中的配置
|
||||||
|
configService.applyToEnv()
|
||||||
|
|
||||||
|
// 更新服务器配置
|
||||||
|
PORT = process.env.PORT || 3001
|
||||||
|
HOST = process.env.HOST || 'localhost'
|
||||||
|
|
||||||
|
// 启动服务器
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log('')
|
||||||
|
console.log('🎉 梦幻西游Web管理系统后端服务启动成功!')
|
||||||
|
console.log(`🌐 服务地址: http://${HOST}:${PORT}`)
|
||||||
|
console.log(`📅 启动时间: ${new Date().toLocaleString('zh-CN')}`)
|
||||||
|
console.log('')
|
||||||
|
console.log('📋 已启用的API端点:')
|
||||||
|
console.log(' • 认证服务: /api/auth')
|
||||||
|
console.log(' • 用户管理: /api/users')
|
||||||
|
console.log(' • 系统配置: /api/config')
|
||||||
|
console.log(' • 游戏服务: /api/game')
|
||||||
|
console.log('')
|
||||||
|
console.log('✨ 系统已就绪,等待客户端连接...')
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 服务器启动失败:', error)
|
||||||
|
console.error('📋 请检查以下配置:')
|
||||||
|
console.error(' • 数据库连接配置是否正确')
|
||||||
|
console.error(' • 数据库服务是否正常运行')
|
||||||
|
console.error(' • 端口是否被占用')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动服务器
|
||||||
|
startServer()
|
||||||
74
backend/src/middleware/authMiddleware.ts
Normal file
74
backend/src/middleware/authMiddleware.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express'
|
||||||
|
import jwt from 'jsonwebtoken'
|
||||||
|
import { User } from '../models/User'
|
||||||
|
import { AppDataSource } from '../config/typeorm.config'
|
||||||
|
|
||||||
|
// 定义请求类型扩展
|
||||||
|
interface AuthRequest extends Request {
|
||||||
|
user?: User
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWT认证中间件
|
||||||
|
const authMiddleware = async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
// 获取Authorization头
|
||||||
|
const authHeader = req.header('Authorization')
|
||||||
|
|
||||||
|
// 检查Authorization头是否存在且格式正确
|
||||||
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: '未授权,请提供有效的认证令牌'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取令牌
|
||||||
|
const token = authHeader.replace('Bearer ', '')
|
||||||
|
|
||||||
|
// 验证令牌
|
||||||
|
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { userId: number }
|
||||||
|
|
||||||
|
// 查找用户
|
||||||
|
const userRepository = AppDataSource.getRepository(User)
|
||||||
|
const user = await userRepository.findOne({ where: { id: decoded.userId } })
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: '认证失败,用户不存在'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将用户信息添加到请求对象
|
||||||
|
req.user = user
|
||||||
|
|
||||||
|
// 继续处理请求
|
||||||
|
next()
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof jwt.TokenExpiredError) {
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: '认证令牌已过期,请重新登录'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: '认证失败,请重新登录'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 管理员权限中间件
|
||||||
|
const adminMiddleware = (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||||
|
if (!req.user || req.user.role !== 'admin') {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: '权限不足,需要管理员权限'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
|
||||||
|
export { authMiddleware, adminMiddleware }
|
||||||
38
backend/src/middleware/errorHandler.ts
Normal file
38
backend/src/middleware/errorHandler.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express'
|
||||||
|
|
||||||
|
// 定义错误接口
|
||||||
|
interface AppError {
|
||||||
|
statusCode: number
|
||||||
|
message: string
|
||||||
|
isOperational?: boolean
|
||||||
|
stack?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误处理中间件
|
||||||
|
const errorHandler = (err: AppError, req: Request, res: Response, next: NextFunction) => {
|
||||||
|
// 设置默认错误状态码和消息
|
||||||
|
const statusCode = err.statusCode || 500
|
||||||
|
const message = err.message || 'Internal Server Error'
|
||||||
|
|
||||||
|
// 构建错误响应
|
||||||
|
const errorResponse = {
|
||||||
|
success: false,
|
||||||
|
message,
|
||||||
|
// 在开发环境中包含堆栈信息
|
||||||
|
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录错误日志
|
||||||
|
console.error(`Error: ${message}`, {
|
||||||
|
statusCode,
|
||||||
|
path: req.path,
|
||||||
|
method: req.method,
|
||||||
|
ip: req.ip,
|
||||||
|
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
|
||||||
|
})
|
||||||
|
|
||||||
|
// 返回错误响应
|
||||||
|
res.status(statusCode).json(errorResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default errorHandler
|
||||||
22
backend/src/models/Config.ts
Normal file
22
backend/src/models/Config.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm'
|
||||||
|
|
||||||
|
@Entity()
|
||||||
|
export class Config {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number
|
||||||
|
|
||||||
|
@Column({ unique: true, length: 50 })
|
||||||
|
key: string
|
||||||
|
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
value: string
|
||||||
|
|
||||||
|
@Column({ length: 100, nullable: true })
|
||||||
|
description?: string
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdAt: Date
|
||||||
|
|
||||||
|
@UpdateDateColumn()
|
||||||
|
updatedAt: Date
|
||||||
|
}
|
||||||
40
backend/src/models/User.ts
Normal file
40
backend/src/models/User.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm'
|
||||||
|
|
||||||
|
export enum UserRole {
|
||||||
|
ADMIN = 'admin'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum UserStatus {
|
||||||
|
ACTIVE = 'ACTIVE',
|
||||||
|
INACTIVE = 'INACTIVE'
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity({ name: 'users' })
|
||||||
|
export class User {
|
||||||
|
@PrimaryGeneratedColumn()
|
||||||
|
id: number
|
||||||
|
|
||||||
|
@Column({ unique: true, length: 50 })
|
||||||
|
username: string
|
||||||
|
|
||||||
|
@Column({ length: 100 })
|
||||||
|
password: string
|
||||||
|
|
||||||
|
@Column({ unique: true, nullable: true, length: 100 })
|
||||||
|
email?: string
|
||||||
|
|
||||||
|
@Column({ unique: true, nullable: true, length: 20 })
|
||||||
|
phone?: string
|
||||||
|
|
||||||
|
@Column({ type: 'enum', enum: UserRole, default: UserRole.ADMIN })
|
||||||
|
role: UserRole
|
||||||
|
|
||||||
|
@Column({ type: 'enum', enum: UserStatus, default: UserStatus.ACTIVE })
|
||||||
|
status: UserStatus
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdAt: Date
|
||||||
|
|
||||||
|
@UpdateDateColumn()
|
||||||
|
updatedAt: Date
|
||||||
|
}
|
||||||
195
backend/src/routes/auth.routes.ts
Normal file
195
backend/src/routes/auth.routes.ts
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
import { Router, Request, Response } from 'express'
|
||||||
|
import bcrypt from 'bcryptjs'
|
||||||
|
import jwt from 'jsonwebtoken'
|
||||||
|
import { User, UserRole } from '../models/User'
|
||||||
|
import { AppDataSource } from '../config/typeorm.config'
|
||||||
|
|
||||||
|
const router = Router()
|
||||||
|
const userRepository = AppDataSource.getRepository(User)
|
||||||
|
|
||||||
|
// 注册路由
|
||||||
|
router.post('/register', async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { username, password, email, phone, role } = req.body
|
||||||
|
|
||||||
|
// 检查用户名是否已存在
|
||||||
|
const existingUser = await userRepository.findOne({ where: { username } })
|
||||||
|
if (existingUser) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '用户名已存在'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查邮箱是否已存在
|
||||||
|
if (email) {
|
||||||
|
const existingEmail = await userRepository.findOne({ where: { email } })
|
||||||
|
if (existingEmail) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '邮箱已被注册'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查手机号是否已存在
|
||||||
|
if (phone) {
|
||||||
|
const existingPhone = await userRepository.findOne({ where: { phone } })
|
||||||
|
if (existingPhone) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '手机号已被注册'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加密密码
|
||||||
|
const hashedPassword = await bcrypt.hash(password, 12)
|
||||||
|
|
||||||
|
// 创建用户
|
||||||
|
const user = userRepository.create({
|
||||||
|
username,
|
||||||
|
password: hashedPassword,
|
||||||
|
email,
|
||||||
|
phone,
|
||||||
|
role: role || UserRole.ADMIN
|
||||||
|
})
|
||||||
|
|
||||||
|
await userRepository.save(user)
|
||||||
|
|
||||||
|
// 生成JWT令牌
|
||||||
|
const token = jwt.sign(
|
||||||
|
{ userId: user.id },
|
||||||
|
process.env.JWT_SECRET as jwt.Secret,
|
||||||
|
{ expiresIn: (process.env.JWT_EXPIRES_IN || '24h') as jwt.SignOptions['expiresIn'] }
|
||||||
|
)
|
||||||
|
|
||||||
|
// 返回用户信息和令牌
|
||||||
|
const { password: _, ...userWithoutPassword } = user
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
message: '注册成功',
|
||||||
|
data: {
|
||||||
|
user: userWithoutPassword,
|
||||||
|
token
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('注册失败:', error)
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '注册失败,请稍后重试'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 登录路由
|
||||||
|
router.post('/login', async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { username, password } = req.body
|
||||||
|
|
||||||
|
// 查找用户
|
||||||
|
const user = await userRepository.findOne({ where: { username } })
|
||||||
|
if (!user) {
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: '用户名或密码错误'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证密码
|
||||||
|
const isPasswordValid = await bcrypt.compare(password, user.password)
|
||||||
|
if (!isPasswordValid) {
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: '用户名或密码错误'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成JWT令牌
|
||||||
|
const token = jwt.sign(
|
||||||
|
{ userId: user.id },
|
||||||
|
process.env.JWT_SECRET as jwt.Secret,
|
||||||
|
{ expiresIn: (process.env.JWT_EXPIRES_IN || '24h') as jwt.SignOptions['expiresIn'] }
|
||||||
|
)
|
||||||
|
|
||||||
|
// 返回用户信息和令牌
|
||||||
|
const { password: _, ...userWithoutPassword } = user
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: '登录成功',
|
||||||
|
data: {
|
||||||
|
user: userWithoutPassword,
|
||||||
|
token
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('登录失败:', error)
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '登录失败,请稍后重试'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取当前用户信息路由
|
||||||
|
router.get('/me', async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
// 从Authorization头获取令牌
|
||||||
|
const authHeader = req.header('Authorization')
|
||||||
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: '未授权,请提供有效的认证令牌'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = authHeader.replace('Bearer ', '')
|
||||||
|
|
||||||
|
// 验证令牌
|
||||||
|
const decoded = jwt.verify(token, process.env.JWT_SECRET as string) as { userId: number }
|
||||||
|
|
||||||
|
// 查找用户
|
||||||
|
const user = await userRepository.findOne({ where: { id: decoded.userId } })
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: '用户不存在'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回用户信息
|
||||||
|
const { password: _, ...userWithoutPassword } = user
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: userWithoutPassword
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof jwt.TokenExpiredError) {
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: '认证令牌已过期,请重新登录'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('获取用户信息失败:', error)
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '获取用户信息失败,请稍后重试'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 登出路由
|
||||||
|
router.post('/logout', async (req: Request, res: Response) => {
|
||||||
|
// 客户端需要自行删除令牌
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: '登出成功'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
118
backend/src/routes/config.routes.ts
Normal file
118
backend/src/routes/config.routes.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import { Router, Request, Response } from 'express'
|
||||||
|
import { Config } from '../models/Config'
|
||||||
|
import { AppDataSource } from '../config/typeorm.config'
|
||||||
|
import { authMiddleware, adminMiddleware } from '../middleware/authMiddleware'
|
||||||
|
import { configService } from '../services/ConfigService'
|
||||||
|
|
||||||
|
const router = Router()
|
||||||
|
const configRepository = AppDataSource.getRepository(Config)
|
||||||
|
|
||||||
|
// 获取所有配置(需要管理员权限)
|
||||||
|
router.get('/', [authMiddleware, adminMiddleware], async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const configs = await configRepository.find()
|
||||||
|
|
||||||
|
// 转换为键值对格式
|
||||||
|
const configMap = configs.reduce((map, config) => {
|
||||||
|
map[config.key] = config.value
|
||||||
|
return map
|
||||||
|
}, {} as Record<string, string>)
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: configMap
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取配置失败:', error)
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '获取配置失败,请稍后重试'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取单个配置
|
||||||
|
router.get('/:key', [authMiddleware], async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { key } = req.params
|
||||||
|
|
||||||
|
const config = await configRepository.findOne({ where: { key } })
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: '配置不存在'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
[config.key]: config.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取配置失败:', error)
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '获取配置失败,请稍后重试'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 设置配置(需要管理员权限)
|
||||||
|
router.put('/:key', [authMiddleware, adminMiddleware], async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { key } = req.params
|
||||||
|
const { value, description } = req.body
|
||||||
|
|
||||||
|
// 使用ConfigService更新配置,会自动处理数据库更新、.env更新和环境变量应用
|
||||||
|
await configService.update(key, value, description)
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: '配置更新成功',
|
||||||
|
data: {
|
||||||
|
[key]: value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新配置失败:', error)
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '更新配置失败,请稍后重试'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 删除配置(需要管理员权限)
|
||||||
|
router.delete('/:key', [authMiddleware, adminMiddleware], async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { key } = req.params
|
||||||
|
|
||||||
|
// 查找配置
|
||||||
|
const config = await configRepository.findOne({ where: { key } })
|
||||||
|
|
||||||
|
if (!config) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: '配置不存在'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await configRepository.remove(config)
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: '配置删除成功'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除配置失败:', error)
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '删除配置失败,请稍后重试'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
153
backend/src/routes/game.routes.ts
Normal file
153
backend/src/routes/game.routes.ts
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import { Router, Request, Response } from 'express'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { authMiddleware } from '../middleware/authMiddleware'
|
||||||
|
import * as dotenv from 'dotenv'
|
||||||
|
|
||||||
|
// 加载环境变量
|
||||||
|
dotenv.config()
|
||||||
|
|
||||||
|
const router = Router()
|
||||||
|
|
||||||
|
// 游戏服务端API基础URL
|
||||||
|
const GAME_API_URL = process.env.GAME_API_URL || 'http://127.0.0.1:8080/tool/http'
|
||||||
|
|
||||||
|
// 检查游戏服务状态
|
||||||
|
router.get('/status', authMiddleware, async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
// 获取PSK密钥
|
||||||
|
const GAME_PSK = process.env.GAME_PSK
|
||||||
|
if (!GAME_PSK) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '游戏服务端PSK密钥未配置'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送测试请求到游戏服务端API
|
||||||
|
const response = await axios.post(GAME_API_URL, { code: 'account/get_account_list', page: 1, pageSize: 1 }, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'psk': GAME_PSK // 添加PSK认证头
|
||||||
|
},
|
||||||
|
timeout: 5000, // 设置5秒超时
|
||||||
|
proxy: false // 禁用代理,确保请求直接发送到目标服务器
|
||||||
|
})
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: '游戏服务端连接成功',
|
||||||
|
data: {
|
||||||
|
status: 'online',
|
||||||
|
responseTime: response.config.timeout
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error('游戏服务状态检查失败:', error)
|
||||||
|
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
// Axios错误处理
|
||||||
|
if (error.code === 'ECONNREFUSED') {
|
||||||
|
return res.status(200).json({
|
||||||
|
success: false,
|
||||||
|
message: '游戏服务端连接失败',
|
||||||
|
data: {
|
||||||
|
status: 'offline',
|
||||||
|
error: '游戏服务端未响应,请检查游戏服务端是否正常运行'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else if (error.code === 'ECONNABORTED') {
|
||||||
|
return res.status(200).json({
|
||||||
|
success: false,
|
||||||
|
message: '游戏服务端请求超时',
|
||||||
|
data: {
|
||||||
|
status: 'timeout',
|
||||||
|
error: '游戏服务端响应超时'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
success: false,
|
||||||
|
message: '游戏服务端状态检查失败',
|
||||||
|
data: {
|
||||||
|
status: 'error',
|
||||||
|
error: '未知错误'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 转发请求到游戏服务端API(需要认证)
|
||||||
|
router.post('/', authMiddleware, async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
// 获取游戏服务端API路径和参数
|
||||||
|
const { path, params } = req.body
|
||||||
|
|
||||||
|
if (!path) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '缺少游戏服务端API路径'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取PSK密钥
|
||||||
|
const GAME_PSK = process.env.GAME_PSK
|
||||||
|
if (!GAME_PSK) {
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '游戏服务端PSK密钥未配置'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建完整的请求URL
|
||||||
|
const apiUrl = `${GAME_API_URL}`
|
||||||
|
|
||||||
|
// 发送请求到游戏服务端API(禁用代理,确保直接连接)
|
||||||
|
const response = await axios.post(apiUrl, { code: path, ...params }, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'psk': GAME_PSK // 添加PSK认证头
|
||||||
|
},
|
||||||
|
timeout: 10000, // 设置10秒超时
|
||||||
|
proxy: false // 禁用代理,确保请求直接发送到目标服务器
|
||||||
|
})
|
||||||
|
|
||||||
|
// 返回游戏服务端API的响应
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: '游戏服务端API调用成功',
|
||||||
|
data: response.data
|
||||||
|
})
|
||||||
|
} catch (error: unknown) {
|
||||||
|
console.error('游戏服务端API调用失败:', error)
|
||||||
|
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
// Axios错误处理
|
||||||
|
if (error.code === 'ECONNREFUSED') {
|
||||||
|
return res.status(503).json({
|
||||||
|
success: false,
|
||||||
|
message: '游戏服务端连接失败,请检查游戏服务端是否正常运行'
|
||||||
|
})
|
||||||
|
} else if (error.code === 'ECONNABORTED') {
|
||||||
|
return res.status(504).json({
|
||||||
|
success: false,
|
||||||
|
message: '游戏服务端API请求超时'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(error.response?.status || 500).json({
|
||||||
|
success: false,
|
||||||
|
message: error.response?.data?.message || '游戏服务端API调用失败',
|
||||||
|
data: error.response?.data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '游戏服务端API调用失败,请稍后重试'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
375
backend/src/routes/user.routes.ts
Normal file
375
backend/src/routes/user.routes.ts
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
import { Router, Request, Response } from 'express'
|
||||||
|
import { User, UserRole, UserStatus } from '../models/User'
|
||||||
|
import { AppDataSource } from '../config/typeorm.config'
|
||||||
|
import { authMiddleware, adminMiddleware } from '../middleware/authMiddleware'
|
||||||
|
import bcrypt from 'bcryptjs'
|
||||||
|
|
||||||
|
const router = Router()
|
||||||
|
const userRepository = AppDataSource.getRepository(User)
|
||||||
|
// 获取用户列表(需要管理员权限)
|
||||||
|
router.get('/', [authMiddleware, adminMiddleware], async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { page = 1, limit = 10, role, status } = req.query
|
||||||
|
|
||||||
|
const skip = (parseInt(page as string) - 1) * parseInt(limit as string)
|
||||||
|
const take = parseInt(limit as string)
|
||||||
|
|
||||||
|
const whereCondition: any = {}
|
||||||
|
if (role && Object.values(UserRole).includes(role as UserRole)) {
|
||||||
|
whereCondition.role = role
|
||||||
|
}
|
||||||
|
if (status && Object.values(UserStatus).includes(status as UserStatus)) {
|
||||||
|
whereCondition.status = status
|
||||||
|
}
|
||||||
|
|
||||||
|
const [users, total] = await userRepository.findAndCount({
|
||||||
|
where: whereCondition,
|
||||||
|
skip,
|
||||||
|
take,
|
||||||
|
order: { createdAt: 'DESC' }
|
||||||
|
})
|
||||||
|
|
||||||
|
// 移除密码字段
|
||||||
|
const usersWithoutPassword = users.map(user => {
|
||||||
|
const { password, ...userData } = user
|
||||||
|
return userData
|
||||||
|
})
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
users: usersWithoutPassword,
|
||||||
|
total,
|
||||||
|
page: parseInt(page as string),
|
||||||
|
limit: parseInt(limit as string),
|
||||||
|
pages: Math.ceil(total / take)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户列表失败:', error)
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '获取用户列表失败,请稍后重试'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 创建用户(需要管理员权限)
|
||||||
|
router.post('/', [authMiddleware, adminMiddleware], async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { username, password, email, phone, role, status } = req.body
|
||||||
|
|
||||||
|
// 检查用户名是否已存在
|
||||||
|
const existingUser = await userRepository.findOne({ where: { username } })
|
||||||
|
if (existingUser) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '用户名已存在'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查邮箱是否已存在
|
||||||
|
if (email) {
|
||||||
|
const existingEmail = await userRepository.findOne({ where: { email } })
|
||||||
|
if (existingEmail) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '邮箱已被注册'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查手机号是否已存在
|
||||||
|
if (phone) {
|
||||||
|
const existingPhone = await userRepository.findOne({ where: { phone } })
|
||||||
|
if (existingPhone) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '手机号已被注册'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加密密码
|
||||||
|
const hashedPassword = await bcrypt.hash(password, 12)
|
||||||
|
|
||||||
|
// 创建新用户
|
||||||
|
const newUser = userRepository.create({
|
||||||
|
username,
|
||||||
|
password: hashedPassword,
|
||||||
|
email,
|
||||||
|
phone,
|
||||||
|
role: role || UserRole.ADMIN,
|
||||||
|
status: status || UserStatus.ACTIVE
|
||||||
|
})
|
||||||
|
|
||||||
|
await userRepository.save(newUser)
|
||||||
|
|
||||||
|
// 移除密码字段
|
||||||
|
const { password: _, ...userData } = newUser
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
message: '用户创建成功',
|
||||||
|
data: userData
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建用户失败:', error)
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '创建用户失败,请稍后重试'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取用户详情
|
||||||
|
router.get('/:id', authMiddleware, async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params
|
||||||
|
const user = await userRepository.findOne({ where: { id: parseInt(id) } })
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: '用户不存在'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除密码字段
|
||||||
|
const { password, ...userData } = user
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: userData
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户详情失败:', error)
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '获取用户详情失败,请稍后重试'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 更新用户信息
|
||||||
|
router.put('/:id', authMiddleware, async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params
|
||||||
|
const { username, email, phone, role, status } = req.body
|
||||||
|
|
||||||
|
// 查找用户
|
||||||
|
const user = await userRepository.findOne({ where: { id: parseInt(id) } })
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: '用户不存在'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户名是否已存在(排除当前用户)
|
||||||
|
if (username && username !== user.username) {
|
||||||
|
const existingUser = await userRepository.findOne({ where: { username } })
|
||||||
|
if (existingUser) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '用户名已存在'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查邮箱是否已存在(排除当前用户)
|
||||||
|
if (email && email !== user.email) {
|
||||||
|
const existingEmail = await userRepository.findOne({ where: { email } })
|
||||||
|
if (existingEmail) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '邮箱已被注册'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查手机号是否已存在(排除当前用户)
|
||||||
|
if (phone && phone !== user.phone) {
|
||||||
|
const existingPhone = await userRepository.findOne({ where: { phone } })
|
||||||
|
if (existingPhone) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '手机号已被注册'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户信息
|
||||||
|
if (username) user.username = username
|
||||||
|
if (email) user.email = email
|
||||||
|
if (phone) user.phone = phone
|
||||||
|
if (role && Object.values(UserRole).includes(role)) {
|
||||||
|
// 只有管理员可以更改角色
|
||||||
|
if ((req as any).user?.role !== UserRole.ADMIN) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: '权限不足,无法更改用户角色'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
user.role = role
|
||||||
|
}
|
||||||
|
if (status && Object.values(UserStatus).includes(status)) {
|
||||||
|
// 只有管理员可以更改状态
|
||||||
|
if ((req as any).user?.role !== UserRole.ADMIN) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: '权限不足,无法更改用户状态'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
user.status = status
|
||||||
|
}
|
||||||
|
|
||||||
|
await userRepository.save(user)
|
||||||
|
|
||||||
|
// 移除密码字段
|
||||||
|
const { password, ...userData } = user
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: '用户信息更新成功',
|
||||||
|
data: userData
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新用户信息失败:', error)
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '更新用户信息失败,请稍后重试'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 更新用户密码
|
||||||
|
router.put('/:id/password', authMiddleware, async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params
|
||||||
|
const { oldPassword, newPassword } = req.body
|
||||||
|
|
||||||
|
// 查找用户
|
||||||
|
const user = await userRepository.findOne({ where: { id: parseInt(id) } })
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: '用户不存在'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证旧密码
|
||||||
|
if ((req as any).user?.role !== UserRole.ADMIN) {
|
||||||
|
// 非管理员需要验证旧密码
|
||||||
|
const isPasswordValid = await bcrypt.compare(oldPassword, user.password)
|
||||||
|
if (!isPasswordValid) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '旧密码错误'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加密新密码
|
||||||
|
const hashedPassword = await bcrypt.hash(newPassword, 12)
|
||||||
|
|
||||||
|
// 更新密码
|
||||||
|
user.password = hashedPassword
|
||||||
|
await userRepository.save(user)
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: '密码更新成功'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新密码失败:', error)
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '更新密码失败,请稍后重试'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 批量更新用户状态(需要管理员权限)
|
||||||
|
router.put('/batch/status', [authMiddleware, adminMiddleware], async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { ids, status } = req.body
|
||||||
|
|
||||||
|
if (!Array.isArray(ids) || ids.length === 0) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '请提供有效的用户ID列表'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!status || !Object.values(UserStatus).includes(status as UserStatus)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '请提供有效的用户状态'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不能修改当前登录用户的状态
|
||||||
|
const currentUserId = (req as any).user?.id
|
||||||
|
const filteredIds = ids.filter(id => id !== currentUserId)
|
||||||
|
|
||||||
|
if (filteredIds.length === 0) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '不能修改当前登录用户的状态'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await userRepository.update(filteredIds, { status })
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: `成功更新${filteredIds.length}个用户的状态`
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('批量更新用户状态失败:', error)
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '批量更新用户状态失败,请稍后重试'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 删除用户(需要管理员权限)
|
||||||
|
router.delete('/:id', [authMiddleware, adminMiddleware], async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params
|
||||||
|
|
||||||
|
// 查找用户
|
||||||
|
const user = await userRepository.findOne({ where: { id: parseInt(id) } })
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: '用户不存在'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不能删除自己
|
||||||
|
if ((req as any).user?.id === user.id) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '不能删除当前登录的用户'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await userRepository.remove(user)
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
message: '用户删除成功'
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除用户失败:', error)
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '删除用户失败,请稍后重试'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
52
backend/src/scripts/initAdmin.ts
Normal file
52
backend/src/scripts/initAdmin.ts
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { AppDataSource } from '../config/typeorm.config'
|
||||||
|
import { User, UserRole } from '../models/User'
|
||||||
|
import bcrypt from 'bcryptjs'
|
||||||
|
|
||||||
|
// 初始化管理员用户
|
||||||
|
const initAdmin = async () => {
|
||||||
|
try {
|
||||||
|
// 连接数据库
|
||||||
|
await AppDataSource.initialize()
|
||||||
|
console.log('数据库连接成功')
|
||||||
|
|
||||||
|
const userRepository = AppDataSource.getRepository(User)
|
||||||
|
|
||||||
|
// 检查是否已存在管理员用户
|
||||||
|
const existingAdmin = await userRepository.findOne({ where: { username: 'admin' } })
|
||||||
|
|
||||||
|
if (existingAdmin) {
|
||||||
|
console.log('管理员用户已存在')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加密密码
|
||||||
|
const hashedPassword = await bcrypt.hash('admin123', 12)
|
||||||
|
|
||||||
|
// 创建管理员用户
|
||||||
|
const adminUser = userRepository.create({
|
||||||
|
username: 'admin',
|
||||||
|
password: hashedPassword,
|
||||||
|
role: UserRole.ADMIN
|
||||||
|
})
|
||||||
|
|
||||||
|
await userRepository.save(adminUser)
|
||||||
|
|
||||||
|
console.log('管理员用户创建成功')
|
||||||
|
console.log('用户名: admin')
|
||||||
|
console.log('密码: admin123')
|
||||||
|
|
||||||
|
// 断开数据库连接
|
||||||
|
await AppDataSource.destroy()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('初始化管理员用户失败:', error)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行初始化
|
||||||
|
export default initAdmin
|
||||||
|
|
||||||
|
// 直接执行脚本
|
||||||
|
if (require.main === module) {
|
||||||
|
initAdmin()
|
||||||
|
}
|
||||||
227
backend/src/services/ConfigService.ts
Normal file
227
backend/src/services/ConfigService.ts
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
import { AppDataSource } from '../config/typeorm.config'
|
||||||
|
import { Config } from '../models/Config'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
import * as path from 'path'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置服务,用于从数据库中读取和管理配置
|
||||||
|
*/
|
||||||
|
export class ConfigService {
|
||||||
|
private configRepository = AppDataSource.getRepository(Config)
|
||||||
|
private configCache: Record<string, string> = {}
|
||||||
|
private envFilePath: string = path.resolve(__dirname, '../../.env')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化配置服务
|
||||||
|
* 从数据库中加载所有配置项到缓存中
|
||||||
|
*/
|
||||||
|
async initialize() {
|
||||||
|
try {
|
||||||
|
console.log('🔄 正在从数据库加载系统配置...')
|
||||||
|
const configs = await this.configRepository.find()
|
||||||
|
this.configCache = configs.reduce((map, config) => {
|
||||||
|
map[config.key] = config.value
|
||||||
|
return map
|
||||||
|
}, {} as Record<string, string>)
|
||||||
|
console.log(`✅ 配置服务初始化成功,已加载 ${configs.length} 个配置项`)
|
||||||
|
|
||||||
|
// 显示关键配置信息(脱敏显示)
|
||||||
|
const importantKeys = ['server_host', 'server_port', 'game_api_url', 'game_psk']
|
||||||
|
importantKeys.forEach(key => {
|
||||||
|
if (this.configCache[key]) {
|
||||||
|
let displayValue = this.configCache[key]
|
||||||
|
// 对敏感信息进行脱敏处理
|
||||||
|
if (key.includes('secret')) {
|
||||||
|
displayValue = displayValue ? '*'.repeat(Math.min(displayValue.length, 8)) : '未设置'
|
||||||
|
}
|
||||||
|
console.log(` 📋 ${key}: ${displayValue}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 配置服务初始化失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取配置值
|
||||||
|
* @param key 配置键名
|
||||||
|
* @param defaultValue 默认值,当配置不存在时返回
|
||||||
|
* @returns 配置值
|
||||||
|
*/
|
||||||
|
get(key: string, defaultValue?: string): string {
|
||||||
|
return this.configCache[key] || defaultValue || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有配置
|
||||||
|
* @returns 所有配置项
|
||||||
|
*/
|
||||||
|
getAll(): Record<string, string> {
|
||||||
|
return { ...this.configCache }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新配置
|
||||||
|
* @param key 配置键名
|
||||||
|
* @param value 配置值
|
||||||
|
* @param description 配置描述
|
||||||
|
*/
|
||||||
|
async update(key: string, value: string, description?: string) {
|
||||||
|
try {
|
||||||
|
// 更新数据库
|
||||||
|
await this.configRepository.upsert(
|
||||||
|
{ key, value, description },
|
||||||
|
['key']
|
||||||
|
)
|
||||||
|
|
||||||
|
// 更新缓存
|
||||||
|
this.configCache[key] = value
|
||||||
|
|
||||||
|
// 更新.env文件
|
||||||
|
await this.updateEnvFile(key, value)
|
||||||
|
|
||||||
|
// 应用到环境变量
|
||||||
|
this.applyToEnvKey(key, value)
|
||||||
|
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新配置失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将数据库配置应用到环境变量
|
||||||
|
* 覆盖process.env中的配置
|
||||||
|
*/
|
||||||
|
applyToEnv() {
|
||||||
|
console.log('🔄 正在应用数据库配置到环境变量...')
|
||||||
|
let appliedCount = 0
|
||||||
|
|
||||||
|
// 服务器配置
|
||||||
|
if (this.configCache.server_host) {
|
||||||
|
process.env.HOST = this.configCache.server_host
|
||||||
|
appliedCount++
|
||||||
|
}
|
||||||
|
if (this.configCache.server_port) {
|
||||||
|
process.env.PORT = this.configCache.server_port
|
||||||
|
appliedCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
// 游戏服务API配置
|
||||||
|
if (this.configCache.game_api_url) {
|
||||||
|
process.env.GAME_API_URL = this.configCache.game_api_url
|
||||||
|
appliedCount++
|
||||||
|
}
|
||||||
|
// 游戏服务PSK配置
|
||||||
|
if (this.configCache.game_psk) {
|
||||||
|
process.env.GAME_PSK = this.configCache.game_psk
|
||||||
|
appliedCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWT配置
|
||||||
|
if (this.configCache.jwt_secret) {
|
||||||
|
process.env.JWT_SECRET = this.configCache.jwt_secret
|
||||||
|
appliedCount++
|
||||||
|
}
|
||||||
|
if (this.configCache.jwt_expires_in) {
|
||||||
|
process.env.JWT_EXPIRES_IN = this.configCache.jwt_expires_in
|
||||||
|
appliedCount++
|
||||||
|
}
|
||||||
|
if (this.configCache.jwt_refresh_secret) {
|
||||||
|
process.env.JWT_REFRESH_SECRET = this.configCache.jwt_refresh_secret
|
||||||
|
appliedCount++
|
||||||
|
}
|
||||||
|
if (this.configCache.jwt_refresh_expires_in) {
|
||||||
|
process.env.JWT_REFRESH_EXPIRES_IN = this.configCache.jwt_refresh_expires_in
|
||||||
|
appliedCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✅ 数据库配置应用完成,已更新 ${appliedCount} 个环境变量`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将单个配置键应用到环境变量
|
||||||
|
* @param key 配置键名
|
||||||
|
* @param value 配置值
|
||||||
|
*/
|
||||||
|
private applyToEnvKey(key: string, value: string) {
|
||||||
|
// 配置键映射:数据库键名 → .env键名
|
||||||
|
const keyMapping: Record<string, string> = {
|
||||||
|
server_host: 'HOST',
|
||||||
|
server_port: 'PORT',
|
||||||
|
game_api_url: 'GAME_API_URL',
|
||||||
|
game_psk: 'GAME_PSK',
|
||||||
|
jwt_secret: 'JWT_SECRET',
|
||||||
|
jwt_expires_in: 'JWT_EXPIRES_IN',
|
||||||
|
jwt_refresh_secret: 'JWT_REFRESH_SECRET',
|
||||||
|
jwt_refresh_expires_in: 'JWT_REFRESH_EXPIRES_IN'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用到环境变量
|
||||||
|
const envKey = keyMapping[key]
|
||||||
|
if (envKey) {
|
||||||
|
process.env[envKey] = value
|
||||||
|
console.log(`配置 ${key} 已应用到环境变量 ${envKey}=${value}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新.env文件中的配置项
|
||||||
|
* @param key 数据库中的配置键名
|
||||||
|
* @param value 配置值
|
||||||
|
*/
|
||||||
|
private async updateEnvFile(key: string, value: string) {
|
||||||
|
try {
|
||||||
|
// 配置键映射:数据库键名 → .env键名
|
||||||
|
const keyMapping: Record<string, string> = {
|
||||||
|
server_host: 'HOST',
|
||||||
|
server_port: 'PORT',
|
||||||
|
game_api_url: 'GAME_API_URL',
|
||||||
|
game_psk: 'GAME_PSK',
|
||||||
|
jwt_secret: 'JWT_SECRET',
|
||||||
|
jwt_expires_in: 'JWT_EXPIRES_IN',
|
||||||
|
jwt_refresh_secret: 'JWT_REFRESH_SECRET',
|
||||||
|
jwt_refresh_expires_in: 'JWT_REFRESH_EXPIRES_IN'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取对应的.env键名
|
||||||
|
const envKey = keyMapping[key]
|
||||||
|
if (!envKey) {
|
||||||
|
console.log(` ℹ️ 配置 ${key} 无需更新到.env文件`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取.env文件内容
|
||||||
|
let envContent = ''
|
||||||
|
if (fs.existsSync(this.envFilePath)) {
|
||||||
|
envContent = fs.readFileSync(this.envFilePath, 'utf8')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建新的配置行
|
||||||
|
const newConfigLine = `${envKey}=${value}`
|
||||||
|
const envKeyRegex = new RegExp(`^${envKey}=.*$`, 'gm')
|
||||||
|
|
||||||
|
// 更新或添加配置行
|
||||||
|
let updatedContent = ''
|
||||||
|
if (envKeyRegex.test(envContent)) {
|
||||||
|
// 替换现有配置行
|
||||||
|
updatedContent = envContent.replace(envKeyRegex, newConfigLine)
|
||||||
|
} else {
|
||||||
|
// 添加新配置行
|
||||||
|
updatedContent = envContent ? `${envContent}\n${newConfigLine}` : newConfigLine
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入.env文件
|
||||||
|
fs.writeFileSync(this.envFilePath, updatedContent, 'utf8')
|
||||||
|
console.log(` ✍️ .env文件已更新: ${envKey}=${value}`)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ 更新.env文件失败:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出单例实例
|
||||||
|
export const configService = new ConfigService()
|
||||||
18
backend/tsconfig.json
Normal file
18
backend/tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2016",
|
||||||
|
"module": "commonjs",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"strictPropertyInitialization": false
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
93
cz.config.js
Normal file
93
cz.config.js
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
module.exports = {
|
||||||
|
// 提交信息的类型
|
||||||
|
types: [
|
||||||
|
{
|
||||||
|
value: 'feat',
|
||||||
|
name: 'feat: 新增功能',
|
||||||
|
emoji: '✨'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'fix',
|
||||||
|
name: 'fix: 修复bug',
|
||||||
|
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: CI/CD配置',
|
||||||
|
emoji: '👷'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'chore',
|
||||||
|
name: 'chore: 其他(不影响代码)',
|
||||||
|
emoji: '🔧'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'revert',
|
||||||
|
name: 'revert: 回滚提交',
|
||||||
|
emoji: '⏪️'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
// 提交信息的范围
|
||||||
|
scopes: [
|
||||||
|
{
|
||||||
|
value: 'frontend',
|
||||||
|
name: 'frontend: 前端'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'backend',
|
||||||
|
name: 'backend: 后端'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'docs',
|
||||||
|
name: 'docs: 文档'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'other',
|
||||||
|
name: 'other: 其他'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
// 允许自定义范围
|
||||||
|
allowCustomScopes: true,
|
||||||
|
// 允许空范围
|
||||||
|
allowEmptyScopes: true,
|
||||||
|
// 提交信息的主题长度限制
|
||||||
|
subjectLimit: 100,
|
||||||
|
// 主题末尾不允许有句号
|
||||||
|
subjectFullStop: false,
|
||||||
|
// 主题不区分大小写
|
||||||
|
subjectCase: false,
|
||||||
|
// 使用emoji
|
||||||
|
useEmoji: true,
|
||||||
|
// emoji放在类型后面
|
||||||
|
emojiAlign: 'right'
|
||||||
|
};
|
||||||
86
docker-compose.yml
Normal file
86
docker-compose.yml
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
version: '1.0'
|
||||||
|
|
||||||
|
services:
|
||||||
|
# 前端服务
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: ./frontend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
networks:
|
||||||
|
- mhxy_network
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
|
||||||
|
# 后端服务
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: ./backend
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
depends_on:
|
||||||
|
- database
|
||||||
|
networks:
|
||||||
|
- mhxy_network
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- PORT=3000
|
||||||
|
- HOST=0.0.0.0
|
||||||
|
- DB_HOST=database
|
||||||
|
- DB_PORT=5432
|
||||||
|
- DB_NAME=mhxy_web
|
||||||
|
- DB_USER=postgres
|
||||||
|
- DB_PASSWORD=postgres
|
||||||
|
# 以下环境变量需要根据实际情况在.env文件中配置
|
||||||
|
# - JWT_SECRET=your_jwt_secret_key_here
|
||||||
|
# - JWT_EXPIRES_IN=24h
|
||||||
|
# - GAME_API_URL=http://your_game_server_url/tool/http
|
||||||
|
# - GAME_PSK=THIS_IS_A_32_BYTE_FIXED_PSK!!!!!
|
||||||
|
volumes:
|
||||||
|
# 可选:挂载日志目录(如果需要)
|
||||||
|
# - ./backend/logs:/app/logs
|
||||||
|
# 可选:挂载环境变量文件(如果需要)
|
||||||
|
- ./backend/.env:/app/.env
|
||||||
|
|
||||||
|
# 数据库服务
|
||||||
|
database:
|
||||||
|
image: mysql:8.4
|
||||||
|
ports:
|
||||||
|
- "3306:3306"
|
||||||
|
environment:
|
||||||
|
- MYSQL_ROOT_PASSWORD=${DB_PASSWORD:-password}
|
||||||
|
- MYSQL_DATABASE=${DB_NAME:-mhxy_web}
|
||||||
|
- MYSQL_USER=${DB_USER:-root}
|
||||||
|
- MYSQL_PASSWORD=${DB_PASSWORD:-password}
|
||||||
|
volumes:
|
||||||
|
# 持久化数据库数据
|
||||||
|
- mysql_data:/var/lib/mysql
|
||||||
|
# 挂载初始化SQL脚本
|
||||||
|
- ./sql/init_mysql.sql:/docker-entrypoint-initdb.d/init.sql
|
||||||
|
networks:
|
||||||
|
- mhxy_network
|
||||||
|
restart: unless-stopped
|
||||||
|
# 可选:配置健康检查
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "mysqladmin ping -h localhost -u ${DB_USER:-root} -p${DB_PASSWORD:-password}"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
# 网络配置
|
||||||
|
networks:
|
||||||
|
mhxy_network:
|
||||||
|
driver: bridge
|
||||||
|
name: mhxy_network
|
||||||
|
|
||||||
|
# 数据卷配置
|
||||||
|
volumes:
|
||||||
|
mysql_data:
|
||||||
|
driver: local
|
||||||
|
name: mhxy_mysql_data
|
||||||
13
frontend/.env.production.example
Normal file
13
frontend/.env.production.example
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# 生产环境配置
|
||||||
|
|
||||||
|
# 游戏服务端API配置(使用Vite代理)
|
||||||
|
VITE_GAME_API_URL=/game-api/tool/http
|
||||||
|
|
||||||
|
# 后端API基础URL
|
||||||
|
VITE_API_BASE_URL=/api
|
||||||
|
|
||||||
|
# 应用标题
|
||||||
|
VITE_APP_TITLE=梦幻西游Web管理系统
|
||||||
|
|
||||||
|
# 应用版本
|
||||||
|
VITE_APP_VERSION=1.0.0
|
||||||
33
frontend/Dockerfile
Normal file
33
frontend/Dockerfile
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# 多阶段构建:构建阶段
|
||||||
|
FROM node:18-alpine AS build-stage
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 设置国内镜像源以加速依赖安装(可选,根据实际情况调整)
|
||||||
|
# RUN npm config set registry https://registry.npmmirror.com
|
||||||
|
|
||||||
|
# 复制package.json和package-lock.json
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# 复制源代码
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# 构建应用
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# 多阶段构建:运行阶段
|
||||||
|
FROM nginx:1.23-alpine AS production-stage
|
||||||
|
|
||||||
|
# 复制构建结果到Nginx
|
||||||
|
COPY --from=build-stage /app/dist /usr/share/nginx/html
|
||||||
|
|
||||||
|
# 复制Nginx配置文件
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
# 暴露端口
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# 启动Nginx
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>一体化游戏运营平台</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
43
frontend/nginx.conf
Normal file
43
frontend/nginx.conf
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
# 配置静态资源服务
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
# 处理前端路由(SPA模式)
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 配置API反向代理到后端服务
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://backend:3000/api/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# 增加超时配置
|
||||||
|
proxy_connect_timeout 5s;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 配置游戏API代理(如果需要)
|
||||||
|
location /game-api/ {
|
||||||
|
proxy_pass http://backend:3000/game-api/;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
# 增加超时配置
|
||||||
|
proxy_connect_timeout 5s;
|
||||||
|
proxy_read_timeout 60s;
|
||||||
|
proxy_send_timeout 60s;
|
||||||
|
}
|
||||||
|
|
||||||
|
# 配置404页面
|
||||||
|
error_page 404 /index.html;
|
||||||
|
}
|
||||||
27
frontend/package.json
Normal file
27
frontend/package.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "game-operation-platform-frontend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vue-tsc && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@remixicon/vue": "^4.7.0",
|
||||||
|
"axios": "^1.6.5",
|
||||||
|
"element-plus": "^2.5.1",
|
||||||
|
"pinia": "^2.1.7",
|
||||||
|
"vue": "^3.4.15",
|
||||||
|
"vue-router": "^4.2.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/chai": "^5.2.3",
|
||||||
|
"@types/deep-eql": "^4.0.2",
|
||||||
|
"@vitejs/plugin-vue": "^5.0.3",
|
||||||
|
"typescript": "^5.2.2",
|
||||||
|
"vite": "^5.0.11",
|
||||||
|
"vue-tsc": "^3.1.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
37
frontend/src/App.vue
Normal file
37
frontend/src/App.vue
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<template>
|
||||||
|
<div id="app">
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
import { useUserStore } from '@/store/user'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
// 应用启动时,从localStorage恢复用户信息
|
||||||
|
onMounted(() => {
|
||||||
|
userStore.recoverUser()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB',
|
||||||
|
'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
114
frontend/src/api/auth.ts
Normal file
114
frontend/src/api/auth.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import api from './index'
|
||||||
|
import { gameApi } from './index'
|
||||||
|
import type { LoginForm, LoginResponse, LogoutResponse } from '@/types/auth'
|
||||||
|
import { usePlayerStore } from '@/store/player'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户登录(运营管理系统)
|
||||||
|
* @param form 登录表单数据
|
||||||
|
* @returns 登录响应
|
||||||
|
*/
|
||||||
|
export const login = (form: LoginForm): Promise<LoginResponse> => {
|
||||||
|
return api.post('/auth/login', form)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 玩家登录(游戏服务端API)
|
||||||
|
* @param form 登录表单数据
|
||||||
|
* @returns 登录响应
|
||||||
|
*/
|
||||||
|
export const playerLogin = async (form: LoginForm): Promise<any> => {
|
||||||
|
// 按照游戏服务端API要求的格式发送请求
|
||||||
|
const requestData = {
|
||||||
|
code: 'auth/login',
|
||||||
|
username: form.username,
|
||||||
|
password: form.password
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await gameApi.post('', requestData)
|
||||||
|
|
||||||
|
// 处理登录成功,保存玩家信息到独立存储
|
||||||
|
// 注意:由于gameApi响应拦截器返回的是response.data,所以response就是实际数据
|
||||||
|
if ((response as any)?.code === 200) {
|
||||||
|
const playerStore = usePlayerStore()
|
||||||
|
// 使用正确的响应数据结构
|
||||||
|
// 游戏服务端返回的data字段就是token
|
||||||
|
if (response.data) {
|
||||||
|
const token = response.data
|
||||||
|
const player = {
|
||||||
|
id: 0,
|
||||||
|
username: form.username,
|
||||||
|
role: 'player' as const,
|
||||||
|
status: 'ACTIVE' as const,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存token和玩家信息
|
||||||
|
playerStore.gameToken = token
|
||||||
|
playerStore.player = player
|
||||||
|
localStorage.setItem('gameToken', token)
|
||||||
|
localStorage.setItem('player', JSON.stringify(player))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户登出(运营管理系统)
|
||||||
|
* @returns 登出响应
|
||||||
|
*/
|
||||||
|
export const logout = (): Promise<LogoutResponse> => {
|
||||||
|
return api.post('/auth/logout')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 玩家登出(游戏服务端API)
|
||||||
|
* @returns 登出响应
|
||||||
|
*/
|
||||||
|
export const playerLogout = async (): Promise<any> => {
|
||||||
|
// 按照游戏服务端API要求的格式发送请求
|
||||||
|
const requestData = {
|
||||||
|
code: 'auth/out_login'
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await gameApi.post('', requestData)
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('玩家登出API调用失败:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前玩家信息(游戏服务端API)
|
||||||
|
* @returns 玩家信息响应
|
||||||
|
*/
|
||||||
|
export const getPlayerInfo = async (): Promise<any> => {
|
||||||
|
// 按照游戏服务端API要求的格式发送请求
|
||||||
|
const requestData = {
|
||||||
|
code: 'account/get_account'
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await gameApi.post('', requestData)
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取玩家信息API调用失败:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户信息(运营管理系统)
|
||||||
|
* @returns 当前用户信息
|
||||||
|
*/
|
||||||
|
export const getCurrentUser = () => {
|
||||||
|
return api.get('/auth/me')
|
||||||
|
}
|
||||||
66
frontend/src/api/config.ts
Normal file
66
frontend/src/api/config.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import api from './index'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有配置项
|
||||||
|
* @returns 配置项列表
|
||||||
|
*/
|
||||||
|
export const getAllConfigs = () => {
|
||||||
|
return api.get('/config')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单个配置项
|
||||||
|
* @param key 配置键名
|
||||||
|
* @returns 配置项详情
|
||||||
|
*/
|
||||||
|
export const getConfig = (key: string) => {
|
||||||
|
return api.get(`/config/${key}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置配置项
|
||||||
|
* @param data 配置数据
|
||||||
|
* @returns 设置结果
|
||||||
|
*/
|
||||||
|
export const addConfig = (data: { key: string; value: string; description?: string }) => {
|
||||||
|
return api.post('/config', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新配置项
|
||||||
|
* @param key 配置键名
|
||||||
|
* @param value 配置值
|
||||||
|
* @param description 配置描述(可选)
|
||||||
|
* @returns 更新结果
|
||||||
|
*/
|
||||||
|
export const updateConfig = (key: string, value: any, description?: string) => {
|
||||||
|
return api.put(`/config/${key}`, { value, description })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除配置项
|
||||||
|
* @param key 配置键名
|
||||||
|
* @returns 删除结果
|
||||||
|
*/
|
||||||
|
export const deleteConfig = (key: string) => {
|
||||||
|
return api.delete(`/config/${key}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除配置项
|
||||||
|
* @param ids 配置项ID列表
|
||||||
|
* @returns 删除结果
|
||||||
|
*/
|
||||||
|
export const batchDeleteConfigs = (ids: string[]) => {
|
||||||
|
return api.delete('/config/batch', { data: { ids } })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一导出配置API
|
||||||
|
export const configApi = {
|
||||||
|
getAllConfigs,
|
||||||
|
getConfig,
|
||||||
|
addConfig,
|
||||||
|
updateConfig,
|
||||||
|
deleteConfig,
|
||||||
|
batchDeleteConfigs
|
||||||
|
}
|
||||||
31
frontend/src/api/game.ts
Normal file
31
frontend/src/api/game.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import api, { gameApi } from './index'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用游戏服务端API(直接调用)
|
||||||
|
* @param path API路径
|
||||||
|
* @param data 请求数据
|
||||||
|
* @returns 响应结果
|
||||||
|
*/
|
||||||
|
export const callGameApi = (path: string, data: any) => {
|
||||||
|
return gameApi.post('', { code: path, ...data })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过后端转发调用游戏服务端API(运营管理系统专用)
|
||||||
|
* @param path API路径
|
||||||
|
* @param data 请求数据
|
||||||
|
* @returns 响应结果
|
||||||
|
*/
|
||||||
|
export const callGameApiThroughBackend = (path: string, data: any) => {
|
||||||
|
// 调用后端的/api/game接口,由后端转发请求到游戏服务端
|
||||||
|
return api.post('/game', { path, params: data })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取游戏服务状态(通过后端)
|
||||||
|
* @returns 服务状态
|
||||||
|
*/
|
||||||
|
export const getGameServiceStatus = () => {
|
||||||
|
// 调用后端的/api/game/status接口
|
||||||
|
return api.get('/game/status')
|
||||||
|
}
|
||||||
152
frontend/src/api/index.ts
Normal file
152
frontend/src/api/index.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import type { AxiosInstance, InternalAxiosRequestConfig, AxiosResponse } from 'axios'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { useUserStore } from '@/store/user'
|
||||||
|
import { usePlayerStore } from '@/store/player'
|
||||||
|
|
||||||
|
// 配置默认值
|
||||||
|
const DEFAULT_BASE_URL = '/api'
|
||||||
|
const DEFAULT_TIMEOUT = 10000
|
||||||
|
|
||||||
|
// 创建axios实例(用于调用后端API)
|
||||||
|
const api: AxiosInstance = axios.create({
|
||||||
|
baseURL: DEFAULT_BASE_URL,
|
||||||
|
timeout: DEFAULT_TIMEOUT,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
// 允许跨域请求携带凭证
|
||||||
|
withCredentials: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// 创建游戏服务端API专用Axios实例(直接调用游戏服务端)
|
||||||
|
const gameApi: AxiosInstance = axios.create({
|
||||||
|
baseURL: import.meta.env.VITE_GAME_API_URL || 'http://127.0.0.1:8080/tool/http',
|
||||||
|
timeout: 15000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
// 允许跨域请求携带凭证
|
||||||
|
withCredentials: true
|
||||||
|
})
|
||||||
|
|
||||||
|
// 主API请求拦截器
|
||||||
|
api.interceptors.request.use(
|
||||||
|
(config: InternalAxiosRequestConfig) => {
|
||||||
|
// 从store获取token
|
||||||
|
const userStore = useUserStore()
|
||||||
|
if (userStore.token) {
|
||||||
|
config.headers = config.headers || {}
|
||||||
|
config.headers.Authorization = `Bearer ${userStore.token}`
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 游戏服务端API请求拦截器
|
||||||
|
gameApi.interceptors.request.use(
|
||||||
|
(config: InternalAxiosRequestConfig) => {
|
||||||
|
// 从玩家存储获取token
|
||||||
|
const playerStore = usePlayerStore()
|
||||||
|
if (playerStore.gameToken) {
|
||||||
|
config.headers = config.headers || {}
|
||||||
|
config.headers.Authorization = `Bearer ${playerStore.gameToken}`
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 主API响应拦截器
|
||||||
|
api.interceptors.response.use(
|
||||||
|
(response: AxiosResponse) => {
|
||||||
|
return response.data
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
// 处理HTTP错误
|
||||||
|
let errorMessage = '网络错误,请稍后重试'
|
||||||
|
if (error.response) {
|
||||||
|
// 服务器返回错误状态码
|
||||||
|
const { status, data } = error.response
|
||||||
|
switch (status) {
|
||||||
|
case 400:
|
||||||
|
errorMessage = data.message || '请求参数错误'
|
||||||
|
break
|
||||||
|
case 401:
|
||||||
|
errorMessage = '登录已过期,请重新登录'
|
||||||
|
// 清除登录状态
|
||||||
|
const userStore = useUserStore()
|
||||||
|
userStore.logout()
|
||||||
|
// 根据当前路径跳转到对应的登录页面
|
||||||
|
const currentPath = window.location.pathname
|
||||||
|
console.log('401错误,当前路径:', currentPath)
|
||||||
|
if (currentPath.startsWith('/admin')) {
|
||||||
|
console.log('跳转到管理员登录页')
|
||||||
|
window.location.href = '/admin/login'
|
||||||
|
} else {
|
||||||
|
console.log('跳转到玩家登录页')
|
||||||
|
window.location.href = '/player/login'
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 403:
|
||||||
|
errorMessage = '权限不足,无法访问该资源'
|
||||||
|
break
|
||||||
|
case 404:
|
||||||
|
errorMessage = '请求的资源不存在'
|
||||||
|
break
|
||||||
|
case 500:
|
||||||
|
errorMessage = '服务器内部错误,请稍后重试'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
errorMessage = data.message || `请求失败 (${status})`
|
||||||
|
}
|
||||||
|
} else if (error.request) {
|
||||||
|
// 请求已发送但未收到响应
|
||||||
|
errorMessage = '服务器无响应,请稍后重试'
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('API响应拦截器错误:', error)
|
||||||
|
ElMessage.error(errorMessage)
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 游戏服务端API响应拦截器
|
||||||
|
gameApi.interceptors.response.use(
|
||||||
|
(response: AxiosResponse) => {
|
||||||
|
// 游戏服务端API的响应格式可能与运营管理系统不同
|
||||||
|
// 直接返回响应数据,由调用方处理
|
||||||
|
return response.data
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
// 处理游戏服务端API错误
|
||||||
|
let errorMessage = '游戏服务端请求失败,请稍后重试'
|
||||||
|
if (error.response) {
|
||||||
|
const { status, data } = error.response
|
||||||
|
if (status === 401) {
|
||||||
|
errorMessage = '登录已过期,请重新登录'
|
||||||
|
// 清除玩家登录状态
|
||||||
|
const playerStore = usePlayerStore()
|
||||||
|
playerStore.logout()
|
||||||
|
// 跳转到玩家登录页面
|
||||||
|
window.location.href = '/player/login'
|
||||||
|
} else {
|
||||||
|
errorMessage = data?.message || data?.error || errorMessage
|
||||||
|
}
|
||||||
|
} else if (error.request) {
|
||||||
|
// 请求已发送但未收到响应
|
||||||
|
errorMessage = '游戏服务端无响应,请稍后重试'
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessage.error(errorMessage)
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default api
|
||||||
|
export { gameApi }
|
||||||
67
frontend/src/api/user.ts
Normal file
67
frontend/src/api/user.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import api from './index'
|
||||||
|
import type { UserQueryParams, UpdateUserForm, ChangePasswordForm } from '@/types/user'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户列表
|
||||||
|
* @param params 查询参数
|
||||||
|
* @returns 用户列表
|
||||||
|
*/
|
||||||
|
export const getUserList = (params: UserQueryParams) => {
|
||||||
|
return api.get('/users', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户详情
|
||||||
|
* @param id 用户ID
|
||||||
|
* @returns 用户详情
|
||||||
|
*/
|
||||||
|
export const getUserById = (id: number) => {
|
||||||
|
return api.get(`/users/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建用户
|
||||||
|
* @param form 用户表单数据
|
||||||
|
* @returns 创建结果
|
||||||
|
*/
|
||||||
|
export const addUser = (form: any) => {
|
||||||
|
return api.post('/users', form)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户信息
|
||||||
|
* @param id 用户ID
|
||||||
|
* @param form 更新表单数据
|
||||||
|
* @returns 更新结果
|
||||||
|
*/
|
||||||
|
export const updateUser = (id: number, form: UpdateUserForm) => {
|
||||||
|
return api.put(`/users/${id}`, form)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户密码
|
||||||
|
* @param id 用户ID
|
||||||
|
* @param form 密码更新表单数据
|
||||||
|
* @returns 更新结果
|
||||||
|
*/
|
||||||
|
export const changePassword = (id: number, form: ChangePasswordForm) => {
|
||||||
|
return api.put(`/users/${id}/password`, form)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除用户
|
||||||
|
* @param id 用户ID
|
||||||
|
* @returns 删除结果
|
||||||
|
*/
|
||||||
|
export const deleteUser = (id: number) => {
|
||||||
|
return api.delete(`/users/${id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量更新用户状态
|
||||||
|
* @param data 批量更新数据
|
||||||
|
* @returns 更新结果
|
||||||
|
*/
|
||||||
|
export const batchUpdateStatus = (data: { ids: number[], status: string }) => {
|
||||||
|
return api.put('/users/batch/status', data)
|
||||||
|
}
|
||||||
196
frontend/src/assets/main.css
Normal file
196
frontend/src/assets/main.css
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
/* 全局重置样式 */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB',
|
||||||
|
'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #303133;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 容器样式 */
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面布局样式 */
|
||||||
|
.page-container {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 主内容区样式 */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
margin: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标题样式 */
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 按钮样式 */
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10px 20px;
|
||||||
|
background-color: #409eff;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background-color: #66b1ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background-color: #67c23a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning {
|
||||||
|
background-color: #e6a23c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background-color: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表单样式 */
|
||||||
|
.form-item {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #dcdfe6;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #409eff;
|
||||||
|
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 卡片样式 */
|
||||||
|
.card {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格样式 */
|
||||||
|
.table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th,
|
||||||
|
.table td {
|
||||||
|
padding: 12px;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tr:hover {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 登录页面样式 */
|
||||||
|
.login-container {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
width: 400px;
|
||||||
|
padding: 40px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.login-form {
|
||||||
|
width: 90%;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
margin: 10px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
174
frontend/src/components/AdminHeader.vue
Normal file
174
frontend/src/components/AdminHeader.vue
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
<template>
|
||||||
|
<el-header class="admin-header">
|
||||||
|
<div class="header-content">
|
||||||
|
<div class="header-left">
|
||||||
|
<!-- 侧边栏展开/折叠按钮 -->
|
||||||
|
<div class="sidebar-toggle-btn" @click="toggleSidebar">
|
||||||
|
<RiArrowLeftSLine v-if="!uiStore.isSidebarCollapsed" />
|
||||||
|
<RiArrowRightSLine v-else />
|
||||||
|
</div>
|
||||||
|
<div class="header-title">
|
||||||
|
<!-- 页面标题插槽,允许自定义 -->
|
||||||
|
<slot name="header-title"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="user-info">
|
||||||
|
<el-dropdown @command="handleCommand">
|
||||||
|
<span class="user-profile">
|
||||||
|
<RiUserLine />
|
||||||
|
<span class="username">{{ userStore.user?.username || '管理员' }}</span>
|
||||||
|
<RiArrowDownSLine class="el-icon--right" />
|
||||||
|
</span>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item command="profile">个人中心</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useUserStore } from '@/store/user'
|
||||||
|
import { useUIStore } from '@/store/ui'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
// 导入Remix Icon组件
|
||||||
|
import { RiUserLine, RiArrowDownSLine, RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const uiStore = useUIStore()
|
||||||
|
|
||||||
|
// 切换侧边栏折叠状态
|
||||||
|
const toggleSidebar = () => {
|
||||||
|
uiStore.toggleSidebar()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理下拉菜单命令
|
||||||
|
const handleCommand = (command: string) => {
|
||||||
|
switch (command) {
|
||||||
|
case 'profile':
|
||||||
|
// TODO: 跳转到个人中心页面
|
||||||
|
ElMessage.info('个人中心功能开发中...')
|
||||||
|
break
|
||||||
|
case 'logout':
|
||||||
|
handleLogout()
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 退出登录
|
||||||
|
const handleLogout = async () => {
|
||||||
|
try {
|
||||||
|
// 清除用户信息和token
|
||||||
|
userStore.logout()
|
||||||
|
|
||||||
|
// 显示成功消息
|
||||||
|
ElMessage.success('退出登录成功')
|
||||||
|
|
||||||
|
// 跳转到登录页面
|
||||||
|
router.push('/admin/login')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('退出登录失败:', error)
|
||||||
|
ElMessage.error('退出登录失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.admin-header {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
padding: 0;
|
||||||
|
height: 60px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-toggle-btn {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-toggle-btn:hover {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-toggle-btn svg {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-profile {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-profile:hover {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-icon--right {
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.header-content {
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
304
frontend/src/components/AdminSidebar.vue
Normal file
304
frontend/src/components/AdminSidebar.vue
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
<template>
|
||||||
|
<el-aside class="admin-sidebar" :width="uiStore.isSidebarCollapsed ? '80px' : '200px'">
|
||||||
|
<div class="sidebar-header" :class="{ 'sidebar-header-collapsed': uiStore.isSidebarCollapsed }">
|
||||||
|
<div class="logo-container" v-if="!uiStore.isSidebarCollapsed">
|
||||||
|
<RiGamepadFill />
|
||||||
|
<span class="logo-text">运营管理系统</span>
|
||||||
|
</div>
|
||||||
|
<div class="logo-mini" v-else>
|
||||||
|
<RiGamepadFill />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-menu
|
||||||
|
:default-active="activeMenu"
|
||||||
|
:default-openeds="openedMenus"
|
||||||
|
class="admin-menu"
|
||||||
|
:collapse="uiStore.isSidebarCollapsed"
|
||||||
|
:collapse-transition="false"
|
||||||
|
router
|
||||||
|
mode="vertical"
|
||||||
|
>
|
||||||
|
<el-menu-item index="/admin">
|
||||||
|
<RiDashboardLine />
|
||||||
|
<span>工作台</span>
|
||||||
|
</el-menu-item>
|
||||||
|
|
||||||
|
<el-sub-menu index="player-management">
|
||||||
|
<template #title>
|
||||||
|
<RiUserLine />
|
||||||
|
<span>玩家管理</span>
|
||||||
|
</template>
|
||||||
|
<el-menu-item index="/admin/players">
|
||||||
|
<RiUserLine />
|
||||||
|
<span>玩家列表</span>
|
||||||
|
</el-menu-item>
|
||||||
|
</el-sub-menu>
|
||||||
|
|
||||||
|
<el-sub-menu index="system-management">
|
||||||
|
<template #title>
|
||||||
|
<RiSettings2Line />
|
||||||
|
<span>系统管理</span>
|
||||||
|
</template>
|
||||||
|
<el-menu-item index="/admin/users">
|
||||||
|
<RiUserSettingsLine />
|
||||||
|
<span>用户管理</span>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/admin/config">
|
||||||
|
<RiSettings2Line />
|
||||||
|
<span>系统配置</span>
|
||||||
|
</el-menu-item>
|
||||||
|
<el-menu-item index="/admin/game">
|
||||||
|
<RiServerLine />
|
||||||
|
<span>游戏服务</span>
|
||||||
|
</el-menu-item>
|
||||||
|
</el-sub-menu>
|
||||||
|
</el-menu>
|
||||||
|
</el-aside>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { useUIStore } from '@/store/ui'
|
||||||
|
// 导入Remix Icon组件
|
||||||
|
import {
|
||||||
|
RiGamepadFill,
|
||||||
|
RiDashboardLine,
|
||||||
|
RiSettings2Line,
|
||||||
|
RiUserSettingsLine,
|
||||||
|
RiServerLine,
|
||||||
|
RiUserLine
|
||||||
|
} from '@remixicon/vue'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const uiStore = useUIStore()
|
||||||
|
|
||||||
|
// 计算当前激活的菜单
|
||||||
|
const activeMenu = computed(() => {
|
||||||
|
return route.path
|
||||||
|
})
|
||||||
|
|
||||||
|
// 计算当前应该展开的子菜单
|
||||||
|
const openedMenus = computed(() => {
|
||||||
|
const path = route.path
|
||||||
|
const menus = []
|
||||||
|
|
||||||
|
// 根据当前路由判断应该展开的子菜单
|
||||||
|
if (path.startsWith('/admin/users') || path.startsWith('/admin/config') || path.startsWith('/admin/game')) {
|
||||||
|
menus.push('system-management')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 玩家管理菜单展开条件
|
||||||
|
if (path.startsWith('/admin/players')) {
|
||||||
|
menus.push('player-management')
|
||||||
|
}
|
||||||
|
|
||||||
|
return menus
|
||||||
|
})
|
||||||
|
|
||||||
|
// 侧边栏折叠状态由顶部导航栏控制,不需要本地切换函数
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.admin-sidebar {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-right: 1px solid #e4e7ed;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
height: 60px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 16px;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 侧边栏折叠时,调整头部布局使logo居中 */
|
||||||
|
.sidebar-header-collapsed {
|
||||||
|
justify-content: center !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-container i {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-mini {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-mini i {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-menu {
|
||||||
|
flex: 1;
|
||||||
|
border-right: none;
|
||||||
|
background-color: transparent;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-menu .el-menu-item {
|
||||||
|
height: 50px;
|
||||||
|
line-height: 50px;
|
||||||
|
margin: 3px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0 16px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-menu .el-menu-item:hover {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-menu .el-menu-item.is-active {
|
||||||
|
background-color: #e6f2ff;
|
||||||
|
color: #409eff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-menu .el-menu-item i,
|
||||||
|
.admin-menu .el-menu-item svg {
|
||||||
|
font-size: 20px;
|
||||||
|
color: inherit;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-menu--vertical) {
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-menu-item) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-sub-menu__title) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
height: 50px;
|
||||||
|
line-height: 50px;
|
||||||
|
margin: 3px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0 16px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-sub-menu__title i),
|
||||||
|
:deep(.el-sub-menu__title svg) {
|
||||||
|
font-size: 20px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-sub-menu__title:hover) {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 保持折叠状态下图标大小与展开时一致 */
|
||||||
|
:deep(.el-menu--collapse .el-menu-item) svg,
|
||||||
|
:deep(.el-menu--collapse .el-sub-menu__title) svg {
|
||||||
|
font-size: 20px !important;
|
||||||
|
width: 20px !important;
|
||||||
|
height: 20px !important;
|
||||||
|
line-height: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保折叠状态下图标居中显示 */
|
||||||
|
:deep(.el-menu--collapse .el-menu-item),
|
||||||
|
:deep(.el-menu--collapse .el-sub-menu__title) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 覆盖Element Plus折叠状态的默认样式 */
|
||||||
|
:deep(.el-menu--collapse) {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-menu--collapse .el-menu-item),
|
||||||
|
:deep(.el-menu--collapse .el-sub-menu__title) {
|
||||||
|
width: 64px; /* 保持与菜单项相同的宽度,确保边框完整显示 */
|
||||||
|
margin: 3px 8px !important; /* 保持外边距,确保有空间显示边框 */
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保折叠状态下不显示文字 */
|
||||||
|
:deep(.el-menu--collapse .el-menu-item span),
|
||||||
|
:deep(.el-menu--collapse .el-sub-menu__title span) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修复一级菜单展开箭头的垂直对齐问题 */
|
||||||
|
:deep(.el-sub-menu__title) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
height: 50px;
|
||||||
|
line-height: 50px;
|
||||||
|
margin: 3px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0 16px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保展开箭头图标与文字在同一水平线上 */
|
||||||
|
:deep(.el-sub-menu__icon-arrow) {
|
||||||
|
position: static !important;
|
||||||
|
margin-left: auto !important;
|
||||||
|
margin-top: 0 !important;
|
||||||
|
transform: none !important;
|
||||||
|
vertical-align: middle !important;
|
||||||
|
display: inline-flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
width: 18px !important;
|
||||||
|
height: 18px !important;
|
||||||
|
font-size: 18px !important;
|
||||||
|
line-height: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 侧边栏折叠状态下隐藏展开箭头 */
|
||||||
|
.el-menu--collapse :deep(.el-sub-menu__icon-arrow) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 当子菜单展开时的箭头旋转 */
|
||||||
|
:deep(.el-sub-menu.is-opened > .el-sub-menu__title .el-sub-menu__icon-arrow) {
|
||||||
|
transform: rotate(180deg) !important;
|
||||||
|
transition: transform 0.3s ease !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
268
frontend/src/components/TabNav.vue
Normal file
268
frontend/src/components/TabNav.vue
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
<template>
|
||||||
|
<div class="tab-nav-container">
|
||||||
|
<div class="tab-nav-wrapper">
|
||||||
|
<div
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:key="tab.path"
|
||||||
|
class="tab-item"
|
||||||
|
:class="{ 'active': tab.path === currentPath }"
|
||||||
|
@click="switchTab(tab)"
|
||||||
|
:draggable="tab.path !== '/admin'"
|
||||||
|
@dragstart="handleDragStart($event, tab)"
|
||||||
|
@dragover="handleDragOver($event)"
|
||||||
|
@drop="handleDrop($event, tab)"
|
||||||
|
>
|
||||||
|
<span class="tab-title">{{ tab.title }}</span>
|
||||||
|
<!-- 工作台标签没有关闭按钮 -->
|
||||||
|
<button
|
||||||
|
v-if="tab.path !== '/admin'"
|
||||||
|
class="tab-close-btn"
|
||||||
|
@click.stop="closeTab(tab)"
|
||||||
|
:title="`关闭 ${tab.title}`"
|
||||||
|
>
|
||||||
|
<RiCloseLine />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch, onMounted } from 'vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { useUIStore } from '@/store/ui'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { RiCloseLine } from '@remixicon/vue'
|
||||||
|
|
||||||
|
interface TabItem {
|
||||||
|
path: string
|
||||||
|
title: string
|
||||||
|
name?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const uiStore = useUIStore()
|
||||||
|
|
||||||
|
// 当前路径
|
||||||
|
const currentPath = computed(() => route.path)
|
||||||
|
|
||||||
|
// 标签列表
|
||||||
|
const tabs = computed(() => uiStore.tabs)
|
||||||
|
|
||||||
|
// 切换标签
|
||||||
|
const switchTab = (tab: TabItem) => {
|
||||||
|
if (tab.path !== currentPath.value) {
|
||||||
|
router.push(tab.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭标签
|
||||||
|
const closeTab = (tab: TabItem) => {
|
||||||
|
if (tabs.value.length <= 1) {
|
||||||
|
ElMessage.warning('至少需要保留一个标签页')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabIndex = tabs.value.findIndex(t => t.path === tab.path)
|
||||||
|
let nextPath = ''
|
||||||
|
|
||||||
|
// 确定关闭标签后的跳转路径
|
||||||
|
if (tab.path === currentPath.value) {
|
||||||
|
// 关闭当前激活的标签
|
||||||
|
if (tabIndex === tabs.value.length - 1) {
|
||||||
|
// 如果是最后一个标签,跳转到前一个
|
||||||
|
nextPath = tabs.value[tabIndex - 1].path
|
||||||
|
} else {
|
||||||
|
// 否则跳转到下一个
|
||||||
|
nextPath = tabs.value[tabIndex + 1].path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从标签列表中移除
|
||||||
|
uiStore.removeTab(tab.path)
|
||||||
|
|
||||||
|
// 如果需要跳转
|
||||||
|
if (nextPath) {
|
||||||
|
router.push(nextPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新当前标签功能已移除
|
||||||
|
|
||||||
|
// 拖拽相关功能
|
||||||
|
const draggedTab = ref<TabItem | null>(null)
|
||||||
|
|
||||||
|
const handleDragStart = (_: DragEvent, tab: TabItem) => {
|
||||||
|
draggedTab.value = tab
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDragOver = (event: DragEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDrop = (event: DragEvent, targetTab: TabItem) => {
|
||||||
|
event.preventDefault()
|
||||||
|
if (draggedTab.value && draggedTab.value.path !== targetTab.path) {
|
||||||
|
uiStore.reorderTabs(draggedTab.value.path, targetTab.path)
|
||||||
|
}
|
||||||
|
draggedTab.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听路由变化,确保标签列表与当前路由一致
|
||||||
|
watch(
|
||||||
|
() => route.path,
|
||||||
|
(newPath) => {
|
||||||
|
// 当路由变化时,确保当前路径在标签列表中
|
||||||
|
if (!tabs.value.some(tab => tab.path === newPath)) {
|
||||||
|
// 如果不在列表中,添加新标签
|
||||||
|
uiStore.addTab({
|
||||||
|
path: newPath,
|
||||||
|
title: route.meta.title as string || '未命名页面',
|
||||||
|
name: route.name as string || ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
// 组件初始化时,确保工作台标签始终存在
|
||||||
|
onMounted(() => {
|
||||||
|
// 检查工作台标签是否存在
|
||||||
|
if (!tabs.value.some(tab => tab.path === '/admin')) {
|
||||||
|
// 如果不存在,添加工作台标签
|
||||||
|
uiStore.addTab({
|
||||||
|
path: '/admin',
|
||||||
|
title: '工作台',
|
||||||
|
name: 'admin'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tab-nav-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 36px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
padding: 0 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0; /* 防止被flex容器压缩 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-nav-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-nav-wrapper::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 16px;
|
||||||
|
height: 32px;
|
||||||
|
margin-right: 2px;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item:hover {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item.active {
|
||||||
|
background-color: #409eff;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-title {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item.active .tab-title {
|
||||||
|
color: #ffffff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-close-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #606266;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 50%;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 1;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item.active .tab-close-btn {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-close-btn:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-nav-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #606266;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn:hover {
|
||||||
|
background-color: #ecf5ff;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.tab-item {
|
||||||
|
padding: 0 12px;
|
||||||
|
margin-right: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-title {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
33
frontend/src/main.ts
Normal file
33
frontend/src/main.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import zhCn from 'element-plus/es/locale/lang/zh-cn' // 导入Element Plus中文语言包
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
|
||||||
|
import App from './App.vue'
|
||||||
|
import router from './router'
|
||||||
|
import './assets/main.css'
|
||||||
|
import { useUserStore } from '@/store/user'
|
||||||
|
import { usePlayerStore } from '@/store/player'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.use(createPinia())
|
||||||
|
app.use(router)
|
||||||
|
|
||||||
|
// 配置Element Plus使用中文语言
|
||||||
|
app.use(ElementPlus, {
|
||||||
|
locale: zhCn // 设置中文语言包
|
||||||
|
})
|
||||||
|
|
||||||
|
// 应用启动时恢复用户状态
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const playerStore = usePlayerStore()
|
||||||
|
|
||||||
|
// 恢复运营管理系统用户状态
|
||||||
|
userStore.recoverUser()
|
||||||
|
|
||||||
|
// 恢复玩家服务中心用户状态
|
||||||
|
playerStore.recoverPlayer()
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
158
frontend/src/router/index.ts
Normal file
158
frontend/src/router/index.ts
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
|
|
||||||
|
const routes: Array<RouteRecordRaw> = [
|
||||||
|
// 玩家服务中心路由
|
||||||
|
{
|
||||||
|
path: '/player/login',
|
||||||
|
name: 'PlayerLogin',
|
||||||
|
component: () => import('@/views/player/Login.vue'),
|
||||||
|
meta: { title: '登录' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/player',
|
||||||
|
name: 'PlayerHome',
|
||||||
|
component: () => import('@/views/player/Home.vue'),
|
||||||
|
meta: { title: '玩家服务中心', requiresAuth: true }
|
||||||
|
},
|
||||||
|
|
||||||
|
// 运营管理系统路由
|
||||||
|
{
|
||||||
|
path: '/admin/login',
|
||||||
|
name: 'AdminLogin',
|
||||||
|
component: () => import('@/views/admin/Login.vue'),
|
||||||
|
meta: { title: '登录' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/admin',
|
||||||
|
name: 'AdminHome',
|
||||||
|
component: () => import('@/views/admin/Home.vue'),
|
||||||
|
meta: { title: '工作台', requiresAuth: true, isAdmin: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/users',
|
||||||
|
name: 'UserManagement',
|
||||||
|
component: () => import('@/views/admin/UserManagement.vue'),
|
||||||
|
meta: { title: '用户管理', requiresAuth: true, isAdmin: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/config',
|
||||||
|
name: 'SystemConfig',
|
||||||
|
component: () => import('@/views/admin/SystemConfig.vue'),
|
||||||
|
meta: { title: '系统配置', requiresAuth: true, isAdmin: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/game',
|
||||||
|
name: 'GameService',
|
||||||
|
component: () => import('@/views/admin/GameService.vue'),
|
||||||
|
meta: { title: '游戏服务', requiresAuth: true, isAdmin: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/players',
|
||||||
|
name: 'PlayerList',
|
||||||
|
component: () => import('@/views/admin/PlayerList.vue'),
|
||||||
|
meta: { title: '玩家列表', requiresAuth: true, isAdmin: true }
|
||||||
|
},
|
||||||
|
|
||||||
|
// 默认路由重定向
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
redirect: '/player'
|
||||||
|
},
|
||||||
|
|
||||||
|
// 404路由
|
||||||
|
{
|
||||||
|
path: '/:pathMatch(.*)*',
|
||||||
|
name: 'NotFound',
|
||||||
|
component: () => import('@/views/NotFoundView.vue'),
|
||||||
|
meta: { title: '页面不存在' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(import.meta.env.BASE_URL as string),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
// 路由守卫
|
||||||
|
router.beforeEach((to, _from, next) => {
|
||||||
|
// 设置页面标题
|
||||||
|
const pageTitle = to.meta.title as string || '首页'
|
||||||
|
let systemName = '梦幻西游一站式运营管理平台'
|
||||||
|
|
||||||
|
// 根据路由路径判断系统类型
|
||||||
|
if (to.path.startsWith('/admin')) {
|
||||||
|
systemName = '运营管理系统'
|
||||||
|
} else if (to.path.startsWith('/player')) {
|
||||||
|
systemName = '玩家服务中心'
|
||||||
|
}
|
||||||
|
|
||||||
|
document.title = `${pageTitle} - ${systemName}`
|
||||||
|
|
||||||
|
// 检查是否需要认证
|
||||||
|
if (to.meta.requiresAuth) {
|
||||||
|
if (to.path.startsWith('/admin')) {
|
||||||
|
// 运营管理系统路由 - 使用管理员认证
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
const userStr = localStorage.getItem('user')
|
||||||
|
const user = userStr ? JSON.parse(userStr) : null
|
||||||
|
|
||||||
|
console.log('管理员认证检查:', {
|
||||||
|
path: to.path,
|
||||||
|
hasToken: !!token,
|
||||||
|
hasUser: !!user,
|
||||||
|
userRole: user?.role
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!token || !user) {
|
||||||
|
// 管理员未登录,跳转到管理员登录页
|
||||||
|
console.log('管理员未登录,跳转到管理员登录页')
|
||||||
|
next('/admin/login')
|
||||||
|
} else if (to.meta.isAdmin && user.role !== 'admin') {
|
||||||
|
// 需要管理员权限,但不是管理员
|
||||||
|
console.log('权限不足,跳转到管理员首页')
|
||||||
|
next('/admin')
|
||||||
|
} else {
|
||||||
|
// 管理员已登录且权限符合,继续访问
|
||||||
|
console.log('管理员已登录且权限符合,继续访问')
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 玩家服务中心路由 - 使用玩家认证
|
||||||
|
const gameToken = localStorage.getItem('gameToken')
|
||||||
|
const playerStr = localStorage.getItem('player')
|
||||||
|
const player = playerStr ? JSON.parse(playerStr) : null
|
||||||
|
|
||||||
|
console.log('玩家认证检查:', {
|
||||||
|
path: to.path,
|
||||||
|
hasGameToken: !!gameToken,
|
||||||
|
hasPlayer: !!player
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!gameToken || !player) {
|
||||||
|
// 玩家未登录,跳转到玩家登录页
|
||||||
|
console.log('玩家未登录,跳转到玩家登录页')
|
||||||
|
next('/player/login')
|
||||||
|
} else {
|
||||||
|
// 玩家已登录,继续访问
|
||||||
|
console.log('玩家已登录,继续访问')
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 不需要认证,直接访问
|
||||||
|
console.log('不需要认证,直接访问')
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 路由后置守卫 - 用于标签导航管理
|
||||||
|
router.afterEach((to) => {
|
||||||
|
// 只在运营管理系统中添加标签
|
||||||
|
if (to.path.startsWith('/admin') && !to.path.startsWith('/admin/login')) {
|
||||||
|
// 由于路由守卫在Pinia实例创建之前执行,我们需要在组件内部处理标签添加
|
||||||
|
// 标签添加逻辑已在TabNav组件中实现
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
72
frontend/src/store/config.ts
Normal file
72
frontend/src/store/config.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { configApi } from '@/api/config'
|
||||||
|
|
||||||
|
export const useConfigStore = defineStore('config', {
|
||||||
|
state: () => ({
|
||||||
|
configs: {} as Record<string, any>,
|
||||||
|
loading: false,
|
||||||
|
error: null as unknown
|
||||||
|
}),
|
||||||
|
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
/**
|
||||||
|
* 获取所有配置
|
||||||
|
* @returns Promise<{ success: boolean; data: any }>
|
||||||
|
*/
|
||||||
|
async getAllConfigs() {
|
||||||
|
this.loading = true
|
||||||
|
this.error = null
|
||||||
|
try {
|
||||||
|
const response = await configApi.getAllConfigs()
|
||||||
|
this.configs = response.data
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
this.error = error
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新单个配置项
|
||||||
|
* @param configData 配置数据,包含key和value字段
|
||||||
|
* @returns Promise<{ success: boolean; message: string }>
|
||||||
|
*/
|
||||||
|
async updateConfig(configData: { key: string; value: any }) {
|
||||||
|
this.loading = true
|
||||||
|
this.error = null
|
||||||
|
try {
|
||||||
|
const response = await configApi.updateConfig(configData.key, configData.value)
|
||||||
|
// 更新本地状态
|
||||||
|
this.configs[configData.key] = configData.value
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
this.error = error
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单个配置项
|
||||||
|
* @param key 配置键
|
||||||
|
* @returns Promise<{ success: boolean; data: any }>
|
||||||
|
*/
|
||||||
|
async getConfig(key: string) {
|
||||||
|
this.loading = true
|
||||||
|
this.error = null
|
||||||
|
try {
|
||||||
|
const response = await configApi.getConfig(key)
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
this.error = error
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
5
frontend/src/store/index.ts
Normal file
5
frontend/src/store/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { createPinia } from 'pinia'
|
||||||
|
|
||||||
|
export const pinia = createPinia()
|
||||||
|
|
||||||
|
export default pinia
|
||||||
149
frontend/src/store/player.ts
Normal file
149
frontend/src/store/player.ts
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import type { User } from '@/types/user'
|
||||||
|
import { playerLogin, playerLogout, getPlayerInfo } from '@/api/auth'
|
||||||
|
import type { LoginForm } from '@/types/auth'
|
||||||
|
|
||||||
|
export const usePlayerStore = defineStore('player', {
|
||||||
|
state: () => ({
|
||||||
|
player: null as User | null,
|
||||||
|
gameToken: localStorage.getItem('gameToken') || '',
|
||||||
|
loading: false
|
||||||
|
}),
|
||||||
|
|
||||||
|
getters: {
|
||||||
|
isLoggedIn: (state) => !!state.gameToken,
|
||||||
|
playerName: (state) => state.player?.username || ''
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
// 玩家登录(游戏服务端API)
|
||||||
|
async login(form: LoginForm) {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
// 调用游戏服务端API登录
|
||||||
|
const response = await playerLogin(form)
|
||||||
|
|
||||||
|
// 检查响应是否成功(游戏服务端返回code=200表示成功)
|
||||||
|
if (response?.code === 200) {
|
||||||
|
// 保存游戏token和初始玩家信息
|
||||||
|
const token = response?.data
|
||||||
|
const player = {
|
||||||
|
id: 0, // 游戏服务端可能不返回id,使用默认值
|
||||||
|
username: form.username,
|
||||||
|
role: 'player' as const, // 玩家角色固定为player
|
||||||
|
status: 'ACTIVE' as const, // 设置用户状态为激活
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
this.gameToken = token
|
||||||
|
this.player = player
|
||||||
|
// 保存到localStorage,使用独立的key
|
||||||
|
localStorage.setItem('gameToken', token)
|
||||||
|
localStorage.setItem('player', JSON.stringify(player))
|
||||||
|
|
||||||
|
// 登录成功后,调用获取玩家信息的API更新真实玩家信息
|
||||||
|
await this.getPlayerInfo()
|
||||||
|
} else {
|
||||||
|
throw new Error('登录失败,未获取到token')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(response?.message || '登录失败,请稍后重试')
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 玩家登出
|
||||||
|
async logout() {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
// 调用游戏服务端登出API
|
||||||
|
await playerLogout()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('玩家登出API调用失败:', error)
|
||||||
|
} finally {
|
||||||
|
// 无论API调用是否成功,都要清除玩家状态
|
||||||
|
this.gameToken = ''
|
||||||
|
this.player = null
|
||||||
|
localStorage.removeItem('gameToken')
|
||||||
|
localStorage.removeItem('player')
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 从localStorage恢复玩家信息
|
||||||
|
recoverPlayer() {
|
||||||
|
const gameToken = localStorage.getItem('gameToken')
|
||||||
|
const playerStr = localStorage.getItem('player')
|
||||||
|
if (gameToken && playerStr) {
|
||||||
|
this.gameToken = gameToken
|
||||||
|
this.player = JSON.parse(playerStr)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取当前玩家信息(从游戏服务端API)
|
||||||
|
async getPlayerInfo() {
|
||||||
|
// 只有在有游戏Token的情况下才调用API
|
||||||
|
if (!this.gameToken) return
|
||||||
|
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
const response = await getPlayerInfo()
|
||||||
|
|
||||||
|
// 检查响应是否成功(游戏服务端返回code=200表示成功)
|
||||||
|
if ((response as any)?.code === 200 && (response as any)?.success === true) {
|
||||||
|
// 从游戏服务端获取玩家信息
|
||||||
|
const playerData = (response as any)?.data
|
||||||
|
|
||||||
|
// 更新玩家信息
|
||||||
|
if (playerData && this.player) {
|
||||||
|
this.player = {
|
||||||
|
...this.player,
|
||||||
|
username: playerData.username || this.player.username,
|
||||||
|
// 可以根据游戏服务端返回的实际字段扩展
|
||||||
|
// 例如:id: playerData.id, status: playerData.status等
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新本地存储
|
||||||
|
localStorage.setItem('player', JSON.stringify(this.player))
|
||||||
|
} else if (playerData && !this.player) {
|
||||||
|
// 如果本地没有玩家信息,创建新的玩家对象
|
||||||
|
this.player = {
|
||||||
|
id: 0, // 使用默认值或游戏服务端返回的ID
|
||||||
|
username: playerData.username || '',
|
||||||
|
role: 'player' as const,
|
||||||
|
status: 'ACTIVE' as const,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新本地存储
|
||||||
|
localStorage.setItem('player', JSON.stringify(this.player))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取玩家信息失败:', error)
|
||||||
|
return null
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 清除玩家状态(不调用API)
|
||||||
|
clearPlayer() {
|
||||||
|
this.gameToken = ''
|
||||||
|
this.player = null
|
||||||
|
localStorage.removeItem('gameToken')
|
||||||
|
localStorage.removeItem('player')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
86
frontend/src/store/ui.ts
Normal file
86
frontend/src/store/ui.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
interface TabItem {
|
||||||
|
path: string
|
||||||
|
title: string
|
||||||
|
name?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUIStore = defineStore('ui', {
|
||||||
|
state: () => ({
|
||||||
|
sidebarCollapsed: false, // 侧边栏折叠状态,默认展开
|
||||||
|
tabs: [] as TabItem[] // 标签列表
|
||||||
|
}),
|
||||||
|
|
||||||
|
getters: {
|
||||||
|
isSidebarCollapsed: (state) => state.sidebarCollapsed
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
// 切换侧边栏折叠状态
|
||||||
|
toggleSidebar() {
|
||||||
|
this.sidebarCollapsed = !this.sidebarCollapsed
|
||||||
|
},
|
||||||
|
|
||||||
|
// 设置侧边栏折叠状态
|
||||||
|
setSidebarCollapsed(collapsed: boolean) {
|
||||||
|
this.sidebarCollapsed = collapsed
|
||||||
|
},
|
||||||
|
|
||||||
|
// 添加标签
|
||||||
|
addTab(tab: TabItem) {
|
||||||
|
// 检查标签是否已存在
|
||||||
|
const existingTab = this.tabs.find(t => t.path === tab.path)
|
||||||
|
if (!existingTab) {
|
||||||
|
// 如果是工作台标签,确保它始终在第一位
|
||||||
|
if (tab.path === '/admin') {
|
||||||
|
// 如果工作台标签不存在,添加到数组开头
|
||||||
|
this.tabs.unshift(tab)
|
||||||
|
} else {
|
||||||
|
// 其他标签添加到数组中,保持工作台在第一位
|
||||||
|
this.tabs.push(tab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 移除标签
|
||||||
|
removeTab(path: string) {
|
||||||
|
// 工作台标签不能被删除
|
||||||
|
if (path === '/admin') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = this.tabs.findIndex(t => t.path === path)
|
||||||
|
if (index > -1) {
|
||||||
|
this.tabs.splice(index, 1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 重新排序标签
|
||||||
|
reorderTabs(fromPath: string, toPath: string) {
|
||||||
|
// 工作台标签不能被重新排序
|
||||||
|
if (fromPath === '/admin' || toPath === '/admin') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromIndex = this.tabs.findIndex(t => t.path === fromPath)
|
||||||
|
const toIndex = this.tabs.findIndex(t => t.path === toPath)
|
||||||
|
|
||||||
|
if (fromIndex > -1 && toIndex > -1 && fromIndex !== toIndex) {
|
||||||
|
// 移动标签
|
||||||
|
const [removedTab] = this.tabs.splice(fromIndex, 1)
|
||||||
|
this.tabs.splice(toIndex, 0, removedTab)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 清空所有标签(保留首页)
|
||||||
|
clearTabs(keepPath: string = '/admin') {
|
||||||
|
this.tabs = this.tabs.filter(t => t.path === keepPath)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 设置标签列表
|
||||||
|
setTabs(tabs: TabItem[]) {
|
||||||
|
this.tabs = tabs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
85
frontend/src/store/user.ts
Normal file
85
frontend/src/store/user.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import type { User } from '@/types/user'
|
||||||
|
import { login, logout, playerLogout } from '@/api/auth'
|
||||||
|
import type { LoginForm } from '@/types/auth'
|
||||||
|
|
||||||
|
export const useUserStore = defineStore('user', {
|
||||||
|
state: () => ({
|
||||||
|
user: null as User | null,
|
||||||
|
token: localStorage.getItem('token') || '',
|
||||||
|
loading: false
|
||||||
|
}),
|
||||||
|
|
||||||
|
getters: {
|
||||||
|
isLoggedIn: (state) => !!state.token,
|
||||||
|
isAdmin: (state) => state.user?.role === 'admin',
|
||||||
|
userName: (state) => state.user?.username || ''
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
// 用户登录(运营管理系统)
|
||||||
|
async login(form: LoginForm) {
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
const response = await login(form)
|
||||||
|
this.token = response.data.token
|
||||||
|
this.user = response.data.user
|
||||||
|
// 保存到localStorage
|
||||||
|
localStorage.setItem('token', response.data.token)
|
||||||
|
localStorage.setItem('user', JSON.stringify(response.data.user))
|
||||||
|
return response
|
||||||
|
} catch (error) {
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 设置用户信息
|
||||||
|
setUser(user: User) {
|
||||||
|
this.user = user
|
||||||
|
// 保存到localStorage
|
||||||
|
localStorage.setItem('user', JSON.stringify(user))
|
||||||
|
},
|
||||||
|
|
||||||
|
// 设置token
|
||||||
|
setToken(token: string) {
|
||||||
|
this.token = token
|
||||||
|
// 保存到localStorage
|
||||||
|
localStorage.setItem('token', token)
|
||||||
|
},
|
||||||
|
|
||||||
|
// 用户登出
|
||||||
|
async logout() {
|
||||||
|
try {
|
||||||
|
// 根据用户角色调用不同的登出接口
|
||||||
|
if (this.isAdmin) {
|
||||||
|
// 管理员调用运营管理系统的登出接口
|
||||||
|
await logout()
|
||||||
|
} else {
|
||||||
|
// 玩家调用游戏服务端的登出接口
|
||||||
|
await playerLogout()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('登出失败:', error)
|
||||||
|
} finally {
|
||||||
|
// 清除状态
|
||||||
|
this.token = ''
|
||||||
|
this.user = null
|
||||||
|
// 清除localStorage
|
||||||
|
localStorage.removeItem('token')
|
||||||
|
localStorage.removeItem('user')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 从localStorage恢复用户信息
|
||||||
|
recoverUser() {
|
||||||
|
const token = localStorage.getItem('token')
|
||||||
|
const userStr = localStorage.getItem('user')
|
||||||
|
if (token && userStr) {
|
||||||
|
this.token = token
|
||||||
|
this.user = JSON.parse(userStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
41
frontend/src/types/auth.ts
Normal file
41
frontend/src/types/auth.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import type { User } from './user'
|
||||||
|
|
||||||
|
// 登录表单接口
|
||||||
|
export interface LoginForm {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册表单接口
|
||||||
|
export interface RegisterForm {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
confirmPassword: string
|
||||||
|
email?: string
|
||||||
|
phone?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登录响应接口
|
||||||
|
export interface LoginResponse {
|
||||||
|
success: boolean
|
||||||
|
message: string
|
||||||
|
data: {
|
||||||
|
token: string
|
||||||
|
user: User
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册响应接口
|
||||||
|
export interface RegisterResponse {
|
||||||
|
success: boolean
|
||||||
|
message: string
|
||||||
|
data: {
|
||||||
|
user: User
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登出响应接口
|
||||||
|
export interface LogoutResponse {
|
||||||
|
success: boolean
|
||||||
|
message: string
|
||||||
|
}
|
||||||
22
frontend/src/types/config.ts
Normal file
22
frontend/src/types/config.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// 配置项接口
|
||||||
|
export interface Config {
|
||||||
|
id: number
|
||||||
|
key: string
|
||||||
|
value: string
|
||||||
|
description?: string
|
||||||
|
createdAt: string
|
||||||
|
updatedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 配置查询参数接口
|
||||||
|
export interface ConfigQueryParams {
|
||||||
|
key?: string
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 配置创建/更新表单接口
|
||||||
|
export interface ConfigForm {
|
||||||
|
key: string
|
||||||
|
value: string
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
50
frontend/src/types/user.ts
Normal file
50
frontend/src/types/user.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// 用户角色类型
|
||||||
|
export type UserRole = 'admin' | 'player'
|
||||||
|
|
||||||
|
// 用户状态类型
|
||||||
|
export type UserStatus = 'ACTIVE' | 'INACTIVE'
|
||||||
|
|
||||||
|
// 用户信息接口
|
||||||
|
export interface User {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
email?: string
|
||||||
|
phone?: string
|
||||||
|
role: UserRole
|
||||||
|
status: UserStatus
|
||||||
|
createdAt: string
|
||||||
|
updatedAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户查询参数接口
|
||||||
|
export interface UserQueryParams {
|
||||||
|
page?: number
|
||||||
|
limit?: number
|
||||||
|
username?: string
|
||||||
|
email?: string
|
||||||
|
phone?: string
|
||||||
|
role?: UserRole
|
||||||
|
status?: UserStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用户更新表单接口
|
||||||
|
export interface UpdateUserForm {
|
||||||
|
username?: string
|
||||||
|
email?: string
|
||||||
|
phone?: string
|
||||||
|
role?: UserRole
|
||||||
|
status?: UserStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
// 密码更新表单接口
|
||||||
|
export interface ChangePasswordForm {
|
||||||
|
oldPassword: string
|
||||||
|
newPassword: string
|
||||||
|
confirmPassword: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量更新状态表单接口
|
||||||
|
export interface BatchUpdateStatusForm {
|
||||||
|
ids: number[]
|
||||||
|
status: UserStatus
|
||||||
|
}
|
||||||
123
frontend/src/utils/helpers.ts
Normal file
123
frontend/src/utils/helpers.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
/**
|
||||||
|
* 格式化日期时间
|
||||||
|
* @param date 日期对象或日期字符串
|
||||||
|
* @param format 格式化字符串,默认 'YYYY-MM-DD HH:mm:ss'
|
||||||
|
* @returns 格式化后的日期字符串
|
||||||
|
*/
|
||||||
|
export const formatDate = (date: Date | string, format: string = 'YYYY-MM-DD HH:mm:ss'): string => {
|
||||||
|
const d = typeof date === 'string' ? new Date(date) : date
|
||||||
|
|
||||||
|
const year = d.getFullYear()
|
||||||
|
const month = String(d.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(d.getDate()).padStart(2, '0')
|
||||||
|
const hours = String(d.getHours()).padStart(2, '0')
|
||||||
|
const minutes = String(d.getMinutes()).padStart(2, '0')
|
||||||
|
const seconds = String(d.getSeconds()).padStart(2, '0')
|
||||||
|
|
||||||
|
return format
|
||||||
|
.replace('YYYY', String(year))
|
||||||
|
.replace('MM', month)
|
||||||
|
.replace('DD', day)
|
||||||
|
.replace('HH', hours)
|
||||||
|
.replace('mm', minutes)
|
||||||
|
.replace('ss', seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证密码强度
|
||||||
|
* @param password 密码字符串
|
||||||
|
* @returns 密码强度结果对象
|
||||||
|
*/
|
||||||
|
export const validatePassword = (password: string): { strong: boolean; message: string } => {
|
||||||
|
if (password.length < 6) {
|
||||||
|
return { strong: false, message: '密码长度不能少于6位' }
|
||||||
|
}
|
||||||
|
if (password.length > 20) {
|
||||||
|
return { strong: false, message: '密码长度不能超过20位' }
|
||||||
|
}
|
||||||
|
if (!/[A-Za-z]/.test(password)) {
|
||||||
|
return { strong: false, message: '密码必须包含字母' }
|
||||||
|
}
|
||||||
|
if (!/[0-9]/.test(password)) {
|
||||||
|
return { strong: false, message: '密码必须包含数字' }
|
||||||
|
}
|
||||||
|
return { strong: true, message: '密码强度符合要求' }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成唯一ID
|
||||||
|
* @returns 唯一ID字符串
|
||||||
|
*/
|
||||||
|
export const generateId = (): string => {
|
||||||
|
return Date.now().toString(36) + Math.random().toString(36).substr(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 防抖函数
|
||||||
|
* @param func 要执行的函数
|
||||||
|
* @param wait 等待时间(毫秒)
|
||||||
|
* @returns 防抖处理后的函数
|
||||||
|
*/
|
||||||
|
export const debounce = <T extends (...args: any[]) => any>(func: T, wait: number): ((...args: Parameters<T>) => void) => {
|
||||||
|
let timeout: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
return (...args: Parameters<T>) => {
|
||||||
|
if (timeout) {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
func(...args)
|
||||||
|
}, wait)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 节流函数
|
||||||
|
* @param func 要执行的函数
|
||||||
|
* @param limit 限制时间(毫秒)
|
||||||
|
* @returns 节流处理后的函数
|
||||||
|
*/
|
||||||
|
export const throttle = <T extends (...args: any[]) => any>(func: T, limit: number): ((...args: Parameters<T>) => void) => {
|
||||||
|
let inThrottle: boolean = false
|
||||||
|
|
||||||
|
return (...args: Parameters<T>) => {
|
||||||
|
if (!inThrottle) {
|
||||||
|
func(...args)
|
||||||
|
inThrottle = true
|
||||||
|
setTimeout(() => {
|
||||||
|
inThrottle = false
|
||||||
|
}, limit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 深拷贝对象
|
||||||
|
* @param obj 要拷贝的对象
|
||||||
|
* @returns 拷贝后的对象
|
||||||
|
*/
|
||||||
|
export const deepClone = <T>(obj: T): T => {
|
||||||
|
if (obj === null || typeof obj !== 'object') {
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj instanceof Date) {
|
||||||
|
return new Date(obj.getTime()) as any
|
||||||
|
}
|
||||||
|
|
||||||
|
if (obj instanceof Array) {
|
||||||
|
return obj.map(item => deepClone(item)) as any
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj === 'object') {
|
||||||
|
const clonedObj = {} as T
|
||||||
|
for (const key in obj) {
|
||||||
|
if (obj.hasOwnProperty(key)) {
|
||||||
|
clonedObj[key as keyof T] = deepClone(obj[key as keyof T])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return clonedObj
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj
|
||||||
|
}
|
||||||
75
frontend/src/views/NotFoundView.vue
Normal file
75
frontend/src/views/NotFoundView.vue
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<template>
|
||||||
|
<div class="not-found-container">
|
||||||
|
<div class="not-found-content">
|
||||||
|
<h1 class="error-code">404</h1>
|
||||||
|
<h2 class="error-message">页面不存在</h2>
|
||||||
|
<p class="error-description">
|
||||||
|
抱歉,您访问的页面不存在或已被移除。
|
||||||
|
</p>
|
||||||
|
<div class="action-buttons">
|
||||||
|
<el-button type="primary" @click="goHome">返回首页</el-button>
|
||||||
|
<el-button @click="goBack">返回上一页</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 返回首页
|
||||||
|
const goHome = () => {
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回上一页
|
||||||
|
const goBack = () => {
|
||||||
|
router.back()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.not-found-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found-content {
|
||||||
|
text-align: center;
|
||||||
|
padding: 50px;
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-code {
|
||||||
|
font-size: 120px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #e74c3c;
|
||||||
|
margin: 0 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
margin: 0 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-description {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #666;
|
||||||
|
margin: 0 0 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
638
frontend/src/views/admin/GameService.vue
Normal file
638
frontend/src/views/admin/GameService.vue
Normal file
@@ -0,0 +1,638 @@
|
|||||||
|
<template>
|
||||||
|
<el-container class="admin-container">
|
||||||
|
<!-- 侧边栏 -->
|
||||||
|
<AdminSidebar />
|
||||||
|
|
||||||
|
<!-- 主内容区 -->
|
||||||
|
<el-container direction="vertical">
|
||||||
|
<!-- 顶部导航栏 -->
|
||||||
|
<AdminHeader />
|
||||||
|
|
||||||
|
<!-- 标签式导航组件 -->
|
||||||
|
<TabNav />
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<el-main class="admin-main">
|
||||||
|
<div class="page-container">
|
||||||
|
<!-- 页面标题 -->
|
||||||
|
<h2 class="page-title">游戏服务监控</h2>
|
||||||
|
|
||||||
|
<!-- 服务状态监控区域 -->
|
||||||
|
<div class="config-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3>服务状态监控</h3>
|
||||||
|
<el-button type="primary" @click="refreshGameStatus">
|
||||||
|
刷新状态
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="status-grid">
|
||||||
|
<div class="status-info">
|
||||||
|
<el-descriptions :column="1" border>
|
||||||
|
<el-descriptions-item label="服务状态">
|
||||||
|
<el-tag :type="gameStatus.connected ? 'success' : 'danger'">
|
||||||
|
{{ gameStatus.connected ? '已连接' : '未连接' }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="服务地址">
|
||||||
|
{{ gameStatus.serverUrl }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="最后检测时间">
|
||||||
|
{{ formatDate(gameStatus.lastCheckTime) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="API调用次数">
|
||||||
|
{{ gameStatus.apiCallCount }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="平均响应时间">
|
||||||
|
{{ gameStatus.avgResponseTime }}ms
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="success-rate">
|
||||||
|
<div class="rate-chart">
|
||||||
|
<el-progress :percentage="gameStatus.responseSuccessRate" :stroke-width="24" />
|
||||||
|
<div class="rate-label">API成功率</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- API调用测试区域 -->
|
||||||
|
<div class="config-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3>API调用测试</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-content">
|
||||||
|
<el-form :model="apiTestForm" label-width="120px">
|
||||||
|
<el-form-item label="API路径">
|
||||||
|
<el-input v-model="apiTestForm.path" placeholder="如: /user/info" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="请求参数">
|
||||||
|
<el-input
|
||||||
|
v-model="apiTestForm.params"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
placeholder='JSON格式参数,如: {"userId": "123"}'
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="testApiCall">
|
||||||
|
发送请求
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="clearApiTest">
|
||||||
|
清空
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<!-- API响应结果 -->
|
||||||
|
<div class="api-response" v-if="apiResponse">
|
||||||
|
<h4>响应结果:</h4>
|
||||||
|
<el-tabs v-model="activeTab">
|
||||||
|
<el-tab-pane label="响应数据" name="data">
|
||||||
|
<pre>{{ formatJson(apiResponse.data) }}</pre>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="响应头" name="headers">
|
||||||
|
<pre>{{ formatJson(apiResponse.headers) }}</pre>
|
||||||
|
</el-tab-pane>
|
||||||
|
<el-tab-pane label="请求信息" name="request">
|
||||||
|
<pre>{{ formatJson(apiResponse.requestInfo) }}</pre>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- API调用历史记录 -->
|
||||||
|
<div class="config-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3>API调用历史</h3>
|
||||||
|
<el-button size="small" @click="clearHistory">
|
||||||
|
清空历史
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-content">
|
||||||
|
<el-table
|
||||||
|
:data="apiHistory"
|
||||||
|
border
|
||||||
|
stripe
|
||||||
|
style="width: 100%"
|
||||||
|
max-height="400"
|
||||||
|
>
|
||||||
|
<el-table-column prop="timestamp" label="调用时间" width="180" sortable />
|
||||||
|
<el-table-column prop="path" label="API路径" width="200" />
|
||||||
|
<el-table-column prop="status" label="状态" width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag :type="scope.row.status === 200 ? 'success' : 'danger'">
|
||||||
|
{{ scope.row.status }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="responseTime" label="响应时间" width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
<span>{{ scope.row.responseTime }}ms</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="userId" label="用户ID" width="120" />
|
||||||
|
<el-table-column prop="params" label="请求参数" min-width="200">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tooltip :content="formatJson(scope.row.params)" placement="top">
|
||||||
|
<span class="param-text">{{ truncateText(formatJson(scope.row.params), 50) }}</span>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="response" label="响应数据" min-width="200">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tooltip :content="formatJson(scope.row.response)" placement="top">
|
||||||
|
<span class="response-text">{{ truncateText(formatJson(scope.row.response), 50) }}</span>
|
||||||
|
</el-tooltip>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 服务配置 -->
|
||||||
|
<div class="config-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<h3>服务配置</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-content">
|
||||||
|
<el-form :model="gameConfig" label-width="120px">
|
||||||
|
<el-form-item label="游戏服务URL">
|
||||||
|
<el-input v-model="gameConfig.serverUrl" placeholder="请输入游戏服务地址" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="连接超时时间">
|
||||||
|
<el-slider v-model="gameConfig.timeout" :min="1000" :max="10000" :step="500" />
|
||||||
|
<span style="margin-left: 10px;">{{ gameConfig.timeout }}ms</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="重试次数">
|
||||||
|
<el-select v-model="gameConfig.retryCount" placeholder="选择重试次数">
|
||||||
|
<el-option label="0次" value="0" />
|
||||||
|
<el-option label="1次" value="1" />
|
||||||
|
<el-option label="2次" value="2" />
|
||||||
|
<el-option label="3次" value="3" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="updateGameConfig">
|
||||||
|
更新配置
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-main>
|
||||||
|
</el-container>
|
||||||
|
</el-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import TabNav from '@/components/TabNav.vue'
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useUserStore } from '@/store/user'
|
||||||
|
import { useConfigStore } from '@/store/config'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import AdminSidebar from '@/components/AdminSidebar.vue'
|
||||||
|
import AdminHeader from '@/components/AdminHeader.vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const configStore = useConfigStore()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 游戏服务状态
|
||||||
|
const gameStatus = ref({
|
||||||
|
connected: true,
|
||||||
|
serverUrl: 'http://localhost:8080/game-api',
|
||||||
|
lastCheckTime: new Date(),
|
||||||
|
apiCallCount: 1234,
|
||||||
|
avgResponseTime: 56,
|
||||||
|
responseSuccessRate: 98
|
||||||
|
})
|
||||||
|
|
||||||
|
// API调用测试表单
|
||||||
|
const apiTestForm = reactive({
|
||||||
|
path: '',
|
||||||
|
params: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// API响应数据类型定义
|
||||||
|
interface ApiResponse {
|
||||||
|
status: number
|
||||||
|
data: any
|
||||||
|
headers: Record<string, string>
|
||||||
|
requestInfo: {
|
||||||
|
path: string
|
||||||
|
method: string
|
||||||
|
params: any
|
||||||
|
timestamp: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// API调用响应
|
||||||
|
const apiResponse = ref<ApiResponse | null>(null)
|
||||||
|
const activeTab = ref('data')
|
||||||
|
|
||||||
|
// API历史记录项类型定义
|
||||||
|
interface ApiHistoryItem {
|
||||||
|
timestamp: string
|
||||||
|
path: string
|
||||||
|
status: number
|
||||||
|
responseTime: number
|
||||||
|
userId: string
|
||||||
|
params: any
|
||||||
|
response: any
|
||||||
|
}
|
||||||
|
|
||||||
|
// API调用历史
|
||||||
|
const apiHistory = ref<ApiHistoryItem[]>([
|
||||||
|
{
|
||||||
|
timestamp: '2023-11-20 14:30:25',
|
||||||
|
path: '/user/info',
|
||||||
|
status: 200,
|
||||||
|
responseTime: 45,
|
||||||
|
userId: '123',
|
||||||
|
params: { userId: '123' },
|
||||||
|
response: { success: true, data: { userId: '123', username: 'test' } }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamp: '2023-11-20 14:25:18',
|
||||||
|
path: '/game/item',
|
||||||
|
status: 404,
|
||||||
|
responseTime: 120,
|
||||||
|
userId: '456',
|
||||||
|
params: { itemId: '789' },
|
||||||
|
response: { success: false, message: 'Item not found' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamp: '2023-11-20 14:20:42',
|
||||||
|
path: '/user/login',
|
||||||
|
status: 200,
|
||||||
|
responseTime: 78,
|
||||||
|
userId: '',
|
||||||
|
params: { username: 'player1', password: '123456' },
|
||||||
|
response: { success: true, token: 'abcdef123456' }
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// 游戏服务配置
|
||||||
|
const gameConfig = reactive({
|
||||||
|
serverUrl: 'http://localhost:8080/game-api',
|
||||||
|
timeout: 5000,
|
||||||
|
retryCount: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
// 刷新游戏服务状态
|
||||||
|
const refreshGameStatus = () => {
|
||||||
|
gameStatus.value.lastCheckTime = new Date()
|
||||||
|
// 模拟检查游戏服务连接状态
|
||||||
|
setTimeout(() => {
|
||||||
|
// 随机模拟连接状态
|
||||||
|
gameStatus.value.connected = Math.random() > 0.1
|
||||||
|
|
||||||
|
if (gameStatus.value.connected) {
|
||||||
|
ElMessage.success('游戏服务连接正常')
|
||||||
|
} else {
|
||||||
|
ElMessage.error('游戏服务连接失败')
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试API调用
|
||||||
|
const testApiCall = async () => {
|
||||||
|
if (!apiTestForm.path) {
|
||||||
|
ElMessage.warning('请输入API路径')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let params = {}
|
||||||
|
if (apiTestForm.params) {
|
||||||
|
params = JSON.parse(apiTestForm.params)
|
||||||
|
}
|
||||||
|
|
||||||
|
const startTime = Date.now()
|
||||||
|
|
||||||
|
// 模拟API调用
|
||||||
|
await new Promise(resolve => setTimeout(resolve, Math.random() * 1000))
|
||||||
|
|
||||||
|
const endTime = Date.now()
|
||||||
|
const responseTime = endTime - startTime
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
status: Math.random() > 0.2 ? 200 : 500,
|
||||||
|
data: Math.random() > 0.2 ? { success: true, message: 'API调用成功', data: params } : { success: false, message: 'API调用失败' },
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Server': 'GameServer/1.0.0',
|
||||||
|
'X-Response-Time': `${responseTime}ms`
|
||||||
|
},
|
||||||
|
requestInfo: {
|
||||||
|
path: apiTestForm.path,
|
||||||
|
params: params,
|
||||||
|
method: 'POST',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apiResponse.value = response
|
||||||
|
|
||||||
|
// 更新历史记录
|
||||||
|
apiHistory.value.unshift({
|
||||||
|
timestamp: formatDate(new Date()),
|
||||||
|
path: apiTestForm.path,
|
||||||
|
status: response.status,
|
||||||
|
responseTime: responseTime,
|
||||||
|
userId: String(userStore.user?.id || ''),
|
||||||
|
params: params,
|
||||||
|
response: response.data
|
||||||
|
})
|
||||||
|
|
||||||
|
// 限制历史记录数量
|
||||||
|
if (apiHistory.value.length > 100) {
|
||||||
|
apiHistory.value = apiHistory.value.slice(0, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新游戏状态统计
|
||||||
|
gameStatus.value.apiCallCount++
|
||||||
|
gameStatus.value.avgResponseTime = Math.round(
|
||||||
|
(gameStatus.value.avgResponseTime * (gameStatus.value.apiCallCount - 1) + responseTime) / gameStatus.value.apiCallCount
|
||||||
|
)
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
ElMessage.success('API调用成功')
|
||||||
|
} else {
|
||||||
|
ElMessage.error('API调用失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('API调用测试失败:', error)
|
||||||
|
ElMessage.error('API调用测试失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空API测试表单
|
||||||
|
const clearApiTest = () => {
|
||||||
|
apiTestForm.path = ''
|
||||||
|
apiTestForm.params = ''
|
||||||
|
apiResponse.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空历史记录
|
||||||
|
const clearHistory = () => {
|
||||||
|
apiHistory.value = []
|
||||||
|
ElMessage.success('历史记录已清空')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新游戏服务配置
|
||||||
|
const updateGameConfig = async () => {
|
||||||
|
try {
|
||||||
|
// 模拟保存配置
|
||||||
|
await configStore.updateConfig({
|
||||||
|
key: 'GAME_API_URL',
|
||||||
|
value: gameConfig.serverUrl
|
||||||
|
})
|
||||||
|
|
||||||
|
await configStore.updateConfig({
|
||||||
|
key: 'GAME_API_TIMEOUT',
|
||||||
|
value: gameConfig.timeout.toString()
|
||||||
|
})
|
||||||
|
|
||||||
|
await configStore.updateConfig({
|
||||||
|
key: 'GAME_API_RETRY_COUNT',
|
||||||
|
value: gameConfig.retryCount.toString()
|
||||||
|
})
|
||||||
|
|
||||||
|
ElMessage.success('配置更新成功')
|
||||||
|
gameStatus.value.serverUrl = gameConfig.serverUrl
|
||||||
|
refreshGameStatus()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('配置更新失败:', error)
|
||||||
|
ElMessage.error('配置更新失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (date: Date): string => {
|
||||||
|
if (!date) return ''
|
||||||
|
return new Date(date).toLocaleString('zh-CN')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化JSON
|
||||||
|
const formatJson = (data: any): string => {
|
||||||
|
try {
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
return JSON.stringify(JSON.parse(data), null, 2)
|
||||||
|
}
|
||||||
|
return JSON.stringify(data, null, 2)
|
||||||
|
} catch (e) {
|
||||||
|
return String(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 截断文本
|
||||||
|
const truncateText = (text: string, maxLength: number): string => {
|
||||||
|
if (text.length <= maxLength) return text
|
||||||
|
return text.substring(0, maxLength) + '...'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时检查登录状态
|
||||||
|
onMounted(async () => {
|
||||||
|
// 检查登录状态
|
||||||
|
if (!userStore.isLoggedIn) {
|
||||||
|
ElMessage.warning('请先登录')
|
||||||
|
router.push('/login')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查管理员权限
|
||||||
|
if (!userStore.isAdmin) {
|
||||||
|
ElMessage.error('您没有管理员权限')
|
||||||
|
router.push('/')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载游戏服务配置
|
||||||
|
try {
|
||||||
|
// 直接从configStore中获取所有配置,避免多次API调用
|
||||||
|
await configStore.getAllConfigs()
|
||||||
|
|
||||||
|
// 从configStore的configs对象中获取具体配置值
|
||||||
|
const serverUrl = configStore.configs.GAME_API_URL
|
||||||
|
if (serverUrl) {
|
||||||
|
gameStatus.value.serverUrl = serverUrl
|
||||||
|
gameConfig.serverUrl = serverUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = configStore.configs.GAME_API_TIMEOUT
|
||||||
|
if (timeout) {
|
||||||
|
gameConfig.timeout = parseInt(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
const retryCount = configStore.configs.GAME_API_RETRY_COUNT
|
||||||
|
if (retryCount) {
|
||||||
|
gameConfig.retryCount = parseInt(retryCount)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载游戏服务配置失败:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新游戏服务状态
|
||||||
|
refreshGameStatus()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.admin-container {
|
||||||
|
height: 100vh;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.admin-main {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-container {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding: 15px 0;
|
||||||
|
border-bottom: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: #303133;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-grid {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-rate {
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rate-chart {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rate-label {
|
||||||
|
margin-top: 10px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-content {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-content :deep(.el-form-item) {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-content {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-response {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-response h4 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: #303133;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-response pre {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #e4e7ed;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-text,
|
||||||
|
.response-text {
|
||||||
|
color: #606266;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-text:hover,
|
||||||
|
.response-text:hover {
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.admin-main {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-grid {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.success-rate {
|
||||||
|
min-width: auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
162
frontend/src/views/admin/Home.vue
Normal file
162
frontend/src/views/admin/Home.vue
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
<template>
|
||||||
|
<el-container class="admin-container">
|
||||||
|
<!-- 侧边栏 -->
|
||||||
|
<AdminSidebar />
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<el-container direction="vertical">
|
||||||
|
<!-- 顶部导航栏 -->
|
||||||
|
<AdminHeader />
|
||||||
|
|
||||||
|
<!-- 标签式导航组件 -->
|
||||||
|
<TabNav />
|
||||||
|
|
||||||
|
<!-- 主要内容 -->
|
||||||
|
<el-main class="admin-main">
|
||||||
|
<el-card class="welcome-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>工作台</span>
|
||||||
|
<el-tag type="success">管理员</el-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="welcome-content">
|
||||||
|
<h3>欢迎回来,{{ userStore.userInfo?.username || '管理员' }}</h3>
|
||||||
|
<p class="welcome-desc">这里是运营管理系统后台,您可以管理用户、配置系统和监控游戏服务。</p>
|
||||||
|
<el-row :gutter="20" class="quick-stats">
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-statistic title="在线用户" :value="128" />
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-statistic title="今日登录" :value="89" />
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-statistic title="系统配置" :value="12" />
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-statistic title="服务状态" value="正常" />
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-main>
|
||||||
|
</el-container>
|
||||||
|
</el-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useUserStore } from '@/store/user'
|
||||||
|
import AdminSidebar from '@/components/AdminSidebar.vue'
|
||||||
|
import AdminHeader from '@/components/AdminHeader.vue'
|
||||||
|
import TabNav from '@/components/TabNav.vue'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.admin-container {
|
||||||
|
height: 100vh;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-header {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
padding: 0;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-profile {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-profile:hover {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
margin: 0 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-main {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-card {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-content h3 {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-desc {
|
||||||
|
margin: 0 0 24px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-stats {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-statistic) {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-statistic__head) {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-statistic__content) {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
165
frontend/src/views/admin/Login.vue
Normal file
165
frontend/src/views/admin/Login.vue
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
<template>
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="login-form">
|
||||||
|
<div class="login-header">
|
||||||
|
<h2>运营管理系统</h2>
|
||||||
|
<p>欢迎登录游戏运营平台后台</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form
|
||||||
|
ref="loginFormRef"
|
||||||
|
:model="loginForm"
|
||||||
|
:rules="loginRules"
|
||||||
|
label-position="top"
|
||||||
|
>
|
||||||
|
<el-form-item label="管理员账号" prop="username">
|
||||||
|
<el-input
|
||||||
|
v-model="loginForm.username"
|
||||||
|
placeholder="请输入管理员账号"
|
||||||
|
size="large"
|
||||||
|
:prefix-icon="RiAdminLine"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="密码" prop="password">
|
||||||
|
<el-input
|
||||||
|
v-model="loginForm.password"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入管理员密码"
|
||||||
|
show-password
|
||||||
|
size="large"
|
||||||
|
:prefix-icon="RiLockLine"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:loading="loading"
|
||||||
|
@click="handleLogin"
|
||||||
|
size="large"
|
||||||
|
block
|
||||||
|
>
|
||||||
|
登录
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<div class="login-footer">
|
||||||
|
<el-link type="primary" :underline="false">忘记密码?</el-link>
|
||||||
|
<el-link type="primary" :underline="false">联系管理员</el-link>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useUserStore } from '@/store/user'
|
||||||
|
import type { LoginForm } from '@/types/auth'
|
||||||
|
// 导入Remix Icon组件
|
||||||
|
import { RiAdminLine, RiLockLine } from '@remixicon/vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
// 登录表单引用
|
||||||
|
const loginFormRef = ref()
|
||||||
|
|
||||||
|
// 加载状态
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 登录表单
|
||||||
|
const loginForm = reactive<LoginForm>({
|
||||||
|
username: '',
|
||||||
|
password: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表单验证规则
|
||||||
|
const loginRules = {
|
||||||
|
username: [
|
||||||
|
{ required: true, message: '请输入管理员账号', trigger: 'blur' },
|
||||||
|
{ min: 3, max: 20, message: '账号长度在 3 到 20 个字符', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
password: [
|
||||||
|
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||||
|
{ min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理登录
|
||||||
|
const handleLogin = async () => {
|
||||||
|
// 表单验证
|
||||||
|
if (!loginFormRef.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await loginFormRef.value.validate()
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
// 调用userStore的登录方法
|
||||||
|
await userStore.login(loginForm)
|
||||||
|
|
||||||
|
// 检查是否为管理员
|
||||||
|
if (!userStore.isAdmin) {
|
||||||
|
throw new Error('您不是管理员,无法登录运营管理系统')
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessage.success('登录成功')
|
||||||
|
|
||||||
|
// 跳转到管理员首页
|
||||||
|
router.push('/admin')
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error(error?.message || '登录失败,请稍后重试')
|
||||||
|
console.error('登录失败:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #1890ff 0%, #52c41a 100%);
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 40px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header h2 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header p {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 输入框图标样式已通过Element Plus的prefix-icon属性自动处理 */
|
||||||
|
</style>
|
||||||
891
frontend/src/views/admin/PlayerList.vue
Normal file
891
frontend/src/views/admin/PlayerList.vue
Normal file
@@ -0,0 +1,891 @@
|
|||||||
|
<template>
|
||||||
|
<el-container class="admin-container">
|
||||||
|
<!-- 侧边栏 -->
|
||||||
|
<AdminSidebar />
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<el-container direction="vertical">
|
||||||
|
<!-- 顶部导航栏 -->
|
||||||
|
<AdminHeader />
|
||||||
|
|
||||||
|
<!-- 标签式导航组件 -->
|
||||||
|
<TabNav />
|
||||||
|
|
||||||
|
<!-- 主要内容 -->
|
||||||
|
<el-main class="admin-main">
|
||||||
|
<div class="page-container">
|
||||||
|
<h2 class="page-title">玩家列表</h2>
|
||||||
|
|
||||||
|
<!-- 搜索筛选区 -->
|
||||||
|
<div class="search-container">
|
||||||
|
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||||
|
<el-form-item label="玩家ID">
|
||||||
|
<el-input v-model="searchForm.playerId" placeholder="请输入玩家ID" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="玩家昵称">
|
||||||
|
<el-input v-model="searchForm.nickname" placeholder="请输入玩家昵称" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
|
||||||
|
<el-option label="在线" value="ONLINE" />
|
||||||
|
<el-option label="离线" value="OFFLINE" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||||
|
<el-button @click="handleReset">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 玩家列表区 -->
|
||||||
|
<div class="list-container">
|
||||||
|
<div class="list-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<el-button type="primary" @click="handleAddPlayer">新增玩家</el-button>
|
||||||
|
<el-button @click="handleExport">导出</el-button>
|
||||||
|
<el-button @click="handleImport">导入</el-button>
|
||||||
|
<el-button type="danger" @click="handleBatchDelete" :disabled="selectedPlayerIds.length === 0">批量删除</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 玩家表格 -->
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
:data="playerList"
|
||||||
|
style="width: 100%"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
>
|
||||||
|
<el-table-column type="selection" width="55" />
|
||||||
|
<el-table-column prop="id" label="ID" width="120" align="center" />
|
||||||
|
<el-table-column prop="nickname" label="账号名称" />
|
||||||
|
<el-table-column prop="level" label="等级" width="80" align="center" />
|
||||||
|
<el-table-column prop="vipLevel" label="VIP等级" width="100" align="center" />
|
||||||
|
<el-table-column prop="lastLoginTime" label="最后登录时间" width="180" />
|
||||||
|
<el-table-column prop="status" label="状态" width="120" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag
|
||||||
|
:type="scope.row.status === 'ONLINE' ? 'success' : 'info'"
|
||||||
|
>
|
||||||
|
{{ scope.row.status === 'ONLINE' ? '在线' : '离线' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="260" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button type="primary" size="small" @click="handleQueryRoles(scope.row)">查询角色</el-button>
|
||||||
|
<el-button type="primary" size="small" @click="handleEditPlayer(scope.row)">编辑</el-button>
|
||||||
|
<el-button type="danger" size="small" @click="handleDeletePlayer(scope.row.id)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.currentPage"
|
||||||
|
v-model:page-size="pagination.pageSize"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
:total="pagination.total"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 玩家表单对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="dialogTitle"
|
||||||
|
width="500px"
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
ref="playerFormRef"
|
||||||
|
:model="playerForm"
|
||||||
|
:rules="formRules"
|
||||||
|
label-width="80px"
|
||||||
|
>
|
||||||
|
<el-form-item label="ID" prop="id">
|
||||||
|
<el-input v-model="playerForm.id" placeholder="请输入ID" :disabled="!isAdd" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="账号名称" prop="nickname">
|
||||||
|
<el-input v-model="playerForm.nickname" placeholder="请输入账号名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="等级" prop="level">
|
||||||
|
<el-input-number v-model="playerForm.level" :min="1" :max="100" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="VIP等级" prop="vipLevel">
|
||||||
|
<el-input-number v-model="playerForm.vipLevel" :min="0" :max="10" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态" prop="status">
|
||||||
|
<el-switch
|
||||||
|
v-model="playerForm.status"
|
||||||
|
active-value="ONLINE"
|
||||||
|
inactive-value="OFFLINE"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSavePlayer">保存</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 角色查询弹窗 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="rolesDialogVisible"
|
||||||
|
:title="rolesDialogTitle"
|
||||||
|
width="800px"
|
||||||
|
destroy-on-close
|
||||||
|
@close="handleRolesDialogClose"
|
||||||
|
>
|
||||||
|
<el-collapse v-if="!rolesLoading">
|
||||||
|
<el-collapse-item
|
||||||
|
v-for="character in charactersList"
|
||||||
|
:key="character.id"
|
||||||
|
:title="`${character.id}-${character.extra_data.name || character.extra_data.名称 || '未知名称'}`"
|
||||||
|
>
|
||||||
|
<!-- 添加滚动条,限制高度 -->
|
||||||
|
<div style="max-height: 500px; overflow-y: auto; padding-right: 10px;">
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<!-- 基本信息 -->
|
||||||
|
<el-descriptions-item label="账号ID">{{ character.uid }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="角色ID">{{ character.id }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="角色名称">{{ character.extra_data.name || character.extra_data.名称 || '未知名称' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="性别">{{ character.extra_data.gender || character.extra_data.性别 || '未知' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="种族">{{ character.extra_data.race || character.extra_data.种族 || '未知' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="门派">{{ character.extra_data.faction || character.extra_data.school || character.extra_data.门派 || '无门派' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="等级">{{ character.extra_data.level || character.extra_data.等级 || 0 }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="状态">{{ character.status }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">{{ character.created_at }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="最后登录">{{ character.last_login }}</el-descriptions-item>
|
||||||
|
|
||||||
|
<!-- 基本属性 -->
|
||||||
|
<el-descriptions-item label="气血" :span="2">
|
||||||
|
{{ character.extra_data.max_qi || character.extra_data.max_health || character.extra_data.最大气血 || 0 }}/{{ character.extra_data.qi || character.extra_data.health || character.extra_data.气血 || 0 }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="魔法" :span="2">
|
||||||
|
{{ character.extra_data.max_magic || character.extra_data.最大魔法 || 0 }}/{{ character.extra_data.magic || character.extra_data.魔法 || 0 }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="伤害">{{ character.extra_data.damage || character.extra_data.伤害 || 0 }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="防御">{{ character.extra_data.defense || character.extra_data.防御 || 0 }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="命中">{{ character.extra_data.hit || character.extra_data.accuracy || character.extra_data.命中 || 0 }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="速度">{{ character.extra_data.speed || character.extra_data.速度 || 0 }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="灵力">{{ character.extra_data.spirit || character.extra_data.intelligence || character.extra_data.灵力 || 0 }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="躲避">{{ character.extra_data.dodge || character.extra_data.躲避 || 0 }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<!-- 详细属性折叠面板 -->
|
||||||
|
<el-collapse>
|
||||||
|
<!-- 修炼信息 -->
|
||||||
|
<el-collapse-item title="修炼信息" v-if="character.extra_data.cultivation || character.extra_data.cultivation_info || character.extra_data.修炼">
|
||||||
|
<el-descriptions :column="3" border>
|
||||||
|
<!-- 根据API返回的实际字段名调整 -->
|
||||||
|
<el-descriptions-item label="攻击修炼">
|
||||||
|
{{ formatCultivation((character.extra_data.cultivation || {}).attack_cultivation || (character.extra_data.cultivation_info || {}).attack_cultivation || (character.extra_data.cultivation || {}).attack || (character.extra_data.修炼 || {}).attack_cultivation || (character.extra_data.修炼 || {}).攻击修炼 || []) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="防御修炼">
|
||||||
|
{{ formatCultivation((character.extra_data.cultivation || {}).defense_cultivation || (character.extra_data.cultivation_info || {}).defense_cultivation || (character.extra_data.cultivation || {}).defense || (character.extra_data.修炼 || {}).defense_cultivation || (character.extra_data.修炼 || {}).防御修炼 || []) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="法术修炼">
|
||||||
|
{{ formatCultivation((character.extra_data.cultivation || {}).spell_cultivation || (character.extra_data.cultivation_info || {}).spell_cultivation || (character.extra_data.cultivation || {}).magic || (character.extra_data.修炼 || {}).spell_cultivation || (character.extra_data.修炼 || {}).法术修炼 || []) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="抗法修炼">
|
||||||
|
{{ formatCultivation((character.extra_data.cultivation || {}).resist_spell_cultivation || (character.extra_data.cultivation_info || {}).resist_spell_cultivation || (character.extra_data.cultivation || {}).resist_magic || (character.extra_data.修炼 || {}).resist_spell_cultivation || (character.extra_data.修炼 || {}).抗法修炼 || []) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="猎术修炼">
|
||||||
|
{{ formatCultivation((character.extra_data.cultivation || {}).hunting_cultivation || (character.extra_data.cultivation_info || {}).hunting_cultivation || (character.extra_data.cultivation || {}).hunting || (character.extra_data.修炼 || {}).hunting_cultivation || (character.extra_data.修炼 || {}).猎术修炼 || []) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="抗物理修炼">
|
||||||
|
{{ formatCultivation((character.extra_data.cultivation || {}).resist_physical_cultivation || (character.extra_data.cultivation_info || {}).resist_physical_cultivation || (character.extra_data.cultivation || {}).resist_physical || (character.extra_data.修炼 || {}).resist_physical_cultivation || (character.extra_data.修炼 || {}).抗物理修炼 || []) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-collapse-item>
|
||||||
|
|
||||||
|
<!-- 技能信息 -->
|
||||||
|
<el-collapse-item title="技能信息" v-if="character.extra_data.skills || character.extra_data.skill_list || character.extra_data.技能">
|
||||||
|
<el-descriptions :column="3" border>
|
||||||
|
<el-descriptions-item
|
||||||
|
v-for="(skill, key) in character.extra_data.skills || character.extra_data.skill_list || character.extra_data.技能"
|
||||||
|
:key="key"
|
||||||
|
:label="skill.name || skill.名称 || key"
|
||||||
|
>
|
||||||
|
{{ skill.level || skill.等级 || 0 }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-collapse-item>
|
||||||
|
|
||||||
|
<!-- 门派信息 -->
|
||||||
|
<el-collapse-item title="门派信息" v-if="character.extra_data.faction || character.extra_data.school || character.extra_data.门派">
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="门派">{{ character.extra_data.faction || character.extra_data.school || character.extra_data.门派 || '无门派' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="当前称谓">{{ character.extra_data.current_title || character.extra_data.当前称谓 || '' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="门贡" :span="2">{{ character.extra_data.faction_contribution || character.extra_data.school_contribution || character.extra_data.门贡 || 0 }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</el-collapse-item>
|
||||||
|
</el-collapse>
|
||||||
|
</div>
|
||||||
|
</el-collapse-item>
|
||||||
|
</el-collapse>
|
||||||
|
|
||||||
|
<!-- 加载状态 -->
|
||||||
|
<el-skeleton :rows="5" animated v-else />
|
||||||
|
|
||||||
|
<!-- 无数据提示 -->
|
||||||
|
<div v-if="!rolesLoading && charactersList.length === 0" class="no-data">
|
||||||
|
<el-empty description="暂无角色数据" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="rolesDialogVisible = false">关闭</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</el-main>
|
||||||
|
</el-container>
|
||||||
|
</el-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import AdminSidebar from '@/components/AdminSidebar.vue'
|
||||||
|
import AdminHeader from '@/components/AdminHeader.vue'
|
||||||
|
import TabNav from '@/components/TabNav.vue'
|
||||||
|
import { callGameApiThroughBackend } from '@/api/game'
|
||||||
|
|
||||||
|
// 搜索表单
|
||||||
|
const searchForm = reactive({
|
||||||
|
playerId: '',
|
||||||
|
nickname: '',
|
||||||
|
status: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
// 玩家列表(模拟数据)
|
||||||
|
const playerList = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const pagination = reactive({
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 选中的玩家ID
|
||||||
|
const selectedPlayerIds = ref<number[]>([])
|
||||||
|
|
||||||
|
// 对话框
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const dialogTitle = ref('新增玩家')
|
||||||
|
const isAdd = ref(true)
|
||||||
|
|
||||||
|
// 玩家表单
|
||||||
|
const playerFormRef = ref()
|
||||||
|
const playerForm = reactive({
|
||||||
|
id: '',
|
||||||
|
nickname: '',
|
||||||
|
level: 1,
|
||||||
|
vipLevel: 0,
|
||||||
|
status: 'ONLINE' as const,
|
||||||
|
lastLoginTime: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表单验证规则
|
||||||
|
const formRules = reactive({
|
||||||
|
id: [
|
||||||
|
{ required: true, message: '请输入玩家ID', trigger: 'blur' },
|
||||||
|
{ min: 1, max: 20, message: '玩家ID长度在 1 到 20 个字符', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
nickname: [
|
||||||
|
{ required: true, message: '请输入玩家昵称', trigger: 'blur' },
|
||||||
|
{ min: 2, max: 20, message: '玩家昵称长度在 2 到 20 个字符', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
level: [
|
||||||
|
{ required: true, message: '请输入等级', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 角色查询相关
|
||||||
|
const rolesDialogVisible = ref(false) // 角色弹窗可见性
|
||||||
|
const rolesDialogTitle = ref('') // 角色弹窗标题
|
||||||
|
const charactersList = ref<any[]>([]) // 角色列表数据
|
||||||
|
const rolesLoading = ref(false) // 角色数据加载状态
|
||||||
|
const currentPlayer = ref<any>(null) // 当前查询的玩家信息
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
onMounted(() => {
|
||||||
|
loadPlayers()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 加载玩家列表
|
||||||
|
const loadPlayers = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
// 通过后端转发调用真实API获取玩家列表(运营管理系统专用)
|
||||||
|
const response = await callGameApiThroughBackend('account/get_account_list', {})
|
||||||
|
// 处理后端返回的数据结构(响应拦截器已处理,response 即为 response.data)
|
||||||
|
const data = response as any
|
||||||
|
if (data && data.success && data.data) {
|
||||||
|
// 检查游戏服务端返回的数据结构
|
||||||
|
if (data.data.success && data.data.data) {
|
||||||
|
const userList = data.data.data.user_list || []
|
||||||
|
// 适配API返回的数据结构
|
||||||
|
const players = userList.map((user: any) => ({
|
||||||
|
id: user.id,
|
||||||
|
nickname: user.username,
|
||||||
|
level: 1, // API未返回等级信息,默认为1
|
||||||
|
vipLevel: 0, // API未返回VIP等级信息,默认为0
|
||||||
|
lastLoginTime: user.last_login || '未知',
|
||||||
|
status: 'OFFLINE' // API未返回在线状态,默认为离线
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 实现前端分页
|
||||||
|
const startIndex = (pagination.currentPage - 1) * pagination.pageSize
|
||||||
|
const endIndex = startIndex + pagination.pageSize
|
||||||
|
|
||||||
|
playerList.value = players.slice(startIndex, endIndex)
|
||||||
|
pagination.total = players.length
|
||||||
|
} else {
|
||||||
|
// 游戏服务端返回错误
|
||||||
|
const errorMsg = data.data.message || '游戏服务端返回数据格式错误'
|
||||||
|
console.error('获取玩家列表失败:', errorMsg)
|
||||||
|
ElMessage.error(errorMsg)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 后端API调用失败
|
||||||
|
const errorMsg = data.message || '游戏服务端请求失败,请稍后重试'
|
||||||
|
console.error('获取玩家列表失败:', errorMsg)
|
||||||
|
ElMessage.error(errorMsg)
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('加载玩家列表失败:', error)
|
||||||
|
const errorMsg = error.message || '游戏服务端请求失败,请稍后重试'
|
||||||
|
ElMessage.error(errorMsg)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
// 模拟搜索功能
|
||||||
|
pagination.currentPage = 1
|
||||||
|
ElMessage.info('搜索功能开发中')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置
|
||||||
|
const handleReset = () => {
|
||||||
|
searchForm.playerId = ''
|
||||||
|
searchForm.nickname = ''
|
||||||
|
searchForm.status = undefined
|
||||||
|
pagination.currentPage = 1
|
||||||
|
loadPlayers()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页大小变化
|
||||||
|
const handleSizeChange = (size: number) => {
|
||||||
|
pagination.pageSize = size
|
||||||
|
loadPlayers()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前页变化
|
||||||
|
const handleCurrentChange = (page: number) => {
|
||||||
|
pagination.currentPage = page
|
||||||
|
loadPlayers()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择变化
|
||||||
|
const handleSelectionChange = (selection: any[]) => {
|
||||||
|
selectedPlayerIds.value = selection.map(item => item.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增玩家
|
||||||
|
const handleAddPlayer = () => {
|
||||||
|
dialogTitle.value = '新增玩家'
|
||||||
|
isAdd.value = true
|
||||||
|
resetForm()
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑玩家
|
||||||
|
const handleEditPlayer = (row: any) => {
|
||||||
|
dialogTitle.value = '编辑玩家'
|
||||||
|
isAdd.value = false
|
||||||
|
// 复制玩家信息到表单
|
||||||
|
Object.assign(playerForm, {
|
||||||
|
...row
|
||||||
|
})
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
const resetForm = () => {
|
||||||
|
if (playerFormRef.value) {
|
||||||
|
playerFormRef.value.resetFields()
|
||||||
|
}
|
||||||
|
Object.assign(playerForm, {
|
||||||
|
id: '',
|
||||||
|
nickname: '',
|
||||||
|
level: 1,
|
||||||
|
vipLevel: 0,
|
||||||
|
status: 'ONLINE',
|
||||||
|
lastLoginTime: ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存玩家
|
||||||
|
const handleSavePlayer = async () => {
|
||||||
|
if (!playerFormRef.value) return
|
||||||
|
try {
|
||||||
|
await playerFormRef.value.validate()
|
||||||
|
if (isAdd.value) {
|
||||||
|
ElMessage.info('新增玩家功能开发中')
|
||||||
|
} else {
|
||||||
|
ElMessage.info('更新玩家功能开发中')
|
||||||
|
dialogVisible.value = false
|
||||||
|
loadPlayers()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== false) {
|
||||||
|
console.error('保存玩家失败:', error)
|
||||||
|
ElMessage.error('保存玩家失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除玩家
|
||||||
|
const handleDeletePlayer = (id: string) => {
|
||||||
|
ElMessageBox.confirm(`确定要删除ID为${id}的玩家吗?`, '提示', {
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
ElMessage.success('删除玩家成功')
|
||||||
|
loadPlayers()
|
||||||
|
}).catch(() => {
|
||||||
|
// 取消删除
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量删除玩家
|
||||||
|
const handleBatchDelete = async () => {
|
||||||
|
if (selectedPlayerIds.value.length === 0) {
|
||||||
|
ElMessage.warning('请选择要删除的玩家')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确定要删除选中的 ${selectedPlayerIds.value.length} 个玩家吗?`, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
|
||||||
|
ElMessage.success('批量删除成功')
|
||||||
|
loadPlayers()
|
||||||
|
selectedPlayerIds.value = []
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('批量删除失败:', error)
|
||||||
|
ElMessage.error('批量删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出玩家
|
||||||
|
const handleExport = () => {
|
||||||
|
ElMessage.info('导出功能开发中')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导入玩家
|
||||||
|
const handleImport = () => {
|
||||||
|
ElMessage.info('导入功能开发中')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化修炼数据
|
||||||
|
const formatCultivation = (cultivation: any) => {
|
||||||
|
if (!Array.isArray(cultivation) || cultivation.length === 0) {
|
||||||
|
return '0'
|
||||||
|
}
|
||||||
|
return cultivation[0] || '0'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 角色弹窗关闭时的处理
|
||||||
|
const handleRolesDialogClose = () => {
|
||||||
|
// 重置角色列表数据
|
||||||
|
charactersList.value = []
|
||||||
|
// 重置当前查询的玩家信息
|
||||||
|
currentPlayer.value = null
|
||||||
|
// 重置弹窗标题
|
||||||
|
rolesDialogTitle.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询角色
|
||||||
|
const handleQueryRoles = async (row: any) => {
|
||||||
|
if (!row || !row.id) {
|
||||||
|
ElMessage.error('无效的玩家信息')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存当前查询的玩家信息
|
||||||
|
currentPlayer.value = row
|
||||||
|
|
||||||
|
// 显示加载状态
|
||||||
|
rolesLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 调用API获取角色数据
|
||||||
|
const response = await callGameApiThroughBackend('characters/get_characters', {
|
||||||
|
code: 'characters/get_characters',
|
||||||
|
uid: row.id
|
||||||
|
})
|
||||||
|
|
||||||
|
// 处理后端返回的数据结构(响应拦截器已处理,response 即为 response.data)
|
||||||
|
const data = response as any
|
||||||
|
if (data && data.success && data.data) {
|
||||||
|
// 检查游戏服务端返回的数据结构
|
||||||
|
if (data.data.success && data.data.data) {
|
||||||
|
const charactersData = data.data.data
|
||||||
|
const characters = charactersData.characters_list || []
|
||||||
|
|
||||||
|
// 更新角色列表数据
|
||||||
|
charactersList.value = characters
|
||||||
|
|
||||||
|
// 设置弹窗标题
|
||||||
|
rolesDialogTitle.value = `${row.nickname}玩家的角色数据(共${characters.length}个角色)`
|
||||||
|
|
||||||
|
// 显示弹窗
|
||||||
|
rolesDialogVisible.value = true
|
||||||
|
} else {
|
||||||
|
// 游戏服务端返回错误
|
||||||
|
const errorMsg = data.data.message || '获取角色数据失败'
|
||||||
|
console.error('获取角色数据失败:', errorMsg)
|
||||||
|
ElMessage.error(errorMsg)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 后端API调用失败
|
||||||
|
const errorMsg = data.message || '游戏服务端请求失败,请稍后重试'
|
||||||
|
console.error('获取角色数据失败:', errorMsg)
|
||||||
|
ElMessage.error(errorMsg)
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('获取角色数据失败:', error)
|
||||||
|
const errorMsg = error.message || '获取角色数据失败,请稍后重试'
|
||||||
|
ElMessage.error(errorMsg)
|
||||||
|
} finally {
|
||||||
|
// 隐藏加载状态
|
||||||
|
rolesLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.admin-container {
|
||||||
|
height: 100vh;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-header {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
padding: 0;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-profile {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-profile:hover {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
margin: 0 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-main {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-container {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 搜索筛选区样式 */
|
||||||
|
.search-container {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 20px 15px;
|
||||||
|
background-color: #f0f9ff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 调整表单元素的默认样式,确保垂直居中 */
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-form-item__content) {
|
||||||
|
margin-top: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 玩家列表区样式 */
|
||||||
|
.list-container {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格样式 */
|
||||||
|
:deep(.el-table) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移除表格所有默认边框 */
|
||||||
|
:deep(.el-table__inner-wrapper) {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__body-wrapper) {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table)::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table)::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__header-wrapper) {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__header) {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__body) {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__footer-wrapper) {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__footer) {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__wrapper) {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__header th) {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__body td) {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__row) {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分页样式 */
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 状态标签样式 */
|
||||||
|
:deep(.el-tag) {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 对话框样式 */
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 角色弹窗样式 */
|
||||||
|
:deep(.el-collapse) {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-collapse-item__header) {
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
padding: 12px 20px !important; /* 增加内边距 */
|
||||||
|
box-sizing: border-box;
|
||||||
|
word-break: break-word; /* 允许标题文字换行 */
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-collapse-item__content) {
|
||||||
|
padding: 10px 0 !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 无数据提示样式 */
|
||||||
|
.no-data {
|
||||||
|
padding: 20px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 描述列表样式 */
|
||||||
|
:deep(.el-descriptions) {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-descriptions__table) {
|
||||||
|
width: 100%;
|
||||||
|
table-layout: fixed; /* 固定表格布局,防止列宽溢出 */
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-descriptions__item) {
|
||||||
|
padding: 8px 12px !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-descriptions__item-label) {
|
||||||
|
background-color: #fafafa;
|
||||||
|
font-weight: bold;
|
||||||
|
width: 120px; /* 固定标签宽度 */
|
||||||
|
word-break: keep-all;
|
||||||
|
text-align: left !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-descriptions__item-content) {
|
||||||
|
word-break: break-word; /* 允许内容换行 */
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
width: calc(100% - 120px); /* 计算内容宽度 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修复折叠面板内部嵌套的折叠面板样式 */
|
||||||
|
:deep(.el-collapse .el-collapse) {
|
||||||
|
margin: 10px 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-collapse .el-collapse-item) {
|
||||||
|
margin-bottom: 8px; /* 增加折叠面板之间的间距 */
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-collapse .el-collapse-item:last-child) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-collapse .el-collapse-item__header) {
|
||||||
|
padding: 10px 16px !important;
|
||||||
|
background-color: #f0f2f5;
|
||||||
|
font-size: 14px;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-collapse .el-collapse-item__content) {
|
||||||
|
padding: 10px 0 !important;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 修复内部描述列表样式 */
|
||||||
|
:deep(.el-collapse .el-descriptions) {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-collapse .el-descriptions__item) {
|
||||||
|
padding: 6px 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-collapse .el-descriptions__item-label) {
|
||||||
|
width: 100px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-collapse .el-descriptions__item-content) {
|
||||||
|
font-size: 13px;
|
||||||
|
width: calc(100% - 100px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
404
frontend/src/views/admin/SystemConfig.vue
Normal file
404
frontend/src/views/admin/SystemConfig.vue
Normal file
@@ -0,0 +1,404 @@
|
|||||||
|
<template>
|
||||||
|
<el-container class="admin-container">
|
||||||
|
<!-- 侧边栏 -->
|
||||||
|
<AdminSidebar />
|
||||||
|
|
||||||
|
<!-- 主内容区 -->
|
||||||
|
<el-container direction="vertical">
|
||||||
|
<!-- 顶部导航栏 -->
|
||||||
|
<AdminHeader />
|
||||||
|
|
||||||
|
<!-- 标签式导航组件 -->
|
||||||
|
<TabNav />
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<el-main class="admin-main">
|
||||||
|
<div class="page-container">
|
||||||
|
<!-- 页面标题 -->
|
||||||
|
<h2 class="page-title">系统配置</h2>
|
||||||
|
|
||||||
|
<!-- 配置表单 - Tabs标签页形式 -->
|
||||||
|
<div class="config-form-container">
|
||||||
|
<el-tabs v-model="activeTab" type="card">
|
||||||
|
<!-- 基础设置 -->
|
||||||
|
<el-tab-pane label="基础设置" name="basic">
|
||||||
|
<el-form :model="configForm" label-width="180px">
|
||||||
|
<el-form-item>
|
||||||
|
<template #label>
|
||||||
|
<span class="label-with-tooltip">
|
||||||
|
网站名称
|
||||||
|
<el-tooltip content="网站的对外显示名称,将出现在页面标题和页脚中" placement="top">
|
||||||
|
<RiInformationFill class="info-icon" />
|
||||||
|
</el-tooltip>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<el-input v-model="configForm.website_name" placeholder="请输入网站名称" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<template #label>
|
||||||
|
<span class="label-with-tooltip">
|
||||||
|
运营后台域名
|
||||||
|
<el-tooltip content="运营后台的访问域名,用于生成管理链接" placement="top">
|
||||||
|
<RiInformationFill class="info-icon" />
|
||||||
|
</el-tooltip>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<el-input v-model="configForm.admin_domain" placeholder="请输入运营后台域名" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<template #label>
|
||||||
|
<span class="label-with-tooltip">
|
||||||
|
玩家中心域名
|
||||||
|
<el-tooltip content="玩家中心的访问域名,用于生成玩家链接" placement="top">
|
||||||
|
<RiInformationFill class="info-icon" />
|
||||||
|
</el-tooltip>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<el-input v-model="configForm.player_domain" placeholder="请输入玩家中心域名" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<!-- 安全设置 -->
|
||||||
|
<el-tab-pane label="安全设置" name="security">
|
||||||
|
<el-form :model="configForm" label-width="180px">
|
||||||
|
<!-- 后端服务器配置 -->
|
||||||
|
<el-divider content-position="left">后端服务器配置</el-divider>
|
||||||
|
<el-form-item>
|
||||||
|
<template #label>
|
||||||
|
<span class="label-with-tooltip">
|
||||||
|
服务器地址
|
||||||
|
<el-tooltip content="请填写本系统后端的服务器地址(IP、域名均可)" placement="top">
|
||||||
|
<RiInformationFill class="info-icon" />
|
||||||
|
</el-tooltip>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<el-input v-model="configForm.server_host" placeholder="请输入服务器IP或域名" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<template #label>
|
||||||
|
<span class="label-with-tooltip">
|
||||||
|
服务器端口
|
||||||
|
<el-tooltip content="请填写本系统后端的运行端口" placement="top">
|
||||||
|
<RiInformationFill class="info-icon" />
|
||||||
|
</el-tooltip>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<el-input v-model="configForm.server_port" placeholder="请输入服务器端口" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- 游戏服务API配置 -->
|
||||||
|
<el-divider content-position="left">游戏服务API配置</el-divider>
|
||||||
|
<el-form-item>
|
||||||
|
<template #label>
|
||||||
|
<span class="label-with-tooltip">
|
||||||
|
游戏服务API地址
|
||||||
|
<el-tooltip content="请填写游戏服务端的API接口地址" placement="top">
|
||||||
|
<RiInformationFill class="info-icon" />
|
||||||
|
</el-tooltip>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<el-input v-model="configForm.game_api_url" placeholder="请输入游戏服务API接口地址" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<template #label>
|
||||||
|
<span class="label-with-tooltip">
|
||||||
|
游戏服务端PSK
|
||||||
|
<el-tooltip content="游戏服务端的PSK密钥,用于API调用的身份验证" placement="top">
|
||||||
|
<RiInformationFill class="info-icon" />
|
||||||
|
</el-tooltip>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<el-input v-model="configForm.game_psk" placeholder="请输入游戏服务端PSK密钥" type="password" show-password style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<!-- JWT配置 -->
|
||||||
|
<el-divider content-position="left">JWT配置</el-divider>
|
||||||
|
<el-form-item>
|
||||||
|
<template #label>
|
||||||
|
<span class="label-with-tooltip">
|
||||||
|
JWT密钥
|
||||||
|
<el-tooltip content="用于生成和验证JWT令牌的密钥,建议使用32位随机字符串" placement="top">
|
||||||
|
<RiInformationFill class="info-icon" />
|
||||||
|
</el-tooltip>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<el-input v-model="configForm.jwt_secret" placeholder="请输入JWT密钥" type="password" show-password style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<template #label>
|
||||||
|
<span class="label-with-tooltip">
|
||||||
|
JWT过期时间
|
||||||
|
<el-tooltip content="JWT令牌的有效期,单位为秒,默认24小时(86400秒)" placement="top">
|
||||||
|
<RiInformationFill class="info-icon" />
|
||||||
|
</el-tooltip>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<el-input v-model="configForm.jwt_expires_in" placeholder="请输入JWT过期时间(秒)" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<template #label>
|
||||||
|
<span class="label-with-tooltip">
|
||||||
|
JWT刷新令牌密钥
|
||||||
|
<el-tooltip content="用于生成和验证刷新令牌的密钥,建议与JWT密钥不同" placement="top">
|
||||||
|
<RiInformationFill class="info-icon" />
|
||||||
|
</el-tooltip>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<el-input v-model="configForm.jwt_refresh_secret" placeholder="请输入JWT刷新令牌密钥" type="password" show-password style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<template #label>
|
||||||
|
<span class="label-with-tooltip">
|
||||||
|
JWT刷新令牌过期时间
|
||||||
|
<el-tooltip content="刷新令牌的有效期,单位为秒,建议设置为7天(604800秒)" placement="top">
|
||||||
|
<RiInformationFill class="info-icon" />
|
||||||
|
</el-tooltip>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<el-input v-model="configForm.jwt_refresh_expires_in" placeholder="请输入JWT刷新令牌过期时间(秒)" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
<!-- 服务设置 -->
|
||||||
|
<el-tab-pane label="服务设置" name="service">
|
||||||
|
<el-form :model="configForm" label-width="180px">
|
||||||
|
<el-form-item>
|
||||||
|
<template #label>
|
||||||
|
<span class="label-with-tooltip">
|
||||||
|
服务运行状态
|
||||||
|
<el-tooltip content="当前系统服务的运行状态,可设置为运行中或维护中" placement="top">
|
||||||
|
<RiInformationFill class="info-icon" />
|
||||||
|
</el-tooltip>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<el-select v-model="configForm.service_status" placeholder="请选择服务状态">
|
||||||
|
<el-option label="运行中" value="running" />
|
||||||
|
<el-option label="维护中" value="maintenance" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<template #label>
|
||||||
|
<span class="label-with-tooltip">
|
||||||
|
维护模式开关
|
||||||
|
<el-tooltip content="开启后系统将进入维护模式,只允许管理员访问" placement="top">
|
||||||
|
<RiInformationFill class="info-icon" />
|
||||||
|
</el-tooltip>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<el-switch v-model="configForm.maintenance_mode" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
|
|
||||||
|
<!-- 保存按钮 -->
|
||||||
|
<div class="form-footer">
|
||||||
|
<el-button type="primary" @click="handleSaveConfig" size="large">
|
||||||
|
保存配置
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-main>
|
||||||
|
</el-container>
|
||||||
|
</el-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import TabNav from '@/components/TabNav.vue'
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { useConfigStore } from '@/store/config'
|
||||||
|
import { useUserStore } from '@/store/user'
|
||||||
|
import { RiInformationFill } from '@remixicon/vue'
|
||||||
|
import AdminSidebar from '@/components/AdminSidebar.vue'
|
||||||
|
import AdminHeader from '@/components/AdminHeader.vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const configStore = useConfigStore()
|
||||||
|
const userStore = useUserStore()
|
||||||
|
|
||||||
|
// 当前激活的标签页
|
||||||
|
const activeTab = ref('basic')
|
||||||
|
|
||||||
|
// 配置表单数据
|
||||||
|
const configForm = reactive({
|
||||||
|
// 基础设置
|
||||||
|
website_name: '',
|
||||||
|
admin_domain: '',
|
||||||
|
player_domain: '',
|
||||||
|
|
||||||
|
// 安全设置
|
||||||
|
server_host: '',
|
||||||
|
server_port: '',
|
||||||
|
game_api_url: '',
|
||||||
|
game_psk: '',
|
||||||
|
jwt_secret: '',
|
||||||
|
jwt_expires_in: '',
|
||||||
|
jwt_refresh_secret: '',
|
||||||
|
jwt_refresh_expires_in: '',
|
||||||
|
|
||||||
|
// 服务设置
|
||||||
|
service_status: 'running',
|
||||||
|
maintenance_mode: false
|
||||||
|
})
|
||||||
|
|
||||||
|
// 加载系统配置
|
||||||
|
const loadSystemConfig = async () => {
|
||||||
|
try {
|
||||||
|
const response = await configStore.getAllConfigs()
|
||||||
|
|
||||||
|
// 将获取到的配置映射到表单中
|
||||||
|
if (response && response.success && response.data) {
|
||||||
|
const configData = response.data
|
||||||
|
Object.assign(configForm, configData)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取系统配置失败:', error)
|
||||||
|
ElMessage.error('获取系统配置失败,请稍后重试')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存系统配置
|
||||||
|
const handleSaveConfig = async () => {
|
||||||
|
try {
|
||||||
|
// 遍历配置表单,逐个保存配置项
|
||||||
|
for (const [key, value] of Object.entries(configForm)) {
|
||||||
|
await configStore.updateConfig({ key, value })
|
||||||
|
}
|
||||||
|
|
||||||
|
ElMessage.success('配置保存成功')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存系统配置失败:', error)
|
||||||
|
ElMessage.error('保存系统配置失败,请稍后重试')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 页面加载时检查登录状态
|
||||||
|
onMounted(async () => {
|
||||||
|
// 检查登录状态
|
||||||
|
if (!userStore.isLoggedIn) {
|
||||||
|
router.push('/admin/login')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查管理员权限
|
||||||
|
if (!userStore.isAdmin) {
|
||||||
|
ElMessage.error('您没有权限访问此页面')
|
||||||
|
router.push('/admin')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载系统配置
|
||||||
|
await loadSystemConfig()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.admin-container {
|
||||||
|
height: 100vh;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.admin-main {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-container {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-form-container {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-form-container :deep(.el-tabs__content) {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-form-container :deep(.el-form-item) {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-form-container :deep(.el-divider--horizontal) {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-text {
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-with-tooltip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-with-tooltip .el-input {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-with-tooltip .el-input__wrapper {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 游戏服务API地址输入框专用样式 */
|
||||||
|
.api-url-input {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-url-input .el-input {
|
||||||
|
flex: 1 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-url-input .el-input__wrapper {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-icon {
|
||||||
|
color: #909399;
|
||||||
|
cursor: help;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-left: 6px;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-with-tooltip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-footer {
|
||||||
|
margin-top: 30px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-top: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
615
frontend/src/views/admin/UserManagement.vue
Normal file
615
frontend/src/views/admin/UserManagement.vue
Normal file
@@ -0,0 +1,615 @@
|
|||||||
|
<template>
|
||||||
|
<el-container class="admin-container">
|
||||||
|
<!-- 侧边栏 -->
|
||||||
|
<AdminSidebar />
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<el-container direction="vertical">
|
||||||
|
<!-- 顶部导航栏 -->
|
||||||
|
<AdminHeader />
|
||||||
|
|
||||||
|
<!-- 标签式导航组件 -->
|
||||||
|
<TabNav />
|
||||||
|
|
||||||
|
<!-- 主要内容 -->
|
||||||
|
<el-main class="admin-main">
|
||||||
|
<div class="page-container">
|
||||||
|
<h2 class="page-title">用户管理</h2>
|
||||||
|
|
||||||
|
<!-- 搜索筛选区 -->
|
||||||
|
<div class="search-container">
|
||||||
|
<el-form :inline="true" :model="searchForm" class="search-form">
|
||||||
|
<el-form-item label="用户名">
|
||||||
|
<el-input v-model="searchForm.username" placeholder="请输入用户名" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="昵称">
|
||||||
|
<el-input v-model="searchForm.nickname" placeholder="请输入昵称" clearable />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
|
||||||
|
<el-option label="正常" value="ACTIVE" />
|
||||||
|
<el-option label="停用" value="INACTIVE" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||||
|
<el-button @click="handleReset">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 用户列表区 -->
|
||||||
|
<div class="list-container">
|
||||||
|
<div class="list-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<el-button type="primary" @click="handleAddUser">新增用户</el-button>
|
||||||
|
<el-button @click="handleExport">导出</el-button>
|
||||||
|
<el-button @click="handleImport">导入</el-button>
|
||||||
|
<el-button type="danger" @click="handleBatchDelete" :disabled="selectedUserIds.length === 0">批量删除</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 用户表格 -->
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
:data="userList"
|
||||||
|
style="width: 100%"
|
||||||
|
@selection-change="handleSelectionChange"
|
||||||
|
>
|
||||||
|
<el-table-column type="selection" width="55" />
|
||||||
|
<el-table-column prop="id" label="ID" width="80" align="center" />
|
||||||
|
<el-table-column prop="username" label="用户名" />
|
||||||
|
<el-table-column prop="nickname" label="昵称" />
|
||||||
|
<el-table-column prop="email" label="邮箱" />
|
||||||
|
<el-table-column prop="roleName" label="角色" />
|
||||||
|
<el-table-column prop="status" label="状态" width="120" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag
|
||||||
|
:type="scope.row.status === 'ACTIVE' ? 'success' : 'danger'"
|
||||||
|
>
|
||||||
|
{{ scope.row.status === 'ACTIVE' ? '正常' : '停用' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="200" align="center">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button type="primary" size="small" @click="handleEditUser(scope.row)">编辑</el-button>
|
||||||
|
<el-button type="danger" size="small" @click="handleDeleteUser(scope.row.id)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="pagination.currentPage"
|
||||||
|
v-model:page-size="pagination.pageSize"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
:total="pagination.total"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 用户表单对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="dialogTitle"
|
||||||
|
width="500px"
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
ref="userFormRef"
|
||||||
|
:model="userForm"
|
||||||
|
:rules="formRules"
|
||||||
|
label-width="80px"
|
||||||
|
>
|
||||||
|
<el-form-item label="用户名" prop="username">
|
||||||
|
<el-input v-model="userForm.username" placeholder="请输入用户名" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="昵称" prop="nickname">
|
||||||
|
<el-input v-model="userForm.nickname" placeholder="请输入昵称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="密码" prop="password">
|
||||||
|
<el-input v-model="userForm.password" type="password" placeholder="请输入密码" :disabled="!isAdd" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="邮箱" prop="email">
|
||||||
|
<el-input v-model="userForm.email" placeholder="请输入邮箱" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="角色" prop="roleId">
|
||||||
|
<el-select v-model="userForm.roleId" placeholder="请选择角色">
|
||||||
|
<el-option
|
||||||
|
v-for="role in roleList"
|
||||||
|
:key="role.id"
|
||||||
|
:label="role.name"
|
||||||
|
:value="role.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态" prop="status">
|
||||||
|
<el-switch
|
||||||
|
v-model="userForm.status"
|
||||||
|
active-value="ACTIVE"
|
||||||
|
inactive-value="INACTIVE"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSaveUser">保存</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</el-main>
|
||||||
|
</el-container>
|
||||||
|
</el-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import AdminSidebar from '@/components/AdminSidebar.vue'
|
||||||
|
import AdminHeader from '@/components/AdminHeader.vue'
|
||||||
|
import TabNav from '@/components/TabNav.vue'
|
||||||
|
import * as userApi from '@/api/user'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 搜索表单
|
||||||
|
const searchForm = reactive({
|
||||||
|
username: '',
|
||||||
|
nickname: '',
|
||||||
|
status: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
// 用户列表
|
||||||
|
const userList = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 分页
|
||||||
|
const pagination = reactive({
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// 选中的用户ID
|
||||||
|
const selectedUserIds = ref<number[]>([])
|
||||||
|
|
||||||
|
// 对话框
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const dialogTitle = ref('新增用户')
|
||||||
|
const isAdd = ref(true)
|
||||||
|
|
||||||
|
// 用户表单
|
||||||
|
const userFormRef = ref()
|
||||||
|
const userForm = reactive({
|
||||||
|
id: '',
|
||||||
|
username: '',
|
||||||
|
nickname: '',
|
||||||
|
password: '',
|
||||||
|
email: '',
|
||||||
|
roleId: '',
|
||||||
|
status: 'ACTIVE' as const
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表单验证规则
|
||||||
|
const formRules = reactive({
|
||||||
|
username: [
|
||||||
|
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||||
|
{ min: 3, max: 20, message: '用户名长度在 3 到 20 个字符', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
nickname: [
|
||||||
|
{ required: true, message: '请输入昵称', trigger: 'blur' },
|
||||||
|
{ min: 2, max: 20, message: '昵称长度在 2 到 20 个字符', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
password: [
|
||||||
|
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||||
|
{ min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
email: [
|
||||||
|
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
||||||
|
{ type: 'email', message: '请输入有效的邮箱地址', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
roleId: [
|
||||||
|
{ required: true, message: '请选择角色', trigger: 'change' }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// 角色列表(硬编码数据)
|
||||||
|
const roleList = ref([
|
||||||
|
{ id: 1, name: '管理员' },
|
||||||
|
{ id: 2, name: '普通用户' }
|
||||||
|
])
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
onMounted(() => {
|
||||||
|
loadUsers()
|
||||||
|
loadRoles()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 加载用户列表
|
||||||
|
const loadUsers = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
page: pagination.currentPage,
|
||||||
|
pageSize: pagination.pageSize,
|
||||||
|
...searchForm
|
||||||
|
}
|
||||||
|
const response = await userApi.getUserList(params)
|
||||||
|
// 处理API响应,适配后端返回的数据结构
|
||||||
|
const responseData = response?.data || response
|
||||||
|
userList.value = responseData?.users || []
|
||||||
|
pagination.total = responseData?.total || 0
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载用户列表失败:', error)
|
||||||
|
ElMessage.error('加载用户列表失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载角色列表
|
||||||
|
const loadRoles = async () => {
|
||||||
|
// 角色列表暂时使用硬编码数据
|
||||||
|
// TODO: 实现角色管理API后,替换为真实API调用
|
||||||
|
console.log('角色列表使用硬编码数据')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
const handleSearch = () => {
|
||||||
|
pagination.currentPage = 1
|
||||||
|
loadUsers()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置
|
||||||
|
const handleReset = () => {
|
||||||
|
searchForm.username = ''
|
||||||
|
searchForm.nickname = ''
|
||||||
|
searchForm.status = undefined
|
||||||
|
pagination.currentPage = 1
|
||||||
|
loadUsers()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页大小变化
|
||||||
|
const handleSizeChange = (size: number) => {
|
||||||
|
pagination.pageSize = size
|
||||||
|
loadUsers()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前页变化
|
||||||
|
const handleCurrentChange = (page: number) => {
|
||||||
|
pagination.currentPage = page
|
||||||
|
loadUsers()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择变化
|
||||||
|
const handleSelectionChange = (selection: any[]) => {
|
||||||
|
selectedUserIds.value = selection.map(item => item.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增用户
|
||||||
|
const handleAddUser = () => {
|
||||||
|
dialogTitle.value = '新增用户'
|
||||||
|
isAdd.value = true
|
||||||
|
resetForm()
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑用户
|
||||||
|
const handleEditUser = (row: any) => {
|
||||||
|
dialogTitle.value = '编辑用户'
|
||||||
|
isAdd.value = false
|
||||||
|
// 复制用户信息到表单,确保状态字段正确映射
|
||||||
|
Object.assign(userForm, {
|
||||||
|
...row,
|
||||||
|
status: row.status || 'ACTIVE'
|
||||||
|
})
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置表单
|
||||||
|
const resetForm = () => {
|
||||||
|
if (userFormRef.value) {
|
||||||
|
userFormRef.value.resetFields()
|
||||||
|
}
|
||||||
|
Object.assign(userForm, {
|
||||||
|
id: '',
|
||||||
|
username: '',
|
||||||
|
nickname: '',
|
||||||
|
password: '',
|
||||||
|
email: '',
|
||||||
|
roleId: '',
|
||||||
|
status: 'ACTIVE'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存用户
|
||||||
|
const handleSaveUser = async () => {
|
||||||
|
if (!userFormRef.value) return
|
||||||
|
try {
|
||||||
|
await userFormRef.value.validate()
|
||||||
|
if (isAdd.value) {
|
||||||
|
// 新增用户功能暂未实现
|
||||||
|
ElMessage.info('新增用户功能开发中')
|
||||||
|
} else {
|
||||||
|
// 更新用户,状态字段已经是正确的格式
|
||||||
|
await userApi.updateUser(parseInt(userForm.id), userForm)
|
||||||
|
ElMessage.success('更新用户成功')
|
||||||
|
dialogVisible.value = false
|
||||||
|
loadUsers()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== false) {
|
||||||
|
console.error('保存用户失败:', error)
|
||||||
|
ElMessage.error('保存用户失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除用户
|
||||||
|
const handleDeleteUser = (id: number) => {
|
||||||
|
ElMessageBox.confirm('确定要删除该用户吗?', '提示', {
|
||||||
|
type: 'warning'
|
||||||
|
}).then(async () => {
|
||||||
|
try {
|
||||||
|
await userApi.deleteUser(id)
|
||||||
|
ElMessage.success('删除用户成功')
|
||||||
|
loadUsers()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('删除用户失败')
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
// 取消删除
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量删除用户
|
||||||
|
const handleBatchDelete = async () => {
|
||||||
|
if (selectedUserIds.value.length === 0) {
|
||||||
|
ElMessage.warning('请选择要删除的用户')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确定要删除选中的 ${selectedUserIds.value.length} 个用户吗?`, '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 使用批量更新状态API替代批量删除
|
||||||
|
await userApi.batchUpdateStatus({
|
||||||
|
ids: selectedUserIds.value,
|
||||||
|
status: 'deleted'
|
||||||
|
})
|
||||||
|
|
||||||
|
ElMessage.success('批量删除成功')
|
||||||
|
loadUsers()
|
||||||
|
selectedUserIds.value = []
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('批量删除失败:', error)
|
||||||
|
ElMessage.error('批量删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出用户
|
||||||
|
const handleExport = () => {
|
||||||
|
ElMessage.info('导出功能开发中')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导入用户
|
||||||
|
const handleImport = () => {
|
||||||
|
ElMessage.info('导入功能开发中')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.admin-container {
|
||||||
|
height: 100vh;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-header {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
padding: 0;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-profile {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-profile:hover {
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
margin: 0 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-main {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-container {
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 搜索筛选区样式 */
|
||||||
|
.search-container {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 20px 15px;
|
||||||
|
background-color: #f0f9ff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 调整表单元素的默认样式,确保垂直居中 */
|
||||||
|
:deep(.el-form-item) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-form-item__content) {
|
||||||
|
margin-top: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 用户列表区样式 */
|
||||||
|
.list-container {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-left {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格样式 */
|
||||||
|
:deep(.el-table) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移除表格所有默认边框 */
|
||||||
|
:deep(.el-table__inner-wrapper) {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__body-wrapper) {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table)::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table)::after {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__header-wrapper) {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__header) {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__body) {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__footer-wrapper) {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__footer) {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__wrapper) {
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__header th) {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__body td) {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-table__row) {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分页样式 */
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 状态标签样式 */
|
||||||
|
:deep(.el-tag) {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 对话框样式 */
|
||||||
|
.dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
429
frontend/src/views/player/Home.vue
Normal file
429
frontend/src/views/player/Home.vue
Normal file
@@ -0,0 +1,429 @@
|
|||||||
|
<template>
|
||||||
|
<div class="player-home">
|
||||||
|
<!-- 顶部导航栏 -->
|
||||||
|
<el-header>
|
||||||
|
<div class="header-content">
|
||||||
|
<div class="header-left">
|
||||||
|
<h1 class="logo">玩家服务中心</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header-right">
|
||||||
|
<el-dropdown>
|
||||||
|
<span class="user-info">
|
||||||
|
<el-avatar size="large">
|
||||||
|
{{ userInfo.username?.charAt(0) || 'U' }}
|
||||||
|
</el-avatar>
|
||||||
|
<span>{{ userInfo.username || '用户' }}</span>
|
||||||
|
<RiArrowDownSLine />
|
||||||
|
</span>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item>
|
||||||
|
<RiUserLine />
|
||||||
|
个人中心
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item>
|
||||||
|
<RiSettingsLine />
|
||||||
|
账号设置
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-divider />
|
||||||
|
<el-dropdown-item @click="handleLogout">
|
||||||
|
<RiSwitchLine />
|
||||||
|
退出登录
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-header>
|
||||||
|
|
||||||
|
<!-- 主要内容区域 -->
|
||||||
|
<el-main>
|
||||||
|
<div class="dashboard">
|
||||||
|
<!-- 欢迎信息 -->
|
||||||
|
<div class="welcome-section">
|
||||||
|
<h2>欢迎回来,{{ userInfo.username || '玩家' }}</h2>
|
||||||
|
<p>今天是 {{ formatDate(new Date()) }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 数据统计卡片 -->
|
||||||
|
<div class="stats-cards">
|
||||||
|
<el-card shadow="hover" class="stats-card">
|
||||||
|
<div class="stats-content">
|
||||||
|
<div class="stats-info">
|
||||||
|
<h3 class="stats-value">123</h3>
|
||||||
|
<p class="stats-label">游戏时长(小时)</p>
|
||||||
|
</div>
|
||||||
|
<div class="stats-icon">
|
||||||
|
<RiTimerLine style="color: #67c23a; font-size: 40px;" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card shadow="hover" class="stats-card">
|
||||||
|
<div class="stats-content">
|
||||||
|
<div class="stats-info">
|
||||||
|
<h3 class="stats-value">45</h3>
|
||||||
|
<p class="stats-label">游戏等级</p>
|
||||||
|
</div>
|
||||||
|
<div class="stats-icon">
|
||||||
|
<RiMedalLine style="color: #e6a23c; font-size: 40px;" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card shadow="hover" class="stats-card">
|
||||||
|
<div class="stats-content">
|
||||||
|
<div class="stats-info">
|
||||||
|
<h3 class="stats-value">678</h3>
|
||||||
|
<p class="stats-label">游戏金币</p>
|
||||||
|
</div>
|
||||||
|
<div class="stats-icon">
|
||||||
|
<RiCoinLine style="color: #f56c6c; font-size: 40px;" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card shadow="hover" class="stats-card">
|
||||||
|
<div class="stats-content">
|
||||||
|
<div class="stats-info">
|
||||||
|
<h3 class="stats-value">90</h3>
|
||||||
|
<p class="stats-label">成就解锁</p>
|
||||||
|
</div>
|
||||||
|
<div class="stats-icon">
|
||||||
|
<RiStarFill style="color: #909399; font-size: 40px;" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 功能模块 -->
|
||||||
|
<div class="function-modules">
|
||||||
|
<el-card shadow="hover" class="module-card">
|
||||||
|
<div class="module-content">
|
||||||
|
<div class="module-icon">
|
||||||
|
<RiGamepadLine style="color: #409eff; font-size: 60px;" />
|
||||||
|
</div>
|
||||||
|
<h3 class="module-title">游戏信息</h3>
|
||||||
|
<p class="module-desc">查看游戏状态和更新日志</p>
|
||||||
|
<el-button type="primary" size="small">进入</el-button>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card shadow="hover" class="module-card">
|
||||||
|
<div class="module-content">
|
||||||
|
<div class="module-icon">
|
||||||
|
<RiGiftLine style="color: #67c23a; font-size: 60px;" />
|
||||||
|
</div>
|
||||||
|
<h3 class="module-title">礼包中心</h3>
|
||||||
|
<p class="module-desc">领取游戏礼包和活动奖励</p>
|
||||||
|
<el-button type="primary" size="small">进入</el-button>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card shadow="hover" class="module-card">
|
||||||
|
<div class="module-content">
|
||||||
|
<div class="module-icon">
|
||||||
|
<RiCoinLine style="color: #e6a23c; font-size: 60px;" />
|
||||||
|
</div>
|
||||||
|
<h3 class="module-title">充值中心</h3>
|
||||||
|
<p class="module-desc">充值游戏金币和会员服务</p>
|
||||||
|
<el-button type="primary" size="small">进入</el-button>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card shadow="hover" class="module-card">
|
||||||
|
<div class="module-content">
|
||||||
|
<div class="module-icon">
|
||||||
|
<RiChat3Fill style="color: #f56c6c; font-size: 60px;" />
|
||||||
|
</div>
|
||||||
|
<h3 class="module-title">客服中心</h3>
|
||||||
|
<p class="module-desc">联系客服解决游戏问题</p>
|
||||||
|
<el-button type="primary" size="small">进入</el-button>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 最近活动 -->
|
||||||
|
<div class="activities-section">
|
||||||
|
<el-card shadow="hover">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>最近活动</h3>
|
||||||
|
<el-button type="text">查看全部</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-timeline>
|
||||||
|
<el-timeline-item>
|
||||||
|
<template #dot>
|
||||||
|
<RiCalendarLine style="color: #e6a23c;" />
|
||||||
|
</template>
|
||||||
|
<div class="timeline-content">
|
||||||
|
<h4>国庆活动礼包</h4>
|
||||||
|
<p>2023-10-01 至 2023-10-07</p>
|
||||||
|
<p>参与国庆活动,领取丰厚奖励</p>
|
||||||
|
<el-button type="primary" size="small">立即参与</el-button>
|
||||||
|
</div>
|
||||||
|
</el-timeline-item>
|
||||||
|
|
||||||
|
<el-timeline-item>
|
||||||
|
<template #dot>
|
||||||
|
<RiCalendarLine style="color: #409eff;" />
|
||||||
|
</template>
|
||||||
|
<div class="timeline-content">
|
||||||
|
<h4>版本更新公告</h4>
|
||||||
|
<p>2023-09-25</p>
|
||||||
|
<p>游戏版本更新至 v1.2.0,新增多种玩法</p>
|
||||||
|
<el-button type="primary" size="small">查看详情</el-button>
|
||||||
|
</div>
|
||||||
|
</el-timeline-item>
|
||||||
|
|
||||||
|
<el-timeline-item>
|
||||||
|
<template #dot>
|
||||||
|
<RiCalendarLine style="color: #67c23a;" />
|
||||||
|
</template>
|
||||||
|
<div class="timeline-content">
|
||||||
|
<h4>新服开启</h4>
|
||||||
|
<p>2023-09-20</p>
|
||||||
|
<p>新服务器「龙腾四海」正式开启</p>
|
||||||
|
<el-button type="primary" size="small">进入新服</el-button>
|
||||||
|
</div>
|
||||||
|
</el-timeline-item>
|
||||||
|
</el-timeline>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { usePlayerStore } from '@/store/player'
|
||||||
|
import { ElMessage, ElNotification } from 'element-plus'
|
||||||
|
import type { User } from '@/types/user'
|
||||||
|
// 导入Remix Icon组件
|
||||||
|
import {
|
||||||
|
RiArrowDownSLine,
|
||||||
|
RiUserLine,
|
||||||
|
RiSettingsLine,
|
||||||
|
RiSwitchLine,
|
||||||
|
RiTimerLine,
|
||||||
|
RiMedalLine,
|
||||||
|
RiStarFill,
|
||||||
|
RiGamepadLine,
|
||||||
|
RiGiftLine,
|
||||||
|
RiCoinLine,
|
||||||
|
RiChat3Fill,
|
||||||
|
RiCalendarLine
|
||||||
|
} from '@remixicon/vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const playerStore = usePlayerStore()
|
||||||
|
|
||||||
|
// 玩家信息
|
||||||
|
const userInfo = computed(() => playerStore.player as User || { username: '' })
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (date: Date): string => {
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||||
|
const day = String(date.getDate()).padStart(2, '0')
|
||||||
|
const week = ['日', '一', '二', '三', '四', '五', '六'][date.getDay()]
|
||||||
|
return `${year}年${month}月${day}日 星期${week}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 退出登录
|
||||||
|
const handleLogout = async () => {
|
||||||
|
try {
|
||||||
|
// 调用playerStore的logout方法,该方法会调用API并清理本地存储
|
||||||
|
await playerStore.logout()
|
||||||
|
ElMessage.success('退出登录成功')
|
||||||
|
router.push('/player/login')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('退出登录失败:', error)
|
||||||
|
ElMessage.error('退出登录失败,请稍后重试')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时检查登录状态
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!playerStore.isLoggedIn) {
|
||||||
|
router.push('/player/login')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取最新的玩家信息
|
||||||
|
await playerStore.getPlayerInfo()
|
||||||
|
|
||||||
|
// 模拟获取用户游戏数据
|
||||||
|
ElNotification({
|
||||||
|
title: '欢迎回来',
|
||||||
|
message: '您有3个未读消息和1个新活动',
|
||||||
|
type: 'info'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.player-home {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-section h2 {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-section p {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #666;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-value {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #333;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-icon {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-modules {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-card {
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-content {
|
||||||
|
text-align: center;
|
||||||
|
padding: 30px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-icon {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-desc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header h3 {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-content {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-content h4 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin: 0 0 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-content p {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
169
frontend/src/views/player/Login.vue
Normal file
169
frontend/src/views/player/Login.vue
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
<template>
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="login-form">
|
||||||
|
<div class="login-header">
|
||||||
|
<h2>一梦西游玩家服务中心</h2>
|
||||||
|
<p>欢迎登录一梦西游一站式玩家服务中心</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-form
|
||||||
|
ref="loginFormRef"
|
||||||
|
:model="loginForm"
|
||||||
|
:rules="loginRules"
|
||||||
|
label-position="top"
|
||||||
|
>
|
||||||
|
<el-form-item label="游戏账号" prop="username">
|
||||||
|
<el-input
|
||||||
|
v-model="loginForm.username"
|
||||||
|
placeholder="请输入游戏账号"
|
||||||
|
size="large"
|
||||||
|
:prefix-icon="RiUserLine"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="密码" prop="password">
|
||||||
|
<el-input
|
||||||
|
v-model="loginForm.password"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入游戏密码"
|
||||||
|
size="large"
|
||||||
|
show-password
|
||||||
|
:prefix-icon="RiLockLine"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:loading="loading"
|
||||||
|
@click="handleLogin"
|
||||||
|
size="large"
|
||||||
|
block
|
||||||
|
>
|
||||||
|
登录
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<div class="login-footer">
|
||||||
|
<el-link type="primary" :underline="false">忘记密码?</el-link>
|
||||||
|
<el-link type="primary" :underline="false">注册账号</el-link>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { playerLogin } from '@/api/auth'
|
||||||
|
import type { LoginForm } from '@/types/auth'
|
||||||
|
|
||||||
|
// 导入Remix Icon组件
|
||||||
|
import { RiUserLine, RiLockLine } from '@remixicon/vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
|
||||||
|
// 登录表单引用
|
||||||
|
const loginFormRef = ref()
|
||||||
|
|
||||||
|
// 加载状态
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 登录表单
|
||||||
|
const loginForm = reactive<LoginForm>({
|
||||||
|
username: '',
|
||||||
|
password: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表单验证规则
|
||||||
|
const loginRules = {
|
||||||
|
username: [
|
||||||
|
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||||
|
{ min: 3, max: 20, message: '用户名长度在 3 到 20 个字符', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
password: [
|
||||||
|
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||||
|
{ min: 6, max: 20, message: '密码长度在 6 到 20 个字符', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理登录
|
||||||
|
const handleLogin = async () => {
|
||||||
|
// 表单验证
|
||||||
|
if (!loginFormRef.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await loginFormRef.value.validate()
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
// 调用游戏服务端API登录(API层会自动处理存储)
|
||||||
|
const response = await playerLogin(loginForm)
|
||||||
|
|
||||||
|
// 检查响应是否成功(游戏服务端返回code=200表示成功)
|
||||||
|
if (response?.code === 200 && response?.success === true) {
|
||||||
|
ElMessage.success('登录成功')
|
||||||
|
|
||||||
|
// 跳转到玩家首页
|
||||||
|
router.push('/player')
|
||||||
|
} else {
|
||||||
|
throw new Error(response?.message || '登录失败,请稍后重试')
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
ElMessage.error(error?.message || '登录失败,请稍后重试')
|
||||||
|
console.error('登录失败:', error)
|
||||||
|
console.log('详细错误信息:', JSON.stringify(error, null, 2))
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 40px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header h2 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header p {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 输入框图标样式已通过Element Plus的prefix-icon属性自动处理 */
|
||||||
|
</style>
|
||||||
15
frontend/src/vite-env.d.ts
vendored
Normal file
15
frontend/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
// 模块声明
|
||||||
|
declare module '@/store/config' {
|
||||||
|
export const useConfigStore: () => any
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@/store/user' {
|
||||||
|
export const useUserStore: () => any
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@/api/config' {
|
||||||
|
export const configApi: any
|
||||||
|
}
|
||||||
|
|
||||||
31
frontend/tsconfig.json
Normal file
31
frontend/tsconfig.json
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
|
||||||
|
/* Path aliases */
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
10
frontend/tsconfig.node.json
Normal file
10
frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
27
frontend/vite.config.ts
Normal file
27
frontend/vite.config.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 5000,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:3000',
|
||||||
|
changeOrigin: true
|
||||||
|
},
|
||||||
|
'/game-api': {
|
||||||
|
target: 'http://127.0.0.1:8080',
|
||||||
|
changeOrigin: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/game-api/, '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
28
package.json
Normal file
28
package.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "mhxy_web",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"directories": {
|
||||||
|
"doc": "docs"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"commit": "git-cz"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"commitizen": {
|
||||||
|
"path": "node_modules/cz-git"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "commonjs",
|
||||||
|
"devDependencies": {
|
||||||
|
"@commitlint/cli": "^20.1.0",
|
||||||
|
"@commitlint/config-conventional": "^20.0.0",
|
||||||
|
"commitizen": "^4.3.1",
|
||||||
|
"cz-git": "^1.12.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
118
sql/init_mysql.sql
Normal file
118
sql/init_mysql.sql
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
-- 一体化游戏运营平台数据库初始化脚本(MySQL 8.4兼容版)
|
||||||
|
-- 创建日期: 2024-05-20
|
||||||
|
|
||||||
|
-- 创建数据库(如果不存在)
|
||||||
|
CREATE DATABASE IF NOT EXISTS mhxy_web DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 切换到mhxy_web数据库
|
||||||
|
USE mhxy_web;
|
||||||
|
|
||||||
|
-- 创建用户表
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
username VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
password VARCHAR(100) NOT NULL,
|
||||||
|
email VARCHAR(100) UNIQUE NULL,
|
||||||
|
phone VARCHAR(20) UNIQUE NULL,
|
||||||
|
role ENUM('admin', 'player') DEFAULT 'admin' NOT NULL,
|
||||||
|
status ENUM('ACTIVE', 'INACTIVE') DEFAULT 'ACTIVE' NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 创建配置表
|
||||||
|
CREATE TABLE IF NOT EXISTS config (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
`key` VARCHAR(50) UNIQUE NOT NULL,
|
||||||
|
value TEXT NULL,
|
||||||
|
description VARCHAR(100) NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 创建管理员用户(密码:admin123,已通过bcrypt加密)
|
||||||
|
INSERT INTO users (username, password, role, status) VALUES (
|
||||||
|
'admin',
|
||||||
|
'$2a$12$A3KXVvye0Q1WMx3AjqRgb.ea3PTUpJHLyOBrm9Q8PAVMYVxNCvZrO',
|
||||||
|
'admin',
|
||||||
|
'ACTIVE'
|
||||||
|
) ON DUPLICATE KEY UPDATE id = id;
|
||||||
|
|
||||||
|
-- 插入初始配置
|
||||||
|
INSERT INTO config (`key`, `value`, description) VALUES
|
||||||
|
('game_api_url', 'http://127.0.0.1:8080/tool/http', '游戏服务端API地址'),
|
||||||
|
('max_login_attempts', '5', '最大登录尝试次数'),
|
||||||
|
('login_lock_time', '300', '登录锁定时间(秒)')
|
||||||
|
ON DUPLICATE KEY UPDATE id = id;
|
||||||
|
|
||||||
|
-- 插入系统配置
|
||||||
|
INSERT INTO config (`key`, `value`, description) VALUES
|
||||||
|
('website_name', '', '网站名称'),
|
||||||
|
('admin_domain', '', '运营后台域名'),
|
||||||
|
('player_domain', '', '玩家中心域名'),
|
||||||
|
('server_host', '', '后端服务器主机地址'),
|
||||||
|
('server_port', '', '后端服务器端口'),
|
||||||
|
('game_api_url', '', '游戏服务API地址'),
|
||||||
|
('game_psk', '', '游戏服务端的PSK'),
|
||||||
|
('jwt_secret', '', 'JWT密钥'),
|
||||||
|
('jwt_expires_in', '', 'JWT过期时间'),
|
||||||
|
('jwt_refresh_secret', '', 'JWT刷新令牌密钥'),
|
||||||
|
('jwt_refresh_expires_in', '', 'JWT刷新令牌过期时间'),
|
||||||
|
('service_status', 'running', '服务运行状态'),
|
||||||
|
('maintenance_mode', 'false', '维护模式开关')
|
||||||
|
ON DUPLICATE KEY UPDATE id = id;
|
||||||
|
|
||||||
|
-- 创建索引(MySQL 8.4不支持IF NOT EXISTS语法,使用IGNORE错误处理)
|
||||||
|
SET @sql = (SELECT IF(
|
||||||
|
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND INDEX_NAME = 'idx_users_username') > 0,
|
||||||
|
'SELECT "Index idx_users_username already exists" AS message;',
|
||||||
|
'CREATE INDEX idx_users_username ON users(username);'
|
||||||
|
));
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql = (SELECT IF(
|
||||||
|
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND INDEX_NAME = 'idx_users_email') > 0,
|
||||||
|
'SELECT "Index idx_users_email already exists" AS message;',
|
||||||
|
'CREATE INDEX idx_users_email ON users(email);'
|
||||||
|
));
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql = (SELECT IF(
|
||||||
|
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND INDEX_NAME = 'idx_users_phone') > 0,
|
||||||
|
'SELECT "Index idx_users_phone already exists" AS message;',
|
||||||
|
'CREATE INDEX idx_users_phone ON users(phone);'
|
||||||
|
));
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql = (SELECT IF(
|
||||||
|
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'users' AND INDEX_NAME = 'idx_users_status') > 0,
|
||||||
|
'SELECT "Index idx_users_status already exists" AS message;',
|
||||||
|
'CREATE INDEX idx_users_status ON users(status);'
|
||||||
|
));
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @sql = (SELECT IF(
|
||||||
|
(SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'config' AND INDEX_NAME = 'idx_config_key') > 0,
|
||||||
|
'SELECT "Index idx_config_key already exists" AS message;',
|
||||||
|
'CREATE INDEX idx_config_key ON config(`key`);'
|
||||||
|
));
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- 输出初始化完成信息
|
||||||
|
SELECT '数据库初始化完成!' AS message;
|
||||||
|
SELECT '管理员用户名: admin, 密码: admin123' AS admin_info;
|
||||||
Reference in New Issue
Block a user