项目初始化

This commit is contained in:
Stev_Wang
2026-01-04 17:19:04 +08:00
commit 93aae460af
41 changed files with 6922 additions and 0 deletions

187
.gitignore vendored Normal file
View 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
View 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
View 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
View 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 '角色ID1:超级管理员)',
`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
-- 密码admin123bcrypt加密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

File diff suppressed because it is too large Load Diff

34
backend/package.json Normal file
View 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"
}
}

View 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')],
});

View 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: '服务器内部错误'
});
}
}
}

View 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
View 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();

View 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无效或已过期'
});
}
};

View 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无效'
});
}
};

View 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;
}

View 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;

View 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
View 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"]
}

View File

@@ -0,0 +1,4 @@
# 开发环境变量
# API 基础地址
VITE_API_BASE_URL=http://localhost:3000

4
frontend/.env.production Normal file
View File

@@ -0,0 +1,4 @@
# 生产环境变量
# API 基础地址(生产环境需要修改为实际地址)
VITE_API_BASE_URL=https://api.example.com

24
frontend/.gitignore vendored Normal file
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

27
frontend/package.json Normal file
View 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
View 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
View 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'
})
}

View 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'
})
}

View 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>

View 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
View 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')

View 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

View 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
}
})

View 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
}
})

View 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

View 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>

View 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>

View 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>

View 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>

View 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
View File

@@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View 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
View 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')
}
}
})