项目初始化
This commit is contained in:
187
.gitignore
vendored
Normal file
187
.gitignore
vendored
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
# ====================================
|
||||||
|
# 依赖目录
|
||||||
|
# ====================================
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# ====================================
|
||||||
|
# 构建输出
|
||||||
|
# ====================================
|
||||||
|
# 前端构建输出
|
||||||
|
frontend/dist/
|
||||||
|
frontend/.temp/
|
||||||
|
frontend/.cache/
|
||||||
|
|
||||||
|
# 后端构建输出
|
||||||
|
backend/dist/
|
||||||
|
backend/build/
|
||||||
|
|
||||||
|
# ====================================
|
||||||
|
# 环境变量文件
|
||||||
|
# ====================================
|
||||||
|
# 根目录环境变量
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# 前端环境变量
|
||||||
|
frontend/.env.local
|
||||||
|
frontend/.env.*.local
|
||||||
|
|
||||||
|
# 后端环境变量
|
||||||
|
backend/.env
|
||||||
|
backend/.env.local
|
||||||
|
backend/.env.*.local
|
||||||
|
|
||||||
|
# ====================================
|
||||||
|
# 缓存目录
|
||||||
|
# ====================================
|
||||||
|
# TypeScript 缓存
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Vite 缓存
|
||||||
|
frontend/node_modules/.vite/
|
||||||
|
|
||||||
|
# npm 缓存
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# eslint 缓存
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# stylelint 缓存
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle 缓存
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# parcel-bundler 缓存
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Snowpack 缓存
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# ====================================
|
||||||
|
# 日志文件
|
||||||
|
# ====================================
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# ====================================
|
||||||
|
# 运行时数据
|
||||||
|
# ====================================
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# ====================================
|
||||||
|
# 测试覆盖率
|
||||||
|
# ====================================
|
||||||
|
coverage/
|
||||||
|
*.lcov
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# ====================================
|
||||||
|
# 编辑器和 IDE 配置
|
||||||
|
# ====================================
|
||||||
|
.vscode/
|
||||||
|
.trae/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.settings/
|
||||||
|
|
||||||
|
# ====================================
|
||||||
|
# 操作系统生成的文件
|
||||||
|
# ====================================
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
|
||||||
|
# ====================================
|
||||||
|
# 临时文件
|
||||||
|
# ====================================
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
*.bak
|
||||||
|
*.backup
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# ====================================
|
||||||
|
# 数据库文件
|
||||||
|
# ====================================
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
*.db
|
||||||
|
*.db-journal
|
||||||
|
|
||||||
|
# ====================================
|
||||||
|
# 压缩文件
|
||||||
|
# ====================================
|
||||||
|
*.zip
|
||||||
|
*.tar.gz
|
||||||
|
*.rar
|
||||||
|
*.7z
|
||||||
|
|
||||||
|
# ====================================
|
||||||
|
# 文档
|
||||||
|
# ====================================
|
||||||
|
docs/
|
||||||
|
|
||||||
|
# ====================================
|
||||||
|
# 开发工具配置
|
||||||
|
# ====================================
|
||||||
|
.trae/
|
||||||
|
.sass-cache/
|
||||||
|
|
||||||
|
# ====================================
|
||||||
|
# 其他
|
||||||
|
# ====================================
|
||||||
|
# REPL 历史
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# npm pack 输出
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn 完整性文件
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
# Serverless 目录
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox 缓存
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB 本地文件
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS 端口文件
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# 存储 VSCode 版本用于测试 VSCode 扩展
|
||||||
|
.vscode-test
|
||||||
20
backend/.env.development
Normal file
20
backend/.env.development
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# 服务器配置
|
||||||
|
PORT=3000
|
||||||
|
NODE_ENV=development
|
||||||
|
|
||||||
|
# 数据库配置
|
||||||
|
DB_HOST=127.0.0.1
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_USERNAME=root
|
||||||
|
DB_PASSWORD=WANGjx064200
|
||||||
|
DB_DATABASE=mhxy_web_vue
|
||||||
|
|
||||||
|
# JWT密钥
|
||||||
|
JWT_SECRET=your-secret-key-change-in-production
|
||||||
|
JWT_EXPIRES_IN=2h
|
||||||
|
|
||||||
|
# 游戏服务端代理地址
|
||||||
|
GAME_SERVER_PROXY_URL=http://127.0.0.1:8080/tool/http
|
||||||
|
|
||||||
|
# CORS配置
|
||||||
|
CORS_ORIGIN=http://localhost:5173
|
||||||
20
backend/.env.production
Normal file
20
backend/.env.production
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# 服务器配置
|
||||||
|
PORT=3000
|
||||||
|
NODE_ENV=production
|
||||||
|
|
||||||
|
# 数据库配置
|
||||||
|
DB_HOST=127.0.0.1
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_USERNAME=root
|
||||||
|
DB_PASSWORD=WANGjx064200
|
||||||
|
DB_DATABASE=mhxy_web_vue
|
||||||
|
|
||||||
|
# JWT密钥(生产环境请使用更安全的密钥)
|
||||||
|
JWT_SECRET=your-production-secret-key-change-this
|
||||||
|
JWT_EXPIRES_IN=2h
|
||||||
|
|
||||||
|
# 游戏服务端代理地址
|
||||||
|
GAME_SERVER_PROXY_URL=http://127.0.0.1:8080/tool/http
|
||||||
|
|
||||||
|
# CORS配置(生产环境请修改为实际域名)
|
||||||
|
CORS_ORIGIN=http://your-frontend-domain.com
|
||||||
60
backend/database/init.sql
Normal file
60
backend/database/init.sql
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
-- ============================================
|
||||||
|
-- 梦幻西游一站式运营管理平台 - 数据库初始化脚本
|
||||||
|
-- MySQL 8.4 兼容版本
|
||||||
|
-- 创建日期:2026-01-04
|
||||||
|
-- ============================================
|
||||||
|
|
||||||
|
-- 使用数据库
|
||||||
|
USE mhxy_web_vue;
|
||||||
|
|
||||||
|
-- 设置字符集
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 表:admin_users(管理员用户表)
|
||||||
|
-- ============================================
|
||||||
|
DROP TABLE IF EXISTS `admin_users`;
|
||||||
|
CREATE TABLE `admin_users` (
|
||||||
|
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
`username` VARCHAR(64) NOT NULL COMMENT '用户名',
|
||||||
|
`password_hash` VARCHAR(255) NOT NULL COMMENT '密码哈希(bcrypt加密)',
|
||||||
|
`real_name` VARCHAR(50) NULL COMMENT '真实姓名',
|
||||||
|
`role_id` INT NOT NULL DEFAULT 1 COMMENT '角色ID(1:超级管理员)',
|
||||||
|
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态(1:正常, 0:禁用)',
|
||||||
|
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE INDEX `idx_username` (`username`),
|
||||||
|
INDEX `idx_status` (`status`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='管理员用户表';
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 插入默认管理员账号
|
||||||
|
-- 用户名:admin
|
||||||
|
-- 密码:admin123(bcrypt加密,10轮)
|
||||||
|
-- ============================================
|
||||||
|
INSERT INTO `admin_users` (`username`, `password_hash`, `real_name`, `role_id`, `status`)
|
||||||
|
VALUES (
|
||||||
|
'admin',
|
||||||
|
'$2b$10$e1hDPxr/A9nbqbJNJFV9COVPfYt.b6REbrvh2rSrn29I1CEE9tski',
|
||||||
|
'超级管理员',
|
||||||
|
1,
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ============================================
|
||||||
|
-- 验证数据
|
||||||
|
-- ============================================
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
username,
|
||||||
|
real_name,
|
||||||
|
role_id,
|
||||||
|
status,
|
||||||
|
created_at
|
||||||
|
FROM admin_users;
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
|
|
||||||
|
-- 初始化完成
|
||||||
3078
backend/package-lock.json
generated
Normal file
3078
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
backend/package.json
Normal file
34
backend/package.json
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "backend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "nodemon --exec ts-node src/index.ts",
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node dist/index.js"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.13.2",
|
||||||
|
"bcrypt": "^6.0.0",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
|
"express": "^5.2.1",
|
||||||
|
"jsonwebtoken": "^9.0.3",
|
||||||
|
"mysql2": "^3.16.0",
|
||||||
|
"typeorm": "^0.3.28"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bcrypt": "^6.0.0",
|
||||||
|
"@types/cors": "^2.8.19",
|
||||||
|
"@types/express": "^5.0.6",
|
||||||
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
|
"@types/node": "^25.0.3",
|
||||||
|
"nodemon": "^3.1.11",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
19
backend/src/config/database.ts
Normal file
19
backend/src/config/database.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { DataSource } from 'typeorm';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
dotenv.config({ path: path.join(__dirname, '../../.env.development') });
|
||||||
|
|
||||||
|
export const AppDataSource = new DataSource({
|
||||||
|
type: 'mysql',
|
||||||
|
host: process.env.DB_HOST || '127.0.0.1',
|
||||||
|
port: parseInt(process.env.DB_PORT || '3306'),
|
||||||
|
username: process.env.DB_USERNAME || 'root',
|
||||||
|
password: process.env.DB_PASSWORD || '',
|
||||||
|
database: process.env.DB_DATABASE || 'mhxy_web_vue',
|
||||||
|
synchronize: false,
|
||||||
|
logging: process.env.NODE_ENV === 'development',
|
||||||
|
entities: [path.join(__dirname, '../models/**/*.ts')],
|
||||||
|
migrations: [path.join(__dirname, '../migrations/**/*.ts')],
|
||||||
|
subscribers: [path.join(__dirname, '../subscribers/**/*.ts')],
|
||||||
|
});
|
||||||
176
backend/src/controllers/adminAuthController.ts
Normal file
176
backend/src/controllers/adminAuthController.ts
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
// 导入Express的Request和Response类型
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
// 导入bcrypt用于密码加密和验证
|
||||||
|
import bcrypt from 'bcrypt';
|
||||||
|
// 导入jsonwebtoken用于生成和验证JWT令牌
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
// 导入数据源配置
|
||||||
|
import { AppDataSource } from '../config/database';
|
||||||
|
// 导入管理员用户模型
|
||||||
|
import { AdminUser } from '../models/AdminUser';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员认证控制器
|
||||||
|
* 处理管理员登录、登出和获取当前用户信息等认证相关操作
|
||||||
|
*/
|
||||||
|
export class AdminAuthController {
|
||||||
|
/**
|
||||||
|
* 管理员登录
|
||||||
|
* 验证用户名和密码,生成JWT令牌并设置到cookie中
|
||||||
|
* @param req - Express请求对象,包含用户名和密码
|
||||||
|
* @param res - Express响应对象
|
||||||
|
*/
|
||||||
|
async login(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
// 从请求体中获取用户名和密码
|
||||||
|
const { username, password } = req.body;
|
||||||
|
|
||||||
|
// 验证用户名和密码是否为空
|
||||||
|
if (!username || !password) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '用户名和密码不能为空'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取管理员用户仓库
|
||||||
|
const adminUserRepository = AppDataSource.getRepository(AdminUser);
|
||||||
|
// 根据用户名查找管理员用户
|
||||||
|
const adminUser = await adminUserRepository.findOne({
|
||||||
|
where: { username }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 用户不存在
|
||||||
|
if (!adminUser) {
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: '用户名或密码错误'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证密码是否正确
|
||||||
|
const isPasswordValid = await bcrypt.compare(password, adminUser.passwordHash);
|
||||||
|
|
||||||
|
// 密码错误
|
||||||
|
if (!isPasswordValid) {
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: '用户名或密码错误'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查账号状态是否被禁用
|
||||||
|
if (adminUser.status !== 1) {
|
||||||
|
return res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
message: '账号已被禁用'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成JWT令牌,包含用户ID、用户名和角色ID
|
||||||
|
const token = jwt.sign(
|
||||||
|
{
|
||||||
|
id: adminUser.id,
|
||||||
|
username: adminUser.username,
|
||||||
|
roleId: adminUser.roleId
|
||||||
|
},
|
||||||
|
process.env.JWT_SECRET || 'your-secret-key',
|
||||||
|
{ expiresIn: '2h' }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 将令牌设置到cookie中,配置安全选项
|
||||||
|
res.cookie('admin_token', token, {
|
||||||
|
httpOnly: true, // 仅HTTP访问,防止XSS攻击
|
||||||
|
secure: process.env.NODE_ENV === 'production', // 生产环境仅HTTPS
|
||||||
|
sameSite: 'strict', // 防止CSRF攻击
|
||||||
|
maxAge: 2 * 60 * 60 * 1000 // 2小时过期
|
||||||
|
});
|
||||||
|
|
||||||
|
// 返回登录成功响应
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
message: '登录成功',
|
||||||
|
data: {
|
||||||
|
token,
|
||||||
|
user: {
|
||||||
|
id: adminUser.id,
|
||||||
|
username: adminUser.username,
|
||||||
|
realName: adminUser.realName,
|
||||||
|
roleId: adminUser.roleId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('管理员登录失败:', error);
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '服务器内部错误'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理员登出
|
||||||
|
* 清除cookie中的令牌
|
||||||
|
* @param req - Express请求对象
|
||||||
|
* @param res - Express响应对象
|
||||||
|
*/
|
||||||
|
async logout(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
// 清除cookie中的令牌
|
||||||
|
res.clearCookie('admin_token');
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
message: '退出登录成功'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('管理员退出登录失败:', error);
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '服务器内部错误'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前登录用户信息
|
||||||
|
* 根据请求中的管理员ID查询并返回用户信息
|
||||||
|
* @param req - Express请求对象,包含已认证的管理员信息
|
||||||
|
* @param res - Express响应对象
|
||||||
|
*/
|
||||||
|
async getCurrentUser(req: any, res: Response) {
|
||||||
|
try {
|
||||||
|
// 获取管理员用户仓库
|
||||||
|
const adminUserRepository = AppDataSource.getRepository(AdminUser);
|
||||||
|
// 根据请求中的管理员ID查询用户信息
|
||||||
|
const adminUser = await adminUserRepository.findOne({
|
||||||
|
where: { id: req.admin.id }
|
||||||
|
});
|
||||||
|
|
||||||
|
// 用户不存在
|
||||||
|
if (!adminUser) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: '用户不存在'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回用户信息
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
id: adminUser.id,
|
||||||
|
username: adminUser.username,
|
||||||
|
realName: adminUser.realName,
|
||||||
|
roleId: adminUser.roleId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户信息失败:', error);
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '服务器内部错误'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
142
backend/src/controllers/playerAuthController.ts
Normal file
142
backend/src/controllers/playerAuthController.ts
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
export class PlayerAuthController {
|
||||||
|
private readonly gameServerUrl: string;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.gameServerUrl = process.env.GAME_SERVER_PROXY_URL || 'http://127.0.0.1:8080/tool/http';
|
||||||
|
}
|
||||||
|
|
||||||
|
async login(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const { username, password } = req.body;
|
||||||
|
|
||||||
|
if (!username || !password) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: '用户名和密码不能为空'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios.post(
|
||||||
|
`${this.gameServerUrl}?code=auth/login`,
|
||||||
|
{
|
||||||
|
username,
|
||||||
|
password
|
||||||
|
},
|
||||||
|
{
|
||||||
|
proxy: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data.success && response.data.code === 200) {
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
message: '登录成功',
|
||||||
|
data: response.data.data
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: response.data.message || '登录失败'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('玩家登录失败:', error);
|
||||||
|
if (error.response) {
|
||||||
|
return res.status(error.response.status || 500).json({
|
||||||
|
success: false,
|
||||||
|
message: error.response.data?.message || '登录失败'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '服务器内部错误'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async logout(req: any, res: Response) {
|
||||||
|
try {
|
||||||
|
const token = req.headers?.authorization?.replace('Bearer ', '');
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: '未授权'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await axios.post(
|
||||||
|
`${this.gameServerUrl}?code=auth/out_login`,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: token
|
||||||
|
},
|
||||||
|
proxy: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
message: '退出登录成功'
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('玩家退出登录失败:', error);
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '退出登录失败'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAccountInfo(req: any, res: Response) {
|
||||||
|
try {
|
||||||
|
const token = req.headers?.authorization?.replace('Bearer ', '');
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: '未授权'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios.post(
|
||||||
|
`${this.gameServerUrl}?code=account/get_account`,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: token
|
||||||
|
},
|
||||||
|
proxy: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.data.success && response.data.code === 200) {
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
data: response.data.data
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: response.data.message || '获取账号信息失败'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('获取账号信息失败:', error);
|
||||||
|
if (error.response) {
|
||||||
|
return res.status(error.response.status || 500).json({
|
||||||
|
success: false,
|
||||||
|
message: error.response.data?.message || '获取账号信息失败'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '服务器内部错误'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
60
backend/src/index.ts
Normal file
60
backend/src/index.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import express, { Express, Request, Response } from 'express';
|
||||||
|
import cors from 'cors';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
import { AppDataSource } from './config/database';
|
||||||
|
import adminRoutes from './routes/adminRoutes';
|
||||||
|
import playerRoutes from './routes/playerRoutes';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const app: Express = express();
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
app.use(cors({
|
||||||
|
origin: process.env.CORS_ORIGIN || 'http://localhost:5173',
|
||||||
|
credentials: true
|
||||||
|
}));
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
|
||||||
|
app.use('/api/admin', adminRoutes);
|
||||||
|
app.use('/api/player', playerRoutes);
|
||||||
|
|
||||||
|
app.get('/', (req: Request, res: Response) => {
|
||||||
|
res.json({
|
||||||
|
message: '梦幻西游一站式运营管理平台 API 服务',
|
||||||
|
version: '1.0.0'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use((req: Request, res: Response) => {
|
||||||
|
res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: '接口不存在'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use((err: any, req: Request, res: Response, next: any) => {
|
||||||
|
console.error('服务器错误:', err);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: '服务器内部错误'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const startServer = async () => {
|
||||||
|
try {
|
||||||
|
await AppDataSource.initialize();
|
||||||
|
console.log('数据库连接成功');
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`服务器运行在 http://localhost:${PORT}`);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('服务器启动失败:', error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
startServer();
|
||||||
38
backend/src/middleware/adminAuth.ts
Normal file
38
backend/src/middleware/adminAuth.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
|
||||||
|
export interface AuthRequest extends Request {
|
||||||
|
admin?: {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
roleId: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const adminAuthMiddleware = (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const token = req.cookies?.admin_token || req.headers?.authorization?.replace('Bearer ', '');
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: '未授权,请先登录'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'your-secret-key') as any;
|
||||||
|
|
||||||
|
req.admin = {
|
||||||
|
id: decoded.id,
|
||||||
|
username: decoded.username,
|
||||||
|
roleId: decoded.roleId
|
||||||
|
};
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Token无效或已过期'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
31
backend/src/middleware/playerAuth.ts
Normal file
31
backend/src/middleware/playerAuth.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
|
||||||
|
export interface PlayerAuthRequest extends Request {
|
||||||
|
player?: {
|
||||||
|
token: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const playerAuthMiddleware = (req: PlayerAuthRequest, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const token = req.headers?.authorization?.replace('Bearer ', '');
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: '未授权,请先登录'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
req.player = {
|
||||||
|
token
|
||||||
|
};
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(401).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Token无效'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
28
backend/src/models/AdminUser.ts
Normal file
28
backend/src/models/AdminUser.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('admin_users')
|
||||||
|
export class AdminUser {
|
||||||
|
@PrimaryGeneratedColumn('increment')
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 64, unique: true })
|
||||||
|
username: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 255, name: 'password_hash' })
|
||||||
|
passwordHash: string;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', length: 50, nullable: true, name: 'real_name' })
|
||||||
|
realName: string;
|
||||||
|
|
||||||
|
@Column({ type: 'int', default: 1, name: 'role_id' })
|
||||||
|
roleId: number;
|
||||||
|
|
||||||
|
@Column({ type: 'tinyint', default: 1 })
|
||||||
|
status: number;
|
||||||
|
|
||||||
|
@CreateDateColumn({ name: 'created_at' })
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ name: 'updated_at' })
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
14
backend/src/routes/adminRoutes.ts
Normal file
14
backend/src/routes/adminRoutes.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import { AdminAuthController } from '../controllers/adminAuthController';
|
||||||
|
import { adminAuthMiddleware, AuthRequest } from '../middleware/adminAuth';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
const adminAuthController = new AdminAuthController();
|
||||||
|
|
||||||
|
router.post('/login', (req, res) => adminAuthController.login(req, res));
|
||||||
|
|
||||||
|
router.post('/logout', (req, res) => adminAuthController.logout(req, res));
|
||||||
|
|
||||||
|
router.get('/me', adminAuthMiddleware, (req: AuthRequest, res) => adminAuthController.getCurrentUser(req, res));
|
||||||
|
|
||||||
|
export default router;
|
||||||
14
backend/src/routes/playerRoutes.ts
Normal file
14
backend/src/routes/playerRoutes.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import { PlayerAuthController } from '../controllers/playerAuthController';
|
||||||
|
import { playerAuthMiddleware, PlayerAuthRequest } from '../middleware/playerAuth';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
const playerAuthController = new PlayerAuthController();
|
||||||
|
|
||||||
|
router.post('/login', (req, res) => playerAuthController.login(req, res));
|
||||||
|
|
||||||
|
router.post('/logout', (req, res) => playerAuthController.logout(req, res));
|
||||||
|
|
||||||
|
router.get('/account', playerAuthMiddleware, (req: PlayerAuthRequest, res) => playerAuthController.getAccountInfo(req, res));
|
||||||
|
|
||||||
|
export default router;
|
||||||
23
backend/tsconfig.json
Normal file
23
backend/tsconfig.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "commonjs",
|
||||||
|
"lib": ["ES2020"],
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"strictPropertyInitialization": false
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
4
frontend/.env.development
Normal file
4
frontend/.env.development
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# 开发环境变量
|
||||||
|
|
||||||
|
# API 基础地址
|
||||||
|
VITE_API_BASE_URL=http://localhost:3000
|
||||||
4
frontend/.env.production
Normal file
4
frontend/.env.production
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# 生产环境变量
|
||||||
|
|
||||||
|
# API 基础地址(生产环境需要修改为实际地址)
|
||||||
|
VITE_API_BASE_URL=https://api.example.com
|
||||||
24
frontend/.gitignore
vendored
Normal file
24
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
5
frontend/README.md
Normal file
5
frontend/README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Vue 3 + TypeScript + Vite
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||||
|
|
||||||
|
Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).
|
||||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>frontend</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2085
frontend/package-lock.json
generated
Normal file
2085
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
frontend/package.json
Normal file
27
frontend/package.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vue-tsc -b && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@remixicon/vue": "^4.8.0",
|
||||||
|
"axios": "^1.13.2",
|
||||||
|
"naive-ui": "^2.43.2",
|
||||||
|
"pinia": "^3.0.4",
|
||||||
|
"vue": "^3.5.24",
|
||||||
|
"vue-router": "^4.6.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^24.10.1",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
|
"@vue/tsconfig": "^0.8.1",
|
||||||
|
"typescript": "~5.9.3",
|
||||||
|
"vite": "^7.2.4",
|
||||||
|
"vue-tsc": "^3.1.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
57
frontend/src/App.vue
Normal file
57
frontend/src/App.vue
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<template>
|
||||||
|
<n-config-provider :theme="theme" :theme-overrides="themeOverrides" :locale="zhCN" :date-locale="dateZhCN">
|
||||||
|
<n-message-provider>
|
||||||
|
<router-view />
|
||||||
|
</n-message-provider>
|
||||||
|
</n-config-provider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { NConfigProvider, NMessageProvider, darkTheme, zhCN, dateZhCN } from 'naive-ui'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
// 主题配置,默认使用亮色主题
|
||||||
|
const theme = ref(null)
|
||||||
|
// 可以根据需要切换为暗色主题
|
||||||
|
// const theme = ref(darkTheme)
|
||||||
|
|
||||||
|
// 主题覆盖配置
|
||||||
|
const themeOverrides = {
|
||||||
|
common: {
|
||||||
|
primaryColor: '#18a058',
|
||||||
|
primaryColorHover: '#36ad6a',
|
||||||
|
primaryColorPressed: '#0c7a43',
|
||||||
|
primaryColorSuppl: '#36ad6a',
|
||||||
|
borderRadius: '4px'
|
||||||
|
},
|
||||||
|
Button: {
|
||||||
|
borderRadius: '4px'
|
||||||
|
},
|
||||||
|
Input: {
|
||||||
|
borderRadius: '4px'
|
||||||
|
},
|
||||||
|
Card: {
|
||||||
|
borderRadius: '8px'
|
||||||
|
},
|
||||||
|
Modal: {
|
||||||
|
borderRadius: '8px'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
16
frontend/src/api/admin.ts
Normal file
16
frontend/src/api/admin.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export const login = (username: string, password: string) => {
|
||||||
|
return request({
|
||||||
|
url: '/api/admin/login',
|
||||||
|
method: 'post',
|
||||||
|
data: { username, password }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCurrentUser = () => {
|
||||||
|
return request({
|
||||||
|
url: '/api/admin/me',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
23
frontend/src/api/player.ts
Normal file
23
frontend/src/api/player.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export const login = (username: string, password: string) => {
|
||||||
|
return request({
|
||||||
|
url: '/api/player/login',
|
||||||
|
method: 'post',
|
||||||
|
data: { username, password }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const logout = () => {
|
||||||
|
return request({
|
||||||
|
url: '/api/player/logout',
|
||||||
|
method: 'post'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAccountInfo = () => {
|
||||||
|
return request({
|
||||||
|
url: '/api/player/account',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
119
frontend/src/layouts/AdminLayout.vue
Normal file
119
frontend/src/layouts/AdminLayout.vue
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<template>
|
||||||
|
<n-layout style="height: 100vh" has-sider>
|
||||||
|
<n-layout-sider
|
||||||
|
bordered
|
||||||
|
show-trigger
|
||||||
|
collapse-mode="width"
|
||||||
|
:collapsed-width="64"
|
||||||
|
:width="240"
|
||||||
|
:native-scrollbar="false"
|
||||||
|
>
|
||||||
|
<div style="height: 60px; display: flex; align-items: center; justify-content: center; border-bottom: 1px solid #e8e8e8;">
|
||||||
|
<h3 style="margin: 0; color: #18a058;">运营管理</h3>
|
||||||
|
</div>
|
||||||
|
<n-menu
|
||||||
|
:collapsed-width="64"
|
||||||
|
:collapsed-icon-size="22"
|
||||||
|
:options="menuOptions"
|
||||||
|
:value="activeKey"
|
||||||
|
@update:value="handleMenuSelect"
|
||||||
|
/>
|
||||||
|
</n-layout-sider>
|
||||||
|
<n-layout>
|
||||||
|
<n-layout-header bordered style="height: 60px; padding: 0 24px; display: flex; align-items: center; justify-content: space-between;">
|
||||||
|
<div class="header-left">
|
||||||
|
<h2 style="margin: 0;">梦幻西游运营管理系统</h2>
|
||||||
|
</div>
|
||||||
|
<div class="header-right" v-if="adminStore.userInfo">
|
||||||
|
<n-dropdown :options="userMenuOptions" @select="handleUserMenuSelect">
|
||||||
|
<div class="user-dropdown-trigger">
|
||||||
|
<span>{{ adminStore.userInfo.username }}</span>
|
||||||
|
<RiArrowDownSLine style="margin-left: 4px;" />
|
||||||
|
</div>
|
||||||
|
</n-dropdown>
|
||||||
|
</div>
|
||||||
|
</n-layout-header>
|
||||||
|
<n-layout-content style="padding: 24px;">
|
||||||
|
<router-view />
|
||||||
|
</n-layout-content>
|
||||||
|
<n-layout-footer bordered style="padding: 16px; text-align: center;">
|
||||||
|
<p style="margin: 0;">© 2026 梦幻西游一站式运营管理平台 - 运营管理系统</p>
|
||||||
|
</n-layout-footer>
|
||||||
|
</n-layout>
|
||||||
|
</n-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, h } from 'vue'
|
||||||
|
import { NLayout, NLayoutSider, NLayoutHeader, NLayoutContent, NLayoutFooter, NMenu, NDropdown } from 'naive-ui'
|
||||||
|
import { RouterLink, useRoute, useRouter } from 'vue-router'
|
||||||
|
import { useAdminStore } from '@/stores/admin'
|
||||||
|
import { RiDashboardLine, RiArrowDownSLine, RiUserLine, RiLogoutBoxRLine } from '@remixicon/vue'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const adminStore = useAdminStore()
|
||||||
|
|
||||||
|
const activeKey = ref(String(route.name))
|
||||||
|
|
||||||
|
const menuOptions = [
|
||||||
|
{
|
||||||
|
label: () => h(RouterLink, { to: '/admin/dashboard' }, { default: () => '工作台' }),
|
||||||
|
key: 'AdminDashboard',
|
||||||
|
icon: () => h(RiDashboardLine, { size: '20px' })
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const userMenuOptions = [
|
||||||
|
{
|
||||||
|
label: '用户信息',
|
||||||
|
key: 'userInfo',
|
||||||
|
icon: () => h(RiUserLine, { size: '18px' })
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'divider',
|
||||||
|
key: 'd1'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: '退出登录',
|
||||||
|
key: 'logout',
|
||||||
|
icon: () => h(RiLogoutBoxRLine, { size: '18px' })
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const handleMenuSelect = (key: string) => {
|
||||||
|
activeKey.value = key
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUserMenuSelect = (key: string) => {
|
||||||
|
if (key === 'logout') {
|
||||||
|
handleLogout()
|
||||||
|
} else if (key === 'userInfo') {
|
||||||
|
console.log('用户信息')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
adminStore.logout()
|
||||||
|
router.push('/admin/login')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.header-left h2 {
|
||||||
|
color: #18a058;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown-trigger {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-dropdown-trigger:hover {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
39
frontend/src/layouts/PlayerLayout.vue
Normal file
39
frontend/src/layouts/PlayerLayout.vue
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<template>
|
||||||
|
<n-layout style="height: 100vh">
|
||||||
|
<n-layout-header bordered style="height: 60px; padding: 0 24px; display: flex; align-items: center; justify-content: space-between;">
|
||||||
|
<div class="header-left">
|
||||||
|
<h2 style="margin: 0;">梦幻西游玩家服务中心</h2>
|
||||||
|
</div>
|
||||||
|
<div class="header-right" v-if="playerStore.userInfo">
|
||||||
|
<span style="margin-right: 16px;">欢迎, {{ playerStore.userInfo.username }}</span>
|
||||||
|
<n-button type="error" @click="handleLogout">退出登录</n-button>
|
||||||
|
</div>
|
||||||
|
</n-layout-header>
|
||||||
|
<n-layout-content style="padding: 24px;">
|
||||||
|
<router-view />
|
||||||
|
</n-layout-content>
|
||||||
|
<n-layout-footer bordered style="padding: 16px; text-align: center;">
|
||||||
|
<p style="margin: 0;">© 2026 梦幻西游一站式运营管理平台 - 玩家服务中心</p>
|
||||||
|
</n-layout-footer>
|
||||||
|
</n-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { NLayout, NLayoutHeader, NLayoutContent, NLayoutFooter, NButton } from 'naive-ui'
|
||||||
|
import { usePlayerStore } from '@/stores/player'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const playerStore = usePlayerStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
await playerStore.logout()
|
||||||
|
router.push('/player/login')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.header-left h2 {
|
||||||
|
color: #18a058;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
11
frontend/src/main.ts
Normal file
11
frontend/src/main.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import router from './router'
|
||||||
|
import App from './App.vue'
|
||||||
|
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
app.use(createPinia())
|
||||||
|
app.use(router)
|
||||||
|
|
||||||
|
app.mount('#app')
|
||||||
81
frontend/src/router/index.ts
Normal file
81
frontend/src/router/index.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
redirect: '/player'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/player/login',
|
||||||
|
name: 'PlayerLogin',
|
||||||
|
component: () => import('@/views/player/Login.vue'),
|
||||||
|
meta: { title: '玩家登录' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/player',
|
||||||
|
component: () => import('@/layouts/PlayerLayout.vue'),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'dashboard',
|
||||||
|
name: 'PlayerDashboard',
|
||||||
|
component: () => import('@/views/player/Dashboard.vue'),
|
||||||
|
meta: { title: '玩家控制台', requiresAuth: true }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/login',
|
||||||
|
name: 'AdminLogin',
|
||||||
|
component: () => import('@/views/admin/Login.vue'),
|
||||||
|
meta: { title: '管理员登录' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/admin',
|
||||||
|
component: () => import('@/layouts/AdminLayout.vue'),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'dashboard',
|
||||||
|
name: 'AdminDashboard',
|
||||||
|
component: () => import('@/views/admin/Dashboard.vue'),
|
||||||
|
meta: { title: '管理控制台', requiresAdminAuth: true }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
document.title = (to.meta.title as string) || '梦幻西游一站式运营管理平台'
|
||||||
|
|
||||||
|
if (to.path.startsWith('/player')) {
|
||||||
|
if (to.path === '/player/login') {
|
||||||
|
next()
|
||||||
|
} else {
|
||||||
|
const playerToken = sessionStorage.getItem('player_token')
|
||||||
|
if (playerToken) {
|
||||||
|
next()
|
||||||
|
} else {
|
||||||
|
next('/player/login')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (to.path.startsWith('/admin')) {
|
||||||
|
if (to.path === '/admin/login') {
|
||||||
|
next()
|
||||||
|
} else {
|
||||||
|
const adminToken = localStorage.getItem('admin_token')
|
||||||
|
if (adminToken) {
|
||||||
|
next()
|
||||||
|
} else {
|
||||||
|
next('/admin/login')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
60
frontend/src/stores/admin.ts
Normal file
60
frontend/src/stores/admin.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { login as loginApi } from '@/api/admin'
|
||||||
|
|
||||||
|
export const useAdminStore = defineStore('admin', () => {
|
||||||
|
const token = ref<string | null>(localStorage.getItem('admin_token'))
|
||||||
|
const userInfo = ref<any>(JSON.parse(localStorage.getItem('admin_userInfo') || 'null'))
|
||||||
|
|
||||||
|
const setToken = (newToken: string) => {
|
||||||
|
token.value = newToken
|
||||||
|
localStorage.setItem('admin_token', newToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setUserInfo = (info: any) => {
|
||||||
|
userInfo.value = info
|
||||||
|
localStorage.setItem('admin_userInfo', JSON.stringify(info))
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearToken = () => {
|
||||||
|
token.value = null
|
||||||
|
localStorage.removeItem('admin_token')
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearUserInfo = () => {
|
||||||
|
userInfo.value = null
|
||||||
|
localStorage.removeItem('admin_userInfo')
|
||||||
|
}
|
||||||
|
|
||||||
|
const login = async (username: string, password: string) => {
|
||||||
|
try {
|
||||||
|
const response = await loginApi(username, password)
|
||||||
|
const data = response.data
|
||||||
|
if (data.success && data.data) {
|
||||||
|
setToken(data.data.token)
|
||||||
|
setUserInfo(data.data.user)
|
||||||
|
return { success: true, message: data.message || '登录成功' }
|
||||||
|
}
|
||||||
|
return { success: false, message: data.message || '登录失败' }
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('登录失败:', error)
|
||||||
|
return { success: false, message: error.response?.data?.message || '网络错误,请稍后重试' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const logout = () => {
|
||||||
|
clearToken()
|
||||||
|
clearUserInfo()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
userInfo,
|
||||||
|
setToken,
|
||||||
|
setUserInfo,
|
||||||
|
clearToken,
|
||||||
|
clearUserInfo,
|
||||||
|
login,
|
||||||
|
logout
|
||||||
|
}
|
||||||
|
})
|
||||||
61
frontend/src/stores/player.ts
Normal file
61
frontend/src/stores/player.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { login as loginApi, logout as logoutApi, getAccountInfo } from '@/api/player'
|
||||||
|
|
||||||
|
export const usePlayerStore = defineStore('player', () => {
|
||||||
|
const token = ref<string | null>(sessionStorage.getItem('player_token'))
|
||||||
|
const userInfo = ref<any>(null)
|
||||||
|
|
||||||
|
const setToken = (newToken: string) => {
|
||||||
|
token.value = newToken
|
||||||
|
sessionStorage.setItem('player_token', newToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearToken = () => {
|
||||||
|
token.value = null
|
||||||
|
sessionStorage.removeItem('player_token')
|
||||||
|
}
|
||||||
|
|
||||||
|
const login = async (username: string, password: string) => {
|
||||||
|
const response = await loginApi(username, password)
|
||||||
|
if (response.success && response.data) {
|
||||||
|
setToken(response.data)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const logout = async () => {
|
||||||
|
try {
|
||||||
|
await logoutApi()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('退出登录失败:', error)
|
||||||
|
} finally {
|
||||||
|
clearToken()
|
||||||
|
userInfo.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchUserInfo = async () => {
|
||||||
|
try {
|
||||||
|
const response = await getAccountInfo()
|
||||||
|
if (response.success && response.data) {
|
||||||
|
userInfo.value = response.data
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户信息失败:', error)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
userInfo,
|
||||||
|
setToken,
|
||||||
|
clearToken,
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
fetchUserInfo
|
||||||
|
}
|
||||||
|
})
|
||||||
38
frontend/src/utils/request.ts
Normal file
38
frontend/src/utils/request.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
const request = axios.create({
|
||||||
|
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
|
||||||
|
request.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
if (config.url?.startsWith('/api/player/')) {
|
||||||
|
const playerToken = sessionStorage.getItem('player_token')
|
||||||
|
if (playerToken) {
|
||||||
|
config.headers.Authorization = playerToken
|
||||||
|
}
|
||||||
|
} else if (config.url?.startsWith('/api/admin/')) {
|
||||||
|
const adminToken = localStorage.getItem('admin_token')
|
||||||
|
if (adminToken) {
|
||||||
|
config.headers.Authorization = `Bearer ${adminToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
request.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
return response.data
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('请求错误:', error)
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default request
|
||||||
35
frontend/src/views/admin/Dashboard.vue
Normal file
35
frontend/src/views/admin/Dashboard.vue
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<template>
|
||||||
|
<div class="dashboard-container">
|
||||||
|
<n-card title="欢迎来到运营管理系统">
|
||||||
|
<n-space vertical size="large">
|
||||||
|
<n-alert type="success" title="欢迎回来!">
|
||||||
|
感谢您使用梦幻西游一站式运营管理平台。
|
||||||
|
</n-alert>
|
||||||
|
<n-statistic label="系统状态" value="正常运行">
|
||||||
|
<template #suffix>
|
||||||
|
<RiCheckboxCircleLine size="24px" color="#18a058" />
|
||||||
|
</template>
|
||||||
|
</n-statistic>
|
||||||
|
<n-card title="快速导航" size="small">
|
||||||
|
<n-space>
|
||||||
|
<n-button type="primary" disabled>用户管理</n-button>
|
||||||
|
<n-button type="primary" disabled>工单管理</n-button>
|
||||||
|
<n-button type="primary" disabled>公告管理</n-button>
|
||||||
|
<n-button type="primary" disabled>数据看板</n-button>
|
||||||
|
</n-space>
|
||||||
|
</n-card>
|
||||||
|
</n-space>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { NCard, NSpace, NAlert, NStatistic, NButton } from 'naive-ui'
|
||||||
|
import { RiCheckboxCircleLine } from '@remixicon/vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dashboard-container {
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
86
frontend/src/views/admin/Login.vue
Normal file
86
frontend/src/views/admin/Login.vue
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<template>
|
||||||
|
<div class="login-container">
|
||||||
|
<n-card title="管理员登录" style="width: 400px;">
|
||||||
|
<n-form ref="formRef" :model="formValue" :rules="rules" size="large">
|
||||||
|
<n-form-item path="username" label="用户名">
|
||||||
|
<n-input v-model:value="formValue.username" placeholder="请输入用户名" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item path="password" label="密码">
|
||||||
|
<n-input
|
||||||
|
v-model:value="formValue.password"
|
||||||
|
type="password"
|
||||||
|
show-password-on="click"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item>
|
||||||
|
<n-button type="primary" block @click="handleLogin" :loading="loading">
|
||||||
|
登录
|
||||||
|
</n-button>
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { NCard, NForm, NFormItem, NInput, NButton, useMessage } from 'naive-ui'
|
||||||
|
import { useAdminStore } from '@/stores/admin'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const message = useMessage()
|
||||||
|
const adminStore = useAdminStore()
|
||||||
|
|
||||||
|
const formRef = ref()
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const formValue = ref({
|
||||||
|
username: '',
|
||||||
|
password: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
username: {
|
||||||
|
required: true,
|
||||||
|
message: '请输入用户名',
|
||||||
|
trigger: 'blur'
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
required: true,
|
||||||
|
message: '请输入密码',
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
try {
|
||||||
|
await formRef.value?.validate()
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
const result = await adminStore.login(formValue.value.username, formValue.value.password)
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
message.success(result.message)
|
||||||
|
router.push('/admin/dashboard')
|
||||||
|
} else {
|
||||||
|
message.error(result.message)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('登录错误:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
38
frontend/src/views/player/Dashboard.vue
Normal file
38
frontend/src/views/player/Dashboard.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<div class="dashboard-container">
|
||||||
|
<n-card title="账号信息" v-if="playerStore.userInfo">
|
||||||
|
<n-descriptions bordered :column="1">
|
||||||
|
<n-descriptions-item label="用户名">
|
||||||
|
{{ playerStore.userInfo.username }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="账号ID">
|
||||||
|
{{ playerStore.userInfo.accountId }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
<n-descriptions-item label="创建时间">
|
||||||
|
{{ playerStore.userInfo.createdAt }}
|
||||||
|
</n-descriptions-item>
|
||||||
|
</n-descriptions>
|
||||||
|
</n-card>
|
||||||
|
<n-card title="欢迎来到玩家控制台" v-else>
|
||||||
|
<p>正在加载账号信息...</p>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
import { NCard, NDescriptions, NDescriptionsItem } from 'naive-ui'
|
||||||
|
import { usePlayerStore } from '@/stores/player'
|
||||||
|
|
||||||
|
const playerStore = usePlayerStore()
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await playerStore.fetchUserInfo()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dashboard-container {
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
86
frontend/src/views/player/Login.vue
Normal file
86
frontend/src/views/player/Login.vue
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<template>
|
||||||
|
<div class="login-container">
|
||||||
|
<n-card title="玩家登录" style="width: 400px;">
|
||||||
|
<n-form ref="formRef" :model="formValue" :rules="rules" size="large">
|
||||||
|
<n-form-item path="username" label="用户名">
|
||||||
|
<n-input v-model:value="formValue.username" placeholder="请输入用户名" />
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item path="password" label="密码">
|
||||||
|
<n-input
|
||||||
|
v-model:value="formValue.password"
|
||||||
|
type="password"
|
||||||
|
show-password-on="click"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
<n-form-item>
|
||||||
|
<n-button type="primary" block @click="handleLogin" :loading="loading">
|
||||||
|
登录
|
||||||
|
</n-button>
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { NCard, NForm, NFormItem, NInput, NButton, useMessage } from 'naive-ui'
|
||||||
|
import { usePlayerStore } from '@/stores/player'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const message = useMessage()
|
||||||
|
const playerStore = usePlayerStore()
|
||||||
|
|
||||||
|
const formRef = ref()
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const formValue = ref({
|
||||||
|
username: '',
|
||||||
|
password: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
username: {
|
||||||
|
required: true,
|
||||||
|
message: '请输入用户名',
|
||||||
|
trigger: 'blur'
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
required: true,
|
||||||
|
message: '请输入密码',
|
||||||
|
trigger: 'blur'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
try {
|
||||||
|
await formRef.value?.validate()
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
const success = await playerStore.login(formValue.value.username, formValue.value.password)
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
message.success('登录成功')
|
||||||
|
router.push('/player/dashboard')
|
||||||
|
} else {
|
||||||
|
message.error('登录失败,请检查用户名和密码')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('登录错误:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
20
frontend/tsconfig.app.json
Normal file
20
frontend/tsconfig.app.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||||
|
}
|
||||||
7
frontend/tsconfig.json
Normal file
7
frontend/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
26
frontend/tsconfig.node.json
Normal file
26
frontend/tsconfig.node.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"types": ["node"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
13
frontend/vite.config.ts
Normal file
13
frontend/vite.config.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user