Browse Source

初始化提交

master
hanqin 2 weeks ago
commit
9c0bcc23ae
  1. 237
      AGENTS.md
  2. 37
      Build_God_Admin_Frontend/.gitignore
  3. 2
      Build_God_Admin_Frontend/Frontend/.env.development
  4. 2
      Build_God_Admin_Frontend/Frontend/.env.production
  5. 39
      Build_God_Admin_Frontend/Frontend/.gitignore
  6. BIN
      Build_God_Admin_Frontend/Frontend/.vs/Frontend/CopilotIndices/17.14.1204.46620/CodeChunks.db
  7. BIN
      Build_God_Admin_Frontend/Frontend/.vs/Frontend/CopilotIndices/17.14.1204.46620/SemanticSymbols.db
  8. BIN
      Build_God_Admin_Frontend/Frontend/.vs/Frontend/FileContentIndex/386c32c4-b9ee-4dbd-a581-3a705798da36.vsidx
  9. BIN
      Build_God_Admin_Frontend/Frontend/.vs/Frontend/FileContentIndex/4728dbf9-f440-46eb-88e4-4db8163ed89b.vsidx
  10. BIN
      Build_God_Admin_Frontend/Frontend/.vs/Frontend/FileContentIndex/5605984b-62f9-4eaa-8bf4-955fb18f6973.vsidx
  11. BIN
      Build_God_Admin_Frontend/Frontend/.vs/Frontend/FileContentIndex/9255374b-c7f1-458c-a33b-8b234c94b5a0.vsidx
  12. BIN
      Build_God_Admin_Frontend/Frontend/.vs/Frontend/FileContentIndex/a4763d64-25e3-4dbe-bf7f-24febff8dc81.vsidx
  13. 1026
      Build_God_Admin_Frontend/Frontend/.vs/Frontend/config/applicationhost.config
  14. BIN
      Build_God_Admin_Frontend/Frontend/.vs/Frontend/v17/.wsuo
  15. 63
      Build_God_Admin_Frontend/Frontend/.vs/Frontend/v17/DocumentLayout.backup.json
  16. 59
      Build_God_Admin_Frontend/Frontend/.vs/Frontend/v17/DocumentLayout.json
  17. 3
      Build_God_Admin_Frontend/Frontend/.vs/ProjectSettings.json
  18. 6
      Build_God_Admin_Frontend/Frontend/.vs/VSWorkspaceState.json
  19. BIN
      Build_God_Admin_Frontend/Frontend/.vs/slnx.sqlite
  20. 173
      Build_God_Admin_Frontend/Frontend/ADMIN_SYSTEM_README.md
  21. 434
      Build_God_Admin_Frontend/Frontend/API_IMPLEMENTATION_COMPLETE.md
  22. 507
      Build_God_Admin_Frontend/Frontend/API_INTEGRATION_COMPLETE.md
  23. 399
      Build_God_Admin_Frontend/Frontend/API_INTEGRATION_GUIDE.md
  24. 306
      Build_God_Admin_Frontend/Frontend/API_QUICK_REFERENCE.md
  25. 401
      Build_God_Admin_Frontend/Frontend/BACKEND_API_EXAMPLE.md
  26. 501
      Build_God_Admin_Frontend/Frontend/PROJECT_SUMMARY.md
  27. 170
      Build_God_Admin_Frontend/Frontend/QUICK_START.md
  28. 42
      Build_God_Admin_Frontend/Frontend/README.md
  29. 235
      Build_God_Admin_Frontend/Frontend/README_API_SUMMARY.md
  30. 390
      Build_God_Admin_Frontend/Frontend/WHAT_TO_DO_NEXT.md
  31. 1
      Build_God_Admin_Frontend/Frontend/env.d.ts
  32. 13
      Build_God_Admin_Frontend/Frontend/index.html
  33. 3561
      Build_God_Admin_Frontend/Frontend/package-lock.json
  34. 35
      Build_God_Admin_Frontend/Frontend/package.json
  35. BIN
      Build_God_Admin_Frontend/Frontend/public/favicon.ico
  36. 25
      Build_God_Admin_Frontend/Frontend/src/App.vue
  37. 70
      Build_God_Admin_Frontend/Frontend/src/api/auth.ts
  38. 89
      Build_God_Admin_Frontend/Frontend/src/api/bag.ts
  39. 31
      Build_God_Admin_Frontend/Frontend/src/api/character.ts
  40. 114
      Build_God_Admin_Frontend/Frontend/src/api/equipment.ts
  41. 62
      Build_God_Admin_Frontend/Frontend/src/api/index.ts
  42. 32
      Build_God_Admin_Frontend/Frontend/src/api/level.ts
  43. 82
      Build_God_Admin_Frontend/Frontend/src/api/mission.ts
  44. 42
      Build_God_Admin_Frontend/Frontend/src/api/missionProgress.ts
  45. 49
      Build_God_Admin_Frontend/Frontend/src/api/pill.ts
  46. 27
      Build_God_Admin_Frontend/Frontend/src/api/spirit.ts
  47. 16
      Build_God_Admin_Frontend/Frontend/src/api/statistics.ts
  48. 86
      Build_God_Admin_Frontend/Frontend/src/assets/base.css
  49. 1
      Build_God_Admin_Frontend/Frontend/src/assets/logo.svg
  50. 28
      Build_God_Admin_Frontend/Frontend/src/assets/main.css
  51. 105
      Build_God_Admin_Frontend/Frontend/src/components/Header.vue
  52. 41
      Build_God_Admin_Frontend/Frontend/src/components/HelloWorld.vue
  53. 192
      Build_God_Admin_Frontend/Frontend/src/components/Sidebar.vue
  54. 95
      Build_God_Admin_Frontend/Frontend/src/components/TheWelcome.vue
  55. 87
      Build_God_Admin_Frontend/Frontend/src/components/WelcomeItem.vue
  56. 7
      Build_God_Admin_Frontend/Frontend/src/components/icons/IconCommunity.vue
  57. 7
      Build_God_Admin_Frontend/Frontend/src/components/icons/IconDocumentation.vue
  58. 7
      Build_God_Admin_Frontend/Frontend/src/components/icons/IconEcosystem.vue
  59. 7
      Build_God_Admin_Frontend/Frontend/src/components/icons/IconSupport.vue
  60. 19
      Build_God_Admin_Frontend/Frontend/src/components/icons/IconTooling.vue
  61. 41
      Build_God_Admin_Frontend/Frontend/src/constants/theme.ts
  62. 27
      Build_God_Admin_Frontend/Frontend/src/main.ts
  63. 104
      Build_God_Admin_Frontend/Frontend/src/router/index.ts
  64. 73
      Build_God_Admin_Frontend/Frontend/src/stores/auth.ts
  65. 12
      Build_God_Admin_Frontend/Frontend/src/stores/counter.ts
  66. 15
      Build_God_Admin_Frontend/Frontend/src/views/AboutView.vue
  67. 79
      Build_God_Admin_Frontend/Frontend/src/views/AdminLayout.vue
  68. 9
      Build_God_Admin_Frontend/Frontend/src/views/HomeView.vue
  69. 166
      Build_God_Admin_Frontend/Frontend/src/views/LoginView.vue
  70. 365
      Build_God_Admin_Frontend/Frontend/src/views/admin/BagsView.vue
  71. 460
      Build_God_Admin_Frontend/Frontend/src/views/admin/DashboardView.vue
  72. 640
      Build_God_Admin_Frontend/Frontend/src/views/admin/EquipmentsView.vue
  73. 433
      Build_God_Admin_Frontend/Frontend/src/views/admin/LevelsView.vue
  74. 1042
      Build_God_Admin_Frontend/Frontend/src/views/admin/MissionView.vue
  75. 542
      Build_God_Admin_Frontend/Frontend/src/views/admin/PillsView.vue
  76. 296
      Build_God_Admin_Frontend/Frontend/src/views/admin/SettingsView.vue
  77. 375
      Build_God_Admin_Frontend/Frontend/src/views/admin/SpiritsView.vue
  78. 426
      Build_God_Admin_Frontend/Frontend/src/views/admin/UsersView.vue
  79. 12
      Build_God_Admin_Frontend/Frontend/tsconfig.app.json
  80. 11
      Build_God_Admin_Frontend/Frontend/tsconfig.json
  81. 19
      Build_God_Admin_Frontend/Frontend/tsconfig.node.json
  82. 18
      Build_God_Admin_Frontend/Frontend/vite.config.ts
  83. 30
      Build_God_Api/.dockerignore
  84. 52
      Build_God_Api/.gitignore
  85. 25
      Build_God_Api/Build_God_Api.sln
  86. 25
      Build_God_Api/Build_God_Api/Build_God_Api.csproj
  87. 6
      Build_God_Api/Build_God_Api/Build_God_Api.http
  88. 41
      Build_God_Api/Build_God_Api/Common/EnumHelper.cs
  89. 94
      Build_God_Api/Build_God_Api/Controllers/AccountController.cs
  90. 121
      Build_God_Api/Build_God_Api/Controllers/BagController.cs
  91. 100
      Build_God_Api/Build_God_Api/Controllers/CharacterController.cs
  92. 75
      Build_God_Api/Build_God_Api/Controllers/EquipmentController.cs
  93. 24
      Build_God_Api/Build_God_Api/Controllers/Game/TrainingController.cs
  94. 42
      Build_God_Api/Build_God_Api/Controllers/LevelController.cs
  95. 130
      Build_God_Api/Build_God_Api/Controllers/MissionController.cs
  96. 59
      Build_God_Api/Build_God_Api/Controllers/PillController.cs
  97. 43
      Build_God_Api/Build_God_Api/Controllers/ProfessionController.cs
  98. 18
      Build_God_Api/Build_God_Api/Controllers/StatisticsController.cs
  99. 19
      Build_God_Api/Build_God_Api/DB/Account.cs
  100. 50
      Build_God_Api/Build_God_Api/DB/Bag.cs

237
AGENTS.md

@ -0,0 +1,237 @@
# AGENTS.md - Build God Project
This document provides guidelines for agentic coding agents working on this repository.
## Project Overview
Build God is a full-stack application with:
- **Backend**: ASP.NET Core 8.0 Web API (C#)
- **Frontend**: Vue 3 + TypeScript + Vite + Element Plus
---
## 1. Build, Lint, and Test Commands
### Backend (.NET API)
Located in: `BuildGod_Api/`
```bash
# Build the solution
dotnet build BuildGod_Api.sln
# Run the API (from BuildGod_Api directory)
dotnet run --project Build_God_Api/Build_God_Api.csproj
# Build and run with Docker
docker build -t build-god-api BuildGod_Api/
docker run -p 5091:80 build-god-api
```
**Note**: No test framework is currently configured for the backend.
### Frontend (Vue 3)
Located in: `Build_God_Admin_Frontend/Frontend/`
```bash
# Install dependencies
npm install
# Start development server
npm run dev
# Build for production
npm run build
# Type-check only
npm run type-check
# Preview production build
npm run preview
```
**Running a single test**: No tests are currently configured.
---
## 2. Code Style Guidelines
### Backend (.NET/C#)
#### General Conventions
- Use **file-scoped namespaces** (`namespace Build_God_Api.Controllers;`)
- Enable **nullable reference types** (`<Nullable>enable</Nullable>`)
- Use **primary constructors** for dependency injection
- Use **async/await** for all I/O operations
#### Naming Conventions
- **Types/Classes**: PascalCase (`AccountController`, `AccountService`)
- **Methods**: PascalCase (`GetAccount`, `Register`)
- **Properties**: PascalCase (`UserName`, `Email`)
- **Local variables**: camelCase (`accountId`, `existingAccount`)
- **Parameters**: camelCase (`accountName`, `emailAddress`)
- **Interfaces**: Prefix with `I` (`IAccountService`, `ICurrentUserService`)
#### Project Structure
```
Controllers/ # API endpoints
Services/ # Business logic (interface + implementation)
DB/ # Database entities/models
```
#### Error Handling
- Return `BadRequest("error message")` for validation errors
- Return `Ok(result)` for successful operations
- Use try-catch with logging for operations that may fail
- Use `ILogger<T>` for logging
#### Dependency Injection
```csharp
public class AccountController(IAccountService service) : ControllerBase
{
private readonly IAccountService _service = service;
// ...
}
```
#### Route Conventions
- Use `[Route("api/god/[controller]")]`
- Use `[ApiController]` attribute
- Use HTTP method attributes: `[HttpGet]`, `[HttpPost]`, `[HttpPut]`, `[HttpDelete]`
---
### Frontend (Vue 3 + TypeScript)
#### General Conventions
- Use **Composition API** with `<script setup lang="ts">`
- Enable **strict TypeScript** mode
- Use **path aliases**: `@/` maps to `src/`
#### Naming Conventions
- **Components**: PascalCase (`LoginView.vue`, `Sidebar.vue`)
- **Variables/functions**: camelCase (`handleLogin`, `userName`)
- **Types/Interfaces**: PascalCase (`LoginRequest`, `User`)
- **Constants**: UPPER_SNAKE_CASE (or camelCase for simple constants)
- **File names**: kebab-case for utilities, PascalCase for components
#### Project Structure
```
src/
api/ # API calls
components/ # Reusable components
views/ # Page components (often in subdirectories)
stores/ # Pinia stores
router/ # Vue Router configuration
```
#### Imports
- Use `@/` alias for src-relative imports: `import { useAuthStore } from '@/stores/auth'`
- Group imports: external libs → internal imports → types
- Use absolute imports for internal modules
#### TypeScript Guidelines
- Always define types/interfaces for API responses and request payloads
- Use `ref<T>` and `computed<T>` for reactive state
- Avoid `any` - use `unknown` or proper types
#### Vue Component Patterns
```vue
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
const router = useRouter()
const loading = ref(false)
// ...
</script>
<template>
<!-- Use Element Plus components -->
</template>
<style scoped lang="css">
/* Component styles */
</style>
```
#### Pinia Store Pattern
```typescript
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useAuthStore = defineStore('auth', () => {
const token = ref('')
const isAuthenticated = computed(() => !!token.value)
const login = async () => { /* ... */ }
return { token, isAuthenticated, login }
})
```
#### UI Library (Element Plus)
- Use Element Plus components: `ElButton`, `ElInput`, `ElTable`, etc.
- Use `ElMessage` for notifications
- Use `ElMessageBox` for confirmations
- Follow Element Plus prop naming conventions
#### Error Handling
- Use try-catch with async/await
- Show errors with `ElMessage.error()`
- Handle 401 (unauthorized) by redirecting to login
---
## 3. API Integration
### Backend API Base URL
- Development: `http://localhost:5091/api/god/`
- Routes follow pattern: `api/god/{entity}` (e.g., `api/god/account/login`)
### Authentication
- Use JWT Bearer tokens
- Store token in `localStorage` as `auth_token`
- Include in requests: `Authorization: Bearer {token}`
### Common Endpoints
- POST `/api/god/account/register` - Register new account
- POST `/api/god/account/login` - User login
- POST `/api/god/account/login/admin` - Admin login (use: name="admin", password="build_god.123")
---
## 4. Development Workflow
1. **Start Backend**: Run `dotnet run` in `BuildGod_Api/`
2. **Start Frontend**: Run `npm run dev` in `Build_God_Admin_Frontend/Frontend/`
3. **Access**: Frontend runs on `http://localhost:5173` by default
---
## 5. Adding New Features
### Backend
1. Create model in `DB/` folder (extend `BaseEntity`)
2. Create service interface in `Services/`
3. Implement service in `Services/`
4. Create controller in `Controllers/`
5. Register service in `Program.cs`
### Frontend
1. Create API module in `src/api/`
2. Add Pinia store in `src/stores/` (if needed)
3. Create view component in `src/views/`
4. Add route in `src/router/index.ts`
---
## 6. Important Notes
- The admin login credentials are hardcoded: `admin` / `build_god.123`
- Backend runs on port 5091
- Frontend uses Vite proxy for API calls (configured in `vite.config.ts`)
- Use `npm run type-check` before committing to catch TypeScript errors
- Always use `await` with async operations - never ignore promises

37
Build_God_Admin_Frontend/.gitignore

@ -0,0 +1,37 @@
# ============== 依赖目录 ==============
# npm/yarn/pnpm 安装的第三方依赖,体积大,无需提交,部署时重新安装即可
node_modules/
.pnpm-store/
# ============== 打包构建产物 ==============
# Vue-CLI 打包输出目录
dist/
# Vite 打包默认输出目录(和dist二选一即可,兼容写法)
dist-ssr/
# 旧版webpack打包目录
build/
# ============== 环境变量配置文件 ==============
# 包含密钥、接口地址等敏感信息,绝对禁止提交
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# ============== 日志与缓存文件 ==============
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# ============== 编辑器/IDE 配置文件 ==============
.idea/
.vscode/
*.swp
*.swo
.DS_Store # macOS系统自动生成的隐藏文件
# ============== 测试/编译缓存 ==============
coverage/
*.local

2
Build_God_Admin_Frontend/Frontend/.env.development

@ -0,0 +1,2 @@
# 开发环境配置
VITE_API_URL=http://localhost:5091/api/god/

2
Build_God_Admin_Frontend/Frontend/.env.production

@ -0,0 +1,2 @@
# 生产环境配置
VITE_API_URL=https://api.example.com/api

39
Build_God_Admin_Frontend/Frontend/.gitignore

@ -0,0 +1,39 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
.eslintcache
# Cypress
/cypress/videos/
/cypress/screenshots/
# Vitest
__screenshots__/
# Vite
*.timestamp-*-*.mjs

BIN
Build_God_Admin_Frontend/Frontend/.vs/Frontend/CopilotIndices/17.14.1204.46620/CodeChunks.db

Binary file not shown.

BIN
Build_God_Admin_Frontend/Frontend/.vs/Frontend/CopilotIndices/17.14.1204.46620/SemanticSymbols.db

Binary file not shown.

BIN
Build_God_Admin_Frontend/Frontend/.vs/Frontend/FileContentIndex/386c32c4-b9ee-4dbd-a581-3a705798da36.vsidx

Binary file not shown.

BIN
Build_God_Admin_Frontend/Frontend/.vs/Frontend/FileContentIndex/4728dbf9-f440-46eb-88e4-4db8163ed89b.vsidx

Binary file not shown.

BIN
Build_God_Admin_Frontend/Frontend/.vs/Frontend/FileContentIndex/5605984b-62f9-4eaa-8bf4-955fb18f6973.vsidx

Binary file not shown.

BIN
Build_God_Admin_Frontend/Frontend/.vs/Frontend/FileContentIndex/9255374b-c7f1-458c-a33b-8b234c94b5a0.vsidx

Binary file not shown.

BIN
Build_God_Admin_Frontend/Frontend/.vs/Frontend/FileContentIndex/a4763d64-25e3-4dbe-bf7f-24febff8dc81.vsidx

Binary file not shown.

1026
Build_God_Admin_Frontend/Frontend/.vs/Frontend/config/applicationhost.config

File diff suppressed because it is too large

BIN
Build_God_Admin_Frontend/Frontend/.vs/Frontend/v17/.wsuo

Binary file not shown.

63
Build_God_Admin_Frontend/Frontend/.vs/Frontend/v17/DocumentLayout.backup.json

@ -0,0 +1,63 @@
{
"Version": 1,
"WorkspaceRootPath": "D:\\Test\\Build_God\\Build_God_Admin_Frontend\\Frontend\\",
"Documents": [],
"DocumentGroupContainers": [
{
"Orientation": 0,
"VerticalTabListWidth": 256,
"DocumentGroups": [
{
"DockedWidth": 200,
"SelectedChildIndex": -1,
"Children": [
{
"$type": "Bookmark",
"Name": "ST:129:0:{1fc202d4-d401-403c-9834-5b218574bb67}"
},
{
"$type": "Bookmark",
"Name": "ST:128:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}"
},
{
"$type": "Bookmark",
"Name": "ST:136:0:{1fc202d4-d401-403c-9834-5b218574bb67}"
},
{
"$type": "Bookmark",
"Name": "ST:135:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}"
},
{
"$type": "Bookmark",
"Name": "ST:134:0:{1fc202d4-d401-403c-9834-5b218574bb67}"
},
{
"$type": "Bookmark",
"Name": "ST:133:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}"
},
{
"$type": "Bookmark",
"Name": "ST:132:0:{1fc202d4-d401-403c-9834-5b218574bb67}"
},
{
"$type": "Bookmark",
"Name": "ST:131:0:{1fc202d4-d401-403c-9834-5b218574bb67}"
},
{
"$type": "Bookmark",
"Name": "ST:130:0:{1fc202d4-d401-403c-9834-5b218574bb67}"
},
{
"$type": "Bookmark",
"Name": "ST:131:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}"
},
{
"$type": "Bookmark",
"Name": "ST:0:0:{1c4feeaa-4718-4aa9-859d-94ce25d182ba}"
}
]
}
]
}
]
}

59
Build_God_Admin_Frontend/Frontend/.vs/Frontend/v17/DocumentLayout.json

@ -0,0 +1,59 @@
{
"Version": 1,
"WorkspaceRootPath": "D:\\Test\\Build_God\\Build_God_Admin_Frontend\\Frontend\\",
"Documents": [],
"DocumentGroupContainers": [
{
"Orientation": 0,
"VerticalTabListWidth": 256,
"DocumentGroups": [
{
"DockedWidth": 200,
"SelectedChildIndex": -1,
"Children": [
{
"$type": "Bookmark",
"Name": "ST:129:0:{1fc202d4-d401-403c-9834-5b218574bb67}"
},
{
"$type": "Bookmark",
"Name": "ST:128:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}"
},
{
"$type": "Bookmark",
"Name": "ST:136:0:{1fc202d4-d401-403c-9834-5b218574bb67}"
},
{
"$type": "Bookmark",
"Name": "ST:135:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}"
},
{
"$type": "Bookmark",
"Name": "ST:134:0:{1fc202d4-d401-403c-9834-5b218574bb67}"
},
{
"$type": "Bookmark",
"Name": "ST:133:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}"
},
{
"$type": "Bookmark",
"Name": "ST:132:0:{1fc202d4-d401-403c-9834-5b218574bb67}"
},
{
"$type": "Bookmark",
"Name": "ST:131:0:{1fc202d4-d401-403c-9834-5b218574bb67}"
},
{
"$type": "Bookmark",
"Name": "ST:130:0:{1fc202d4-d401-403c-9834-5b218574bb67}"
},
{
"$type": "Bookmark",
"Name": "ST:131:0:{116d2292-e37d-41cd-a077-ebacac4c8cc4}"
}
]
}
]
}
]
}

3
Build_God_Admin_Frontend/Frontend/.vs/ProjectSettings.json

@ -0,0 +1,3 @@
{
"CurrentProjectSetting": null
}

6
Build_God_Admin_Frontend/Frontend/.vs/VSWorkspaceState.json

@ -0,0 +1,6 @@
{
"ExpandedNodes": [
""
],
"PreviewInSolutionExplorer": false
}

BIN
Build_God_Admin_Frontend/Frontend/.vs/slnx.sqlite

Binary file not shown.

173
Build_God_Admin_Frontend/Frontend/ADMIN_SYSTEM_README.md

@ -0,0 +1,173 @@
# 后台管理系统
一个基于 Vue 3 + TypeScript + Vite 的现代化后台管理系统框架。
## 🎨 特性
- ✨ **暗黑主题设计** - 黑白双色优雅设计,舒适的深色主题
- 🔐 **认证系统** - 完整的登录/登出功能
- 📊 **仪表板** - 数据统计和概览
- 👥 **用户管理** - CRUD 操作示例
- 📦 **产品管理** - 完整的产品管理模块
- 📋 **订单管理** - 订单管理系统
- ⚙️ **系统设置** - 系统配置管理
- 🎯 **响应式布局** - 完美适配各种屏幕尺寸
## 🚀 快速开始
### 安装依赖
```bash
npm install
```
### 开发模式
```bash
npm run dev
```
访问 `http://localhost:5173` 查看应用
### 生产构建
```bash
npm run build
```
### 类型检查
```bash
npm run type-check
```
### 预览构建结果
```bash
npm run preview
```
## 📁 项目结构
```
src/
├── assets/ # 静态资源和样式
├── components/ # 可复用组件
│ ├── Header.vue # 顶部导航栏
│ └── Sidebar.vue # 侧边栏菜单
├── router/ # 路由配置
├── stores/ # Pinia 状态管理
│ ├── auth.ts # 认证状态
│ └── counter.ts # 计数器示例
├── views/ # 页面组件
│ ├── LoginView.vue # 登录页面
│ ├── AdminLayout.vue # 后台布局
│ └── admin/ # 后台管理页面
│ ├── DashboardView.vue # 仪表板
│ ├── UsersView.vue # 用户管理
│ ├── ProductsView.vue # 产品管理
│ ├── OrdersView.vue # 订单管理
│ └── SettingsView.vue # 系统设置
└── main.ts # 应用入口
```
## 🔑 认证信息
### 测试账号
- **用户名**: `admin`
- **密码**: `123456`
> 注:当前使用本地模拟认证,实际项目需要连接真实 API 服务器
## 🎯 功能模块
### 仪表板 (Dashboard)
- 数据统计卡片
- 最近订单表格
- 统计数据可视化
### 用户管理
- 用户列表展示
- 搜索功能
- 新增用户
- 编辑用户信息
- 删除用户
- 状态管理(启用/禁用)
### 产品管理
- 产品列表管理
- 产品搜索和过滤
- 新增产品
- 编辑产品
- 删除产品
- 库存管理
- 产品分类
### 订单管理
- 订单列表展示
- 订单搜索和过滤
- 订单状态管理
- 新增/编辑/删除订单
- 支付方式记录
### 系统设置
- 基本设置
- 安全设置
- 备份配置
- 文件上传设置
## 🎨 主题配置
系统使用黑白双色暗黑主题:
- **主色调**: `#667eea` (紫蓝色)
- **背景色**: `#0a0e27` (深蓝黑)
- **卡片色**: `#1f2937` (深灰)
- **文字色**: `#e5e7eb` (浅灰)
## 📦 技术栈
- **Vue 3** - 渐进式 JavaScript 框架
- **TypeScript** - JavaScript 超集
- **Vite** - 下一代前端构建工具
- **Vue Router** - 官方路由库
- **Pinia** - Vue 3 状态管理
- **Element Plus** - UI 组件库
- **Axios** - HTTP 客户端库
## 🔒 路由保护
系统包含路由导航守卫,确保:
- 未登录用户无法访问后台页面
- 已登录用户不能重复进入登录页
- 自动重定向到登录页
## 💾 数据持久化
认证信息保存在 `localStorage` 中:
- `auth_token` - 用户令牌
- `user` - 用户信息
## 🛠 开发建议
1. **状态管理**: 使用 Pinia store 管理全局状态
2. **API 集成**: 将模拟数据替换为真实 API 调用
3. **表单验证**: 为表单添加完善的验证规则
4. **错误处理**: 添加全局错误处理和日志记录
5. **权限管理**: 根据用户角色实现权限控制
## 📝 注意事项
- 当前使用本地状态管理,实际项目需要后端 API 支持
- 表单数据为演示用途,需要根据实际需求修改
- 样式可根据公司品牌进行定制
- 建议在生产环境中启用权限检查和数据验证
## 🤝 贡献
欢迎提交 Issue 和 Pull Request
## 📄 许可证
MIT

434
Build_God_Admin_Frontend/Frontend/API_IMPLEMENTATION_COMPLETE.md

@ -0,0 +1,434 @@
# ✨ API 集成完成!完整总结
## 🎉 已为你完成的工作
### 1. ✅ Axios 配置文件 (`src/api/index.ts`)
完整的 HTTP 客户端配置,包括:
```typescript
✓ axios 实例创建
✓ 请求拦截器 - 自动添加 Authorization header
✓ 响应拦截器 - 自动解析响应、处理错误
✓ Token 过期处理 - 自动清除并跳转登录
✓ 环境变量支持 - 开发/生产不同 API 地址
```
### 2. ✅ API 服务层 (`src/api/auth.ts`)
完整的登录 API 接口定义,包括:
```typescript
✓ 类型定义 (TypeScript)
✓ loginApi() - 登录接口
✓ logoutApi() - 登出接口
✓ getUserInfoApi() - 获取用户信息
✓ refreshTokenApi() - 刷新 token
✓ 详细的 JSDoc 注释
```
### 3. ✅ 认证 Store 更新 (`src/stores/auth.ts`)
集成后端 API 的状态管理:
```typescript
✓ async login() - 调用真实 API
✓ async logout() - 调用登出 API
✓ setUser() - 更新用户信息
✓ localStorage 持久化
✓ 错误处理
```
### 4. ✅ 登录页面更新 (`src/views/LoginView.vue`)
支持异步 API 调用的登录页面:
```typescript
✓ async handleLogin()
✓ 加载状态管理
✓ 错误处理和提示
✓ 回车键提交
✓ 完整的错误消息
```
### 5. ✅ 环境配置文件
```
✓ .env.development - 开发环境 API 地址
✓ .env.production - 生产环境 API 地址
✓ .env.local - 本地覆盖配置
```
### 6. ✅ 完整的文档
```
✓ API_INTEGRATION_GUIDE.md - 详细集成指南
✓ BACKEND_API_EXAMPLE.md - 后端实现参考 (Node.js/Python)
✓ API_INTEGRATION_COMPLETE.md - 完整工作流程说明
✓ API_QUICK_REFERENCE.md - 快速参考卡片
```
## 📂 项目结构
```
src/
├── api/
│ ├── index.ts ← axios 实例 + 拦截器
│ └── auth.ts ← 登录 API 接口定义 ← 可在这里添加更多 API
├── stores/
│ ├── auth.ts ← 认证状态管理(已与 API 集成)
│ └── counter.ts
├── views/
│ ├── LoginView.vue ← 登录页面(已支持异步 API)
│ ├── AdminLayout.vue
│ └── admin/
│ ├── DashboardView.vue
│ ├── UsersView.vue
│ ├── ProductsView.vue
│ ├── OrdersView.vue
│ └── SettingsView.vue
├── components/
│ ├── Header.vue
│ ├── Sidebar.vue
│ └── ...
├── router/
│ └── index.ts
├── main.ts
└── App.vue
root/
├── .env.development ← 开发环境配置
├── .env.production ← 生产环境配置
├── .env.local ← 本地覆盖
├── API_INTEGRATION_GUIDE.md ← 详细教程
├── BACKEND_API_EXAMPLE.md ← 后端代码示例
├── API_INTEGRATION_COMPLETE.md ← 完整说明
└── API_QUICK_REFERENCE.md ← 快速参考
```
## 🔄 完整工作流
### 前端流程
```
1️⃣ 用户在登录页输入用户名和密码
└─> username.value = 'admin'
└─> password.value = '123456'
2️⃣ 点击"登录"按钮
└─> handleLogin() 被调用
3️⃣ 登录方法开始
└─> loading.value = true
└─> authStore.login(username, password) 被调用
4️⃣ Store 中的登录方法
└─> loginApi({ username, password }) 被调用
└─> 返回 Promise
5️⃣ API 函数
└─> http.post('/auth/login', { username, password })
└─> 返回 Promise
6️⃣ Axios 请求拦截器
└─> 检查 localStorage 中是否有 auth_token
└─> 如果有,添加 Authorization: Bearer <token>
└─> 发送 HTTP 请求
7️⃣ HTTP 请求发送到后端
└─> POST http://localhost:3000/api/auth/login
└─> Content-Type: application/json
└─> Body: { "username": "admin", "password": "123456" }
8️⃣ 后端处理请求
└─> 验证用户名和密码
└─> 生成 JWT token
└─> 返回 JSON 响应
9️⃣ Axios 响应拦截器
└─> 检查响应状态码
└─> 如果 401,清除 token 并跳转登录
└─> 提取 response.data 并返回
🔟 Store 接收响应
└─> 检查 response.code === 200
└─> 保存 token 和 user 到 localStorage
└─> 返回 true
1️⃣1️⃣ 组件接收结果
└─> 显示"登录成功"提示
└─> 路由跳转到 /admin/dashboard
✨ 完成!用户已登录
```
## 💻 后端需要做什么
### 最小实现示例 (Node.js Express)
```javascript
app.post('/api/auth/login', (req, res) => {
const { username, password } = req.body
// 验证用户
if (username === 'admin' && password === '123456') {
// 生成 token (使用 JWT)
const token = jwt.sign({ id: 1, username }, 'secret')
// 返回正确格式的响应
res.json({
code: 200,
message: '登录成功',
data: {
token,
user: {
id: '1',
username: 'admin',
email: 'admin@example.com',
role: 'admin'
}
}
})
} else {
res.status(401).json({
code: 401,
message: '用户名或密码错误'
})
}
})
```
### 响应格式必须严格遵守
✅ **必须返回**
```json
{
"code": 200,
"message": "登录成功",
"data": {
"token": "jwt_token_here",
"user": {
"id": "1",
"username": "admin",
"email": "admin@example.com",
"role": "admin"
}
}
}
```
❌ **不要这样返回**
```json
{
"token": "...", // ❌ 缺少 code 和 message
"user": {...}
}
```
## 🚀 立即开始
### 第 1 步:准备后端服务
```bash
# 方案 A: 使用提供的 Node.js 示例
# 参考 BACKEND_API_EXAMPLE.md 创建 server.js
node server.js
# 方案 B: 使用你自己的后端
# 确保实现了 POST /api/auth/login 接口
# 返回上述格式的 JSON
```
### 第 2 步:配置前端 API 地址
编辑 `.env.development`(可选,因为已配置默认值):
```
VITE_API_URL=http://localhost:3000/api
```
### 第 3 步:启动前端开发服务器
```bash
npm run dev
```
### 第 4 步:测试登录
1. 访问 http://localhost:5173
2. 输入用户名和密码
3. 点击登录
4. 应该看到"登录成功"提示并跳转到仪表板
### 第 5 步:验证请求
1. 打开浏览器开发者工具 (F12)
2. 点击 Network 标签
3. 再次登录
4. 看到 POST /api/auth/login 请求
5. 点击查看请求和响应的详细信息
## 📝 代码使用示例
### 在登录页使用
```typescript
// LoginView.vue
import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore()
const success = await authStore.login('admin', '123456')
```
### 在其他组件中获取用户信息
```typescript
import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore()
console.log(authStore.user) // 用户信息
console.log(authStore.token) // JWT token
console.log(authStore.isAuthenticated) // 是否已登录
```
### 创建新的 API 接口
```typescript
// src/api/users.ts
import http from './index'
export const getUsersApi = () => http.get('/users')
export const createUserApi = (data) => http.post('/users', data)
export const updateUserApi = (id, data) => http.put(`/users/${id}`, data)
export const deleteUserApi = (id) => http.delete(`/users/${id}`)
// 在组件中使用
import { getUsersApi } from '@/api/users'
const response = await getUsersApi()
```
## ✅ 完成清单
### 前端完成
- [x] Axios 配置
- [x] 请求/响应拦截器
- [x] API 服务层
- [x] 认证 Store
- [x] 登录页面
- [x] 类型定义
- [x] 环境配置
- [x] 完整文档
### 等待后端完成
- [ ] 实现 POST /api/auth/login 接口
- [ ] 返回正确的 JSON 格式
- [ ] 配置 CORS 允许前端请求
- [ ] 其他业务接口 (可选)
## 🎓 后续学习
### 添加其他 API
参考登录 API,在 `src/api/` 中创建新文件:
```
src/api/
├── index.ts # axios 实例
├── auth.ts # ✓ 已完成
├── users.ts # 待添加
├── products.ts # 待添加
├── orders.ts # 待添加
└── ...
```
### 完整的 CRUD 示例 (用户管理)
```typescript
// src/api/users.ts
import http from './index'
export interface User {
id: number
username: string
email: string
}
// 查 - Read
export const getUsersApi = (page = 1, limit = 10) =>
http.get('/users', { params: { page, limit } })
// 增 - Create
export const createUserApi = (data: User) =>
http.post('/users', data)
// 改 - Update
export const updateUserApi = (id: number, data: Partial<User>) =>
http.put(`/users/${id}`, data)
// 删 - Delete
export const deleteUserApi = (id: number) =>
http.delete(`/users/${id}`)
```
## 🔍 调试技巧
### 查看请求和响应
```typescript
// 在 api/index.ts 中添加日志
instance.interceptors.request.use((config) => {
console.log('📤 发送请求:', config.method?.toUpperCase(), config.url)
console.log('数据:', config.data)
return config
})
instance.interceptors.response.use((response) => {
console.log('📥 收到响应:', response)
return response
})
```
### 检查 localStorage
```typescript
// 在浏览器控制台执行
localStorage.getItem('auth_token') // 查看 token
localStorage.getItem('user') // 查看用户信息
```
### 测试 API 接口
```bash
# 使用 curl
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"123456"}'
# 或使用 Postman/Insomnia GUI 工具
```
## 🎉 总结
你现在拥有:
**完整的前端 API 架构** - 随时可以调用后端接口
**登录示例** - 展示了完整的请求/响应流程
**可扩展的 API 服务层** - 轻松添加更多接口
**完整的文档** - 学习和参考资料
**后端代码示例** - Node.js 和 Python 实现
现在你需要做的只是:
1. 实现后端登录接口
2. 启动后端和前端
3. 测试登录功能
其他所有的 CRUD 操作都可以按照登录示例的方式进行!
祝你开发愉快!🚀
---
📚 **相关文档**
- [详细 API 集成指南](./API_INTEGRATION_GUIDE.md)
- [后端实现参考](./BACKEND_API_EXAMPLE.md)
- [快速参考卡片](./API_QUICK_REFERENCE.md)

507
Build_God_Admin_Frontend/Frontend/API_INTEGRATION_COMPLETE.md

@ -0,0 +1,507 @@
# API 集成完整指南
你好!现在项目已经完全配置好了 axios 和 API 层。以下是完整的说明。
## 🎯 快速概览
### 已为你完成的工作
**Axios 配置** - 完整的 HTTP 客户端配置
**API 服务层** - 登录 API 接口定义
**认证 Store** - 与后端 API 集成的状态管理
**登录页面** - 支持异步 API 调用
**环境配置** - 开发和生产环境变量
**详细文档** - API 集成指南和后端示例
## 📁 项目结构
```
src/
├── api/
│ ├── index.ts ← axios 实例(请求/响应拦截器)
│ └── auth.ts ← 登录 API 接口(可扩展)
├── stores/
│ └── auth.ts ← 认证状态(调用 API)
├── views/
│ └── LoginView.vue ← 登录页面(异步登录)
└── ...
root/
├── .env.development ← 开发环境配置
├── .env.production ← 生产环境配置
├── API_INTEGRATION_GUIDE.md ← API 集成详细指南
├── BACKEND_API_EXAMPLE.md ← 后端实现参考
└── ...
```
## 🔄 工作流程
### 1️⃣ 用户登录流程
```
用户输入用户名和密码
点击"登录"按钮
LoginView.vue 调用 authStore.login(username, password)
authStore 调用 loginApi({ username, password })
axios 发送 POST /api/auth/login 请求
axios 拦截器处理:
- 添加 Authorization 头
- 处理错误响应
后端返回 JSON 响应
前端解析 response.data(自动由拦截器处理)
保存 token 和 user 到 localStorage
路由跳转到仪表板
```
### 2️⃣ 后续请求流程
```
任何组件需要调用 API
导入 API 函数(如 getUsersApi)
await getUsersApi()
axios 请求拦截器自动添加 Authorization: Bearer <token>
后端验证 token
后端返回数据
axios 响应拦截器处理(提取 data)
组件接收数据
```
## 💻 代码实现细节
### axios 实例 (`src/api/index.ts`)
```typescript
import axios from 'axios'
const instance = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api',
timeout: 10000
})
// 请求拦截器 - 自动添加 token
instance.interceptors.request.use((config) => {
const token = localStorage.getItem('auth_token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// 响应拦截器 - 处理错误和 token 过期
instance.interceptors.response.use(
(response) => response.data, // ← 返回 response.data
(error) => {
if (error.response?.status === 401) {
// token 过期,清除并跳转
localStorage.removeItem('auth_token')
window.location.href = '/login'
}
return Promise.reject(error)
}
)
export default instance
```
**关键点:**
1. 所有请求自动添加 `Authorization: Bearer <token>`
2. 所有响应自动提取 `response.data`
3. 401 错误自动清除 token 并跳转
### API 接口定义 (`src/api/auth.ts`)
```typescript
import http from './index'
export interface LoginRequest {
username: string
password: string
}
export interface LoginResponse {
code: number
message: string
data: {
token: string
user: User
}
}
export const loginApi = (credentials: LoginRequest): Promise<LoginResponse> => {
return http.post('/auth/login', credentials)
}
```
**使用方式:**
```typescript
const response = await loginApi({ username: 'admin', password: '123456' })
// response.code === 200
// response.data.token
// response.data.user
```
### 认证 Store (`src/stores/auth.ts`)
```typescript
export const useAuthStore = defineStore('auth', () => {
const login = async (username: string, password: string): Promise<boolean> => {
try {
const response = await loginApi({ username, password })
if (response.code === 200 && response.data) {
token.value = response.data.token
user.value = response.data.user
localStorage.setItem('auth_token', token.value)
return true
}
} catch (error) {
console.error('登录失败:', error)
return false
}
}
return { login, logout, user, token, isAuthenticated }
})
```
### 登录页面 (`src/views/LoginView.vue`)
```typescript
const handleLogin = async () => {
loading.value = true
try {
// 调用异步登录
const success = await authStore.login(username.value, password.value)
if (success) {
ElMessage.success('登录成功')
router.push('/admin/dashboard')
} else {
ElMessage.error('登录失败')
}
} catch (error: any) {
ElMessage.error(error?.message || '登录出错')
} finally {
loading.value = false
}
}
```
## 🛠 后端 API 需要返回什么?
### 登录接口
**端点:** `POST /api/auth/login`
**请求:**
```json
{
"username": "admin",
"password": "123456"
}
```
**响应格式(必须严格按照):**
```json
{
"code": 200,
"message": "登录成功",
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "1",
"username": "admin",
"email": "admin@example.com",
"role": "admin"
}
}
}
```
**关键点:**
- `code` 必须是 200 表示成功
- `data.token` 是 JWT token,后续请求在 `Authorization: Bearer <token>` 中使用
- `data.user` 必须包含至少 `id, username, email, role` 字段
## 📝 添加更多 API 接口
### 例子:用户列表 API
1. **在 `src/api/` 中创建 `users.ts`:**
```typescript
import http from './index'
export interface User {
id: number
username: string
email: string
role: string
}
export interface UsersResponse {
code: number
message: string
data: {
items: User[]
total: number
}
}
// 获取用户列表
export const getUsersApi = (
page: number = 1,
limit: number = 10
): Promise<UsersResponse> => {
return http.get('/users', {
params: { page, limit }
})
}
// 创建用户
export const createUserApi = (data: User): Promise<any> => {
return http.post('/users', data)
}
// 更新用户
export const updateUserApi = (id: number, data: Partial<User>): Promise<any> => {
return http.put(`/users/${id}`, data)
}
// 删除用户
export const deleteUserApi = (id: number): Promise<any> => {
return http.delete(`/users/${id}`)
}
```
2. **在组件中使用:**
```typescript
import { getUsersApi } from '@/api/users'
const users = ref([])
const loading = ref(false)
const fetchUsers = async () => {
loading.value = true
try {
const response = await getUsersApi(1, 10)
if (response.code === 200) {
users.value = response.data.items
}
} catch (error) {
ElMessage.error('获取用户列表失败')
} finally {
loading.value = false
}
}
onMounted(() => {
fetchUsers()
})
```
## 🌐 环境配置
### 开发环境 (`.env.development`)
```
VITE_API_URL=http://localhost:3000/api
```
### 生产环境 (`.env.production`)
```
VITE_API_URL=https://api.example.com/api
```
### 在代码中使用
```typescript
const baseURL = import.meta.env.VITE_API_URL
console.log(baseURL) // 自动根据环境选择
// 或直接在 axios 配置中
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api'
```
## 🧪 测试步骤
### 1. 启动后端服务
```bash
# Node.js Express 示例
node server.js
# 或 Python Flask 示例
python app.py
# 或你自己的后端服务
```
### 2. 验证后端在运行
访问 `http://localhost:3000/api/auth/login` (应该看到 404 或错误,说明服务在运行)
### 3. 使用 cURL 测试登录
```bash
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"123456"}'
```
应该返回:
```json
{
"code": 200,
"message": "登录成功",
"data": {
"token": "...",
"user": {...}
}
}
```
### 4. 启动前端
```bash
npm run dev
```
### 5. 在前端登录
- 访问 http://localhost:5173
- 输入用户名和密码
- 点击登录
- 如果成功,应该跳转到仪表板
### 6. 检查浏览器开发者工具
- **Network 标签:** 查看请求和响应
- **Application 标签:** 查看 localStorage 中的 `auth_token`
- **Console 标签:** 查看是否有错误
## 🔒 安全建议
1. **始终使用 HTTPS** (生产环境)
2. **密码必须加密存储** (使用 bcrypt 等)
3. **使用 JWT 令牌** 而不是 session
4. **设置合理的 token 过期时间** (如 7 天)
5. **实现 token 刷新机制** (可选但推荐)
6. **验证 CORS 配置** 只允许特定域名
7. **记录所有登录尝试** 用于审计
8. **使用速率限制** 防止暴力破解
## 常见问题
### Q: 如何在其他组件中获取当前用户?
```typescript
import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore()
console.log(authStore.user) // 当前用户
console.log(authStore.token) // 当前 token
```
### Q: 如何处理 token 刷新?
参考 `src/api/auth.ts` 中已定义的 `refreshTokenApi` 函数,在响应拦截器中实现 token 刷新逻辑。
### Q: 如何在请求中添加其他头信息?
```typescript
// 在 src/api/index.ts 中修改请求拦截器
instance.interceptors.request.use((config) => {
const token = localStorage.getItem('auth_token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
// 添加其他头
config.headers['X-Custom-Header'] = 'value'
return config
})
```
### Q: 如何处理超时错误?
```typescript
const login = async () => {
try {
await authStore.login(username, password)
} catch (error: any) {
if (error.code === 'ECONNABORTED') {
ElMessage.error('请求超时,请检查网络')
} else {
ElMessage.error(error.message)
}
}
}
```
### Q: 前后端如何联调?
1. 后端启动在 http://localhost:3000
2. 前端启动在 http://localhost:5173
3. 确保 `.env.development``VITE_API_URL=http://localhost:3000/api`
4. 后端需要配置 CORS 允许 http://localhost:5173 访问
### Q: CORS 错误怎么解决?
后端需要配置 CORS:
**Express 示例:**
```javascript
const cors = require('cors')
app.use(cors({
origin: 'http://localhost:5173',
credentials: true
}))
```
**Flask 示例:**
```python
from flask_cors import CORS
CORS(app, resources={r"/api/*": {"origins": "http://localhost:5173"}})
```
## 📚 相关文档
- [API 集成详细指南](./API_INTEGRATION_GUIDE.md) - 完整的 API 使用说明
- [后端 API 实现参考](./BACKEND_API_EXAMPLE.md) - Node.js/Python 后端示例代码
- [项目总结](./PROJECT_SUMMARY.md) - 整个项目的概览
## 🎉 总结
你现在有:
✅ 完整的 axios 配置(请求/响应拦截器)
✅ API 服务层架构
✅ 登录示例(已与后端集成)
✅ 完整的类型定义 (TypeScript)
✅ 环境变量配置
✅ 详细的文档和后端示例
**下一步:**
1. 启动你的后端服务,确保监听 3000 端口
2. 实现后端的 `/api/auth/login` 接口,返回上述格式的 JSON
3. 运行 `npm run dev` 启动前端
4. 测试登录功能
有任何问题,请参考文档或查看相关代码注释!🚀

399
Build_God_Admin_Frontend/Frontend/API_INTEGRATION_GUIDE.md

@ -0,0 +1,399 @@
# API 集成指南
## 概述
项目已完整配置 axios,包括请求拦截器、响应拦截器和 API 服务层。
## 文件结构
```
src/api/
├── index.ts # axios 实例配置(请求/响应拦截器)
└── auth.ts # 登录相关 API 接口定义
```
## 核心概念
### 1. axios 实例配置 (`src/api/index.ts`)
```typescript
// 创建 axios 实例
const instance = axios.create({
baseURL: 'http://localhost:3000/api', // 从环境变量读取
timeout: 10000
})
```
**请求拦截器**:自动在请求头添加 Authorization token
```typescript
instance.interceptors.request.use((config) => {
const token = localStorage.getItem('auth_token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
```
**响应拦截器**:统一处理错误和 token 过期
```typescript
instance.interceptors.response.use(
(response) => response.data, // 成功时返回 data
(error) => {
if (error.response?.status === 401) {
// Token 过期,清除登录状态并跳转到登录页
}
}
)
```
### 2. API 服务层 (`src/api/auth.ts`)
定义所有 API 接口和类型:
```typescript
// 定义请求/响应类型
export interface LoginRequest {
username: string
password: string
}
export interface LoginResponse {
code: number
message: string
data: {
token: string
user: User
}
}
// 定义 API 接口
export const loginApi = (credentials: LoginRequest): Promise<LoginResponse> => {
return http.post('/auth/login', credentials)
}
```
### 3. 状态管理 (`src/stores/auth.ts`)
在 Pinia store 中调用 API:
```typescript
const login = async (username: string, password: string): Promise<boolean> => {
try {
const response = await loginApi({ username, password })
if (response.code === 200 && response.data) {
token.value = response.data.token
user.value = response.data.user
// 保存到本地存储
localStorage.setItem('auth_token', token.value)
localStorage.setItem('user', JSON.stringify(user.value))
return true
}
} catch (error) {
console.error('登录失败:', error)
return false
}
}
```
## 登录流程详解
### 前端请求流程
```
1. 用户在登录页输入用户名和密码
2. 点击"登录"按钮调用 handleLogin()
3. authStore.login(username, password) 被调用
4. loginApi({ username, password }) 发送 HTTP 请求
5. axios 拦截器处理(添加 headers 等)
6. 发送 POST /auth/login 请求到后端
```
### 后端需要返回的数据格式
```json
{
"code": 200,
"message": "登录成功",
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "1",
"username": "admin",
"email": "admin@example.com",
"role": "admin"
}
}
}
```
**字段说明:**
- `code`: HTTP 状态码(200 表示成功)
- `message`: 返回消息
- `data.token`: JWT token,用于后续请求认证
- `data.user`: 用户信息对象
- `id`: 用户 ID
- `username`: 用户名
- `email`: 邮箱
- `role`: 用户角色(admin 或 user)
## 环境配置
### `.env.development` (开发环境)
```
VITE_API_URL=http://localhost:3000/api
```
### `.env.production` (生产环境)
```
VITE_API_URL=https://api.example.com/api
```
### 在代码中获取环境变量
```typescript
const baseURL = import.meta.env.VITE_API_URL || 'http://localhost:3000/api'
```
## 使用示例
### 1. 调用登录 API
```typescript
import { loginApi } from '@/api/auth'
const response = await loginApi({
username: 'admin',
password: '123456'
})
if (response.code === 200) {
console.log('登录成功,用户信息:', response.data.user)
console.log('Token:', response.data.token)
}
```
### 2. 在组件中使用 store
```vue
<script setup lang="ts">
import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore()
const login = async () => {
const success = await authStore.login('admin', '123456')
if (success) {
// 登录成功,跳转
router.push('/admin/dashboard')
}
}
</script>
```
### 3. 创建新的 API 接口
`src/api/` 中创建新文件,如 `users.ts`
```typescript
import http from './index'
export interface User {
id: number
username: string
email: string
role: string
}
// 获取用户列表
export const getUsersApi = (page: number, limit: number) => {
return http.get('/users', {
params: { page, limit }
})
}
// 创建用户
export const createUserApi = (data: User) => {
return http.post('/users', data)
}
// 更新用户
export const updateUserApi = (id: number, data: Partial<User>) => {
return http.put(`/users/${id}`, data)
}
// 删除用户
export const deleteUserApi = (id: number) => {
return http.delete(`/users/${id}`)
}
```
## 错误处理
### 全局错误处理
axios 响应拦截器已处理以下情况:
| 状态码 | 处理方式 |
|-------|--------|
| 401 | Token 过期,清除登录状态,跳转到登录页 |
| 其他 4xx/5xx | 返回错误信息 |
| 网络错误 | 返回"网络错误"提示 |
### 在组件中处理错误
```typescript
try {
const response = await loginApi({ username, password })
// 处理成功
} catch (error: any) {
ElMessage.error(error?.message || '登录失败')
}
```
## 请求示例
### POST 请求 (登录)
```typescript
// 请求
POST /api/auth/login
Content-Type: application/json
Authorization: Bearer <token>
{
"username": "admin",
"password": "123456"
}
// 响应
{
"code": 200,
"message": "登录成功",
"data": {
"token": "...",
"user": { ... }
}
}
```
### GET 请求 (获取列表)
```typescript
// 请求
GET /api/users?page=1&limit=10
Authorization: Bearer <token>
// 响应
{
"code": 200,
"message": "成功",
"data": {
"items": [...],
"total": 100
}
}
```
### PUT 请求 (更新)
```typescript
// 请求
PUT /api/users/1
Content-Type: application/json
Authorization: Bearer <token>
{
"username": "new_name",
"email": "new@email.com"
}
// 响应
{
"code": 200,
"message": "更新成功",
"data": { ... }
}
```
## 常见问题
### Q: Token 过期怎么处理?
A: 后端返回 401 时,拦截器会自动清除 token 并跳转到登录页。
### Q: 如何添加其他请求头?
A: 修改 `src/api/index.ts` 中的拦截器:
```typescript
instance.interceptors.request.use((config) => {
config.headers['Custom-Header'] = 'value'
return config
})
```
### Q: 如何处理超时?
A: 已在 axios 配置中设置 `timeout: 10000`(10秒)。修改 `src/api/index.ts` 中的 `timeout` 值。
### Q: 如何调用 API 时显示加载状态?
A: 在组件中使用 `loading` ref:
```typescript
const loading = ref(false)
loading.value = true
try {
const response = await loginApi(...)
} finally {
loading.value = false
}
```
## 集成后端步骤
1. **启动后端服务**
```bash
# 后端服务应该运行在 http://localhost:3000
```
2. **更新 API 地址** (如果不同)
```
修改 .env.development 中的 VITE_API_URL
```
3. **确保后端返回正确格式**
```json
{
"code": 200,
"message": "...",
"data": { ... }
}
```
4. **测试登录流程**
- 访问 http://localhost:5173
- 输入用户名和密码
- 查看浏览器控制台的网络请求
5. **检查 Token 存储**
- 打开浏览器开发者工具 → Application → LocalStorage
- 应该看到 `auth_token``user` 字段
## 下一步
- [ ] 集成后端登录接口
- [ ] 添加用户列表 API (`src/api/users.ts`)
- [ ] 添加产品 API (`src/api/products.ts`)
- [ ] 添加订单 API (`src/api/orders.ts`)
- [ ] 实现 token 刷新机制
- [ ] 添加错误日志上报

306
Build_God_Admin_Frontend/Frontend/API_QUICK_REFERENCE.md

@ -0,0 +1,306 @@
# API 快速参考卡片
## 📋 一页纸速查表
### 项目文件对应关系
| 文件 | 作用 | 修改频率 |
|-----|------|--------|
| `src/api/index.ts` | axios 配置 + 拦截器 | 很少 |
| `src/api/auth.ts` | API 接口定义 | 经常 |
| `src/stores/auth.ts` | 认证状态管理 | 偶尔 |
| `src/views/LoginView.vue` | 登录页面 | 偶尔 |
| `.env.development` | 开发环境 API 地址 | 很少 |
| `.env.production` | 生产环境 API 地址 | 很少 |
### 工作流程简图
```
前端组件
import { loginApi } from '@/api/auth'
await loginApi({ username, password })
axios instance (请求拦截器添加 token)
POST /api/auth/login
后端返回 JSON
axios instance (响应拦截器提取 data)
return response.data
前端接收数据
```
### 常用代码片段
**1. 登录**
```typescript
import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore()
const success = await authStore.login('admin', '123456')
```
**2. 获取当前用户**
```typescript
const authStore = useAuthStore()
console.log(authStore.user)
console.log(authStore.token)
```
**3. 登出**
```typescript
const authStore = useAuthStore()
await authStore.logout()
```
**4. 创建新 API 接口**
```typescript
// src/api/users.ts
import http from './index'
export const getUsersApi = () => {
return http.get('/users')
}
// 组件中使用
import { getUsersApi } from '@/api/users'
const response = await getUsersApi()
```
### 后端需要的东西
**登录接口**
```
POST /api/auth/login
请求:
{
"username": "admin",
"password": "123456"
}
响应:
{
"code": 200,
"message": "登录成功",
"data": {
"token": "jwt_token_here",
"user": {
"id": "1",
"username": "admin",
"email": "admin@example.com",
"role": "admin"
}
}
}
```
**所有其他接口**
```
Authorization: Bearer <token> (自动添加)
返回格式:
{
"code": 200,
"message": "...",
"data": {...}
}
```
### 调试技巧
**1. 查看网络请求**
- 打开浏览器开发者工具 (F12)
- 点击 Network 标签
- 登录看看 POST /api/auth/login 的请求和响应
**2. 查看存储的 token**
- 打开浏览器开发者工具 (F12)
- 点击 Application 标签
- 找到 LocalStorage
- 查看 `auth_token``user`
**3. 打印错误**
```typescript
try {
await loginApi(...)
} catch (error) {
console.log(error) // 查看具体错误
}
```
### 环境变量配置
**开发环境** (`.env.development`)
```
VITE_API_URL=http://localhost:3000/api
```
**生产环境** (`.env.production`)
```
VITE_API_URL=https://api.yourdomain.com/api
```
**在代码中使用**
```typescript
console.log(import.meta.env.VITE_API_URL)
```
### 常见错误解决
| 错误 | 原因 | 解决 |
|-----|------|------|
| CORS error | 跨域问题 | 后端配置 CORS |
| 401 | token 过期 | 自动清除,跳转登录 |
| 404 | 接口不存在 | 检查后端路由 |
| 500 | 服务器错误 | 检查后端日志 |
| 网络错误 | 后端未启动 | 启动后端服务 |
### 完整登录示例
```typescript
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { ElMessage } from 'element-plus'
const router = useRouter()
const authStore = useAuthStore()
const username = ref('admin')
const password = ref('123456')
const loading = ref(false)
const handleLogin = async () => {
loading.value = true
try {
// 调用异步登录
const success = await authStore.login(username.value, password.value)
if (success) {
ElMessage.success('登录成功')
router.push('/admin/dashboard')
} else {
ElMessage.error('登录失败')
}
} catch (error: any) {
ElMessage.error(error?.message || '登录错误')
} finally {
loading.value = false
}
}
</script>
<template>
<input v-model="username" type="text" placeholder="用户名" />
<input v-model="password" type="password" placeholder="密码" />
<button @click="handleLogin" :disabled="loading">
{{ loading ? '登录中...' : '登录' }}
</button>
</template>
```
### API 调用模板
```typescript
// 1. 创建接口文件 src/api/xxx.ts
import http from './index'
export interface XXXRequest {
// 请求参数
}
export interface XXXResponse {
code: number
message: string
data: {
// 响应数据
}
}
export const xxxApi = (params: XXXRequest): Promise<XXXResponse> => {
return http.post('/xxx', params)
}
// 2. 在组件中使用
import { xxxApi } from '@/api/xxx'
const response = await xxxApi({ /* 参数 */ })
if (response.code === 200) {
// 成功处理
}
```
### HTTP 方法速查
```typescript
// GET 请求
http.get('/users')
http.get('/users?page=1&limit=10')
http.get('/users', { params: { page: 1 } })
// POST 请求
http.post('/users', { name: 'John' })
// PUT 请求
http.put('/users/1', { name: 'Jane' })
// DELETE 请求
http.delete('/users/1')
// PATCH 请求
http.patch('/users/1', { name: 'Bob' })
```
### token 在请求中的流程
```
1. 用户登录 → token 保存到 localStorage
2. 请求拦截器检查 localStorage 中的 token
3. 如果有 token,添加到请求头:Authorization: Bearer <token>
4. 后端验证 token
5. 后端返回数据或 401 错误
6. 响应拦截器检查状态码
7. 如果 401,清除 token 并跳转登录页
```
### 测试后端接口的命令
```bash
# 测试登录
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"123456"}'
# 获取响应中的 token
# 复制 token 值
# 测试其他接口(需要 token)
curl -X GET http://localhost:3000/api/users \
-H "Authorization: Bearer YOUR_TOKEN_HERE"
```
## 🚀 快速启动清单
- [ ] 后端启动在 http://localhost:3000
- [ ] 后端实现 POST /api/auth/login 接口
- [ ] 后端返回正确的 JSON 格式
- [ ] 前端运行 `npm run dev`
- [ ] 前端 .env.development 中 VITE_API_URL 指向后端
- [ ] 打开浏览器访问 http://localhost:5173
- [ ] 输入用户名和密码登录
- [ ] 查看浏览器 Network 标签验证请求
- [ ] 检查 localStorage 中是否保存了 token
- [ ] 登录成功后跳转到仪表板
## 📞 获取帮助
1. 查看 `API_INTEGRATION_GUIDE.md` - 详细教程
2. 查看 `BACKEND_API_EXAMPLE.md` - 后端代码示例
3. 查看组件代码注释
4. 打开浏览器开发者工具查看错误
5. 检查后端日志

401
Build_God_Admin_Frontend/Frontend/BACKEND_API_EXAMPLE.md

@ -0,0 +1,401 @@
# 后端 API 实现参考
这是一个使用 Node.js + Express 的简单后端示例,展示如何实现登录接口。
## 快速开始 (Node.js + Express)
### 1. 初始化项目
```bash
mkdir admin-backend
cd admin-backend
npm init -y
npm install express cors jsonwebtoken bcryptjs dotenv
npm install -D nodemon typescript ts-node
```
### 2. 创建 `server.js`
```javascript
const express = require('express')
const cors = require('cors')
const jwt = require('jsonwebtoken')
require('dotenv').config()
const app = express()
const PORT = process.env.PORT || 3000
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key'
// 中间件
app.use(cors())
app.use(express.json())
// 模拟用户数据
const users = [
{
id: '1',
username: 'admin',
password: '123456', // 实际应该加密存储
email: 'admin@example.com',
role: 'admin'
},
{
id: '2',
username: 'user',
password: '123456',
email: 'user@example.com',
role: 'user'
}
]
// 登录接口
app.post('/api/auth/login', (req, res) => {
const { username, password } = req.body
// 验证输入
if (!username || !password) {
return res.status(400).json({
code: 400,
message: '用户名和密码不能为空'
})
}
// 查找用户
const user = users.find(u => u.username === username && u.password === password)
if (!user) {
return res.status(401).json({
code: 401,
message: '用户名或密码错误'
})
}
// 生成 JWT token
const token = jwt.sign(
{ id: user.id, username: user.username, role: user.role },
JWT_SECRET,
{ expiresIn: '7d' }
)
// 返回成功响应
res.json({
code: 200,
message: '登录成功',
data: {
token,
user: {
id: user.id,
username: user.username,
email: user.email,
role: user.role
}
}
})
})
// 登出接口
app.post('/api/auth/logout', (req, res) => {
res.json({
code: 200,
message: '登出成功'
})
})
// 获取用户信息接口 (需要认证)
app.get('/api/auth/userinfo', authenticateToken, (req, res) => {
const user = users.find(u => u.id === req.user.id)
if (!user) {
return res.status(404).json({
code: 404,
message: '用户不存在'
})
}
res.json({
code: 200,
message: '获取成功',
data: {
id: user.id,
username: user.username,
email: user.email,
role: user.role
}
})
})
// Token 验证中间件
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization']
const token = authHeader && authHeader.split(' ')[1] // Bearer <token>
if (!token) {
return res.status(401).json({
code: 401,
message: 'Token 缺失'
})
}
jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) {
return res.status(401).json({
code: 401,
message: 'Token 无效或已过期'
})
}
req.user = user
next()
})
}
// 启动服务器
app.listen(PORT, () => {
console.log(\`✅ 服务器运行在 http://localhost:\${PORT}\`)
console.log(\`📝 API 文档: http://localhost:\${PORT}/api/docs\`)
})
```
### 3. 创建 `.env`
```env
PORT=3000
JWT_SECRET=your-super-secret-key-change-in-production
NODE_ENV=development
```
### 4. 运行服务器
```bash
node server.js
# 或使用 nodemon 自动重启
npx nodemon server.js
```
## API 端点说明
### 登录接口
**请求**
```
POST /api/auth/login
Content-Type: application/json
{
"username": "admin",
"password": "123456"
}
```
**成功响应 (200)**
```json
{
"code": 200,
"message": "登录成功",
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "1",
"username": "admin",
"email": "admin@example.com",
"role": "admin"
}
}
}
```
**错误响应 (401)**
```json
{
"code": 401,
"message": "用户名或密码错误"
}
```
### 获取用户信息接口
**请求**
```
GET /api/auth/userinfo
Authorization: Bearer <token>
```
**成功响应**
```json
{
"code": 200,
"message": "获取成功",
"data": {
"id": "1",
"username": "admin",
"email": "admin@example.com",
"role": "admin"
}
}
```
## 使用 cURL 测试
```bash
# 登录
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"123456"}'
# 获取用户信息
curl -X GET http://localhost:3000/api/auth/userinfo \
-H "Authorization: Bearer YOUR_TOKEN_HERE"
```
## 使用 Postman 测试
1. 打开 Postman
2. 创建 POST 请求到 `http://localhost:3000/api/auth/login`
3. 在 Body → raw → JSON 中输入:
```json
{
"username": "admin",
"password": "123456"
}
```
4. 点击 Send
5. 复制响应中的 token
## Python Flask 示例
```python
from flask import Flask, request, jsonify
from flask_cors import CORS
from functools import wraps
import jwt
from datetime import datetime, timedelta
app = Flask(__name__)
CORS(app)
app.config['JSON_AS_ASCII'] = False
SECRET_KEY = 'your-secret-key'
# 模拟用户数据
USERS = [
{'id': '1', 'username': 'admin', 'password': '123456', 'email': 'admin@example.com', 'role': 'admin'},
{'id': '2', 'username': 'user', 'password': '123456', 'email': 'user@example.com', 'role': 'user'}
]
def token_required(f):
@wraps(f)
def decorated(*args, **kwargs):
token = request.headers.get('Authorization', '').replace('Bearer ', '')
if not token:
return jsonify({'code': 401, 'message': 'Token 缺失'}), 401
try:
data = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
request.user_id = data['id']
except:
return jsonify({'code': 401, 'message': 'Token 无效'}), 401
return f(*args, **kwargs)
return decorated
@app.route('/api/auth/login', methods=['POST'])
def login():
data = request.json
username = data.get('username')
password = data.get('password')
if not username or not password:
return jsonify({'code': 400, 'message': '用户名和密码不能为空'}), 400
user = next((u for u in USERS if u['username'] == username and u['password'] == password), None)
if not user:
return jsonify({'code': 401, 'message': '用户名或密码错误'}), 401
token = jwt.encode(
{'id': user['id'], 'username': user['username']},
SECRET_KEY,
algorithm='HS256'
)
return jsonify({
'code': 200,
'message': '登录成功',
'data': {
'token': token,
'user': {
'id': user['id'],
'username': user['username'],
'email': user['email'],
'role': user['role']
}
}
})
@app.route('/api/auth/userinfo', methods=['GET'])
@token_required
def get_userinfo():
user = next((u for u in USERS if u['id'] == request.user_id), None)
if not user:
return jsonify({'code': 404, 'message': '用户不存在'}), 404
return jsonify({
'code': 200,
'message': '获取成功',
'data': {
'id': user['id'],
'username': user['username'],
'email': user['email'],
'role': user['role']
}
})
if __name__ == '__main__':
app.run(debug=True, port=3000)
```
## Java Spring Boot 示例
```java
@RestController
@RequestMapping("/api/auth")
@CrossOrigin(origins = "*")
public class AuthController {
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
// 验证用户
User user = authenticateUser(request.getUsername(), request.getPassword());
if (user == null) {
return ResponseEntity.status(401).body(new ApiResponse(401, "用户名或密码错误"));
}
// 生成 JWT token
String token = JwtUtils.generateToken(user);
return ResponseEntity.ok(new ApiResponse(200, "登录成功",
new LoginResponse(token, user)));
}
@GetMapping("/userinfo")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<?> getUserInfo() {
User user = getCurrentUser();
return ResponseEntity.ok(new ApiResponse(200, "获取成功", user));
}
}
```
## 注意事项
1. **密码加密**:在生产环境中,必须加密存储密码(使用 bcrypt 等)
2. **HTTPS**:生产环境必须使用 HTTPS
3. **CORS**:根据前端域名配置 CORS
4. **Token 有效期**:建议设置合理的过期时间(如 7 天)
5. **错误处理**:返回统一的错误格式
6. **日志记录**:记录所有登录尝试
7. **速率限制**:防止暴力破解
## 与前端集成
1. 启动后端服务:`node server.js`
2. 启动前端开发服务:`npm run dev`
3. 前端会自动从 `http://localhost:3000/api` 调用后端接口
4. 在登录页输入 `admin / 123456` 进行测试

501
Build_God_Admin_Frontend/Frontend/PROJECT_SUMMARY.md

@ -0,0 +1,501 @@
# 🎉 后台管理系统 - 项目完成总结
## ✨ 项目概述
一个现代化的 Vue 3 + TypeScript 后台管理系统框架,具有完整的登录认证、用户管理、产品管理、订单管理等功能模块,采用优雅的暗黑主题设计。
---
## 🎨 设计主题
### 颜色方案(黑白双色暗黑风格)
```css
主色调:#667eea (紫蓝色) - 用于强调和交互元素
副色调:#764ba2 (深紫) - 用于渐变和悬停效果
背景色:#0a0e27 (深蓝黑) - 主要背景
卡片色:#1f2937 (深灰) - 内容卡片背景
边框色:#374151 (浅深灰) - 边框分割线
文字色:#e5e7eb (浅灰) - 主文本
辅文字:#9ca3af (中灰) - 辅助文本
成功:#22c55e (绿色)
警告:#f59e0b (橙色)
错误:#ef4444 (红色)
信息:#3b82f6 (蓝色)
```
### 设计风格
- ✅ 极简现代设计
- ✅ 深色护眼主题
- ✅ 圆角柔和设计
- ✅ 平滑过渡动画
- ✅ 响应式布局
---
## 📁 完整的文件清单
### 核心文件
```
✅ src/main.ts - 应用入口,集成 Element Plus
✅ src/App.vue - 根组件,简化为路由容器
✅ src/router/index.ts - 路由配置(含导航守卫)
✅ src/assets/main.css - 暗黑主题全局样式
```
### 状态管理
```
✅ src/stores/auth.ts - 用户认证状态管理
- 登录/登出功能
- 用户信息管理
- Token 管理
- localStorage 持久化
```
### 页面组件
```
✅ src/views/LoginView.vue - 登录页面
- 用户名/密码输入
- 表单验证
- 记住登录状态
✅ src/views/AdminLayout.vue - 后台主布局
- 侧边栏 + 内容区域布局
- 顶部导航栏
- 退出登录功能
✅ src/views/admin/DashboardView.vue - 仪表板
- 数据统计卡片
- 最近订单表格
- 数据概览展示
✅ src/views/admin/UsersView.vue - 用户管理
- 用户列表展示
- 搜索功能
- 新增用户
- 编辑用户
- 删除用户
- 状态管理
✅ src/views/admin/ProductsView.vue - 产品管理
- 产品列表管理
- 搜索过滤
- 新增产品
- 编辑产品
- 删除产品
- 库存管理
- 价格管理
✅ src/views/admin/OrdersView.vue - 订单管理
- 订单列表展示
- 高级过滤
- 订单搜索
- 新增订单
- 编辑订单
- 删除订单
- 状态跟踪
✅ src/views/admin/SettingsView.vue - 系统设置
- 基本设置
- 安全设置
- 备份配置
- 文件类型管理
```
### 可复用组件
```
✅ src/components/Header.vue - 顶部导航栏
- 菜单切换按钮
- 用户信息展示
- 退出登录按钮
✅ src/components/Sidebar.vue - 侧边栏菜单
- 可折叠菜单
- 图标 + 文字标签
- 当前路由高亮
- 菜单项导航
```
### 文档文件
```
✅ ADMIN_SYSTEM_README.md - 项目完整文档
✅ QUICK_START.md - 快速开始指南
✅ PROJECT_SUMMARY.md - 本文件
```
---
## 🎯 功能清单
### 已实现的功能
#### 1. 认证系统 ✅
- [x] 登录页面(用户名/密码)
- [x] 登录状态验证
- [x] 自动登录(localStorage 记忆)
- [x] 登出功能
- [x] 路由保护守卫
- [x] 登录失败提示
#### 2. 仪表板 ✅
- [x] 数据统计卡片(4 个指标)
- [x] 统计数据展示
- [x] 最近订单表格
- [x] 欢迎信息
#### 3. 用户管理(CRUD 完整示例)✅
- [x] 用户列表展示
- [x] 搜索功能(用户名/邮箱)
- [x] 新增用户对话框
- [x] 编辑用户信息
- [x] 删除用户
- [x] 状态管理(启用/禁用)
- [x] 角色管理(管理员/普通用户)
- [x] 创建时间记录
#### 4. 产品管理(CRUD 完整示例)✅
- [x] 产品列表展示
- [x] 搜索功能(名称/SKU)
- [x] 新增产品
- [x] 编辑产品信息
- [x] 删除产品
- [x] 库存管理
- [x] 价格管理
- [x] 分类管理
- [x] 上架/下架状态
- [x] 库存警告显示
#### 5. 订单管理(CRUD 完整示例)✅
- [x] 订单列表展示
- [x] 搜索功能(订单号/客户名)
- [x] 状态过滤
- [x] 新增订单
- [x] 编辑订单
- [x] 删除订单
- [x] 支付方式管理
- [x] 订单状态追踪
- [x] 金额管理
#### 6. 系统设置 ✅
- [x] 基本设置表单
- [x] 安全设置
- [x] 备份配置
- [x] 文件上传设置
- [x] 文件类型管理
- [x] 设置保存功能
#### 7. UI 组件 ✅
- [x] 侧边栏菜单(可折叠)
- [x] 顶部导航栏
- [x] 对话框表单
- [x] 数据表格
- [x] 搜索输入
- [x] 状态标签
- [x] 按钮组件
- [x] 表单输入
#### 8. 交互功能 ✅
- [x] 弹出确认对话框
- [x] 提示消息提醒
- [x] 加载状态显示
- [x] 表单验证
- [x] 回车键提交
- [x] 菜单切换动画
#### 9. 路由系统 ✅
- [x] 登录路由
- [x] 后台管理路由
- [x] 路由懒加载
- [x] 导航守卫(认证检查)
- [x] 自动重定向
- [x] 404 处理
---
## 🚀 技术架构
### 技术栈
```
Frontend:
- Vue 3.5.27 前端框架
- TypeScript 5.9.3 类型安全
- Vite 7.3.1 构建工具
- Vue Router 4.6.4 路由管理
- Pinia 3.0.4 状态管理
- Element Plus 2.7.0 UI 组件库
- Axios HTTP 请求
```
### 架构特点
```
1. 组件化设计
- 可复用组件模式
- 父子组件通信
- Emits 事件机制
2. 状态管理
- Pinia 集中管理状态
- localStorage 持久化
- 单一数据源
3. 路由管理
- 嵌套路由结构
- 导航守卫保护
- 懒加载优化
4. 样式系统
- Scoped CSS
- CSS 变量(可扩展)
- 响应式设计
5. 类型安全
- 完整的 TypeScript 支持
- 接口定义
- 类型检查
```
---
## 📊 数据流
### 登录流程
```
用户输入 → 表单验证 → authStore.login()
→ localStorage 保存 → 路由重定向 → 仪表板
```
### 页面导航
```
点击菜单 → Router.push() → 导航守卫检查
→ 页面组件加载 → 内容展示
```
### 数据操作
```
表单提交 → 数据验证 → ref 状态更新
→ UI 重新渲染 → 操作反馈
```
---
## 🎨 样式系统
### 响应式设计
```css
✅ 桌面端:完整布局
✅ 平板端:侧边栏可收缩
✅ 手机端:响应式卡片布局
```
### 颜色管理
```css
主题颜色统一使用:
- 主调:#667eea(可自定义)
- 背景:#0a0e27(可自定义)
- 卡片:#1f2937(可自定义)
- 文本:#e5e7eb(可自定义)
```
---
## 🔐 安全特性
### 已实现
```
✅ 路由认证守卫
✅ 登录状态管理
✅ 表单数据验证
✅ 错误提示处理
✅ Token 管理
```
### 建议增强
```
⚠️ HTTPS 传输
⚠️ API 签名验证
⚠️ CSRF 令牌
⚠️ XSS 防护
⚠️ 权限细粒度控制
```
---
## 📈 性能优化
### 已优化项
```
✅ 路由懒加载
✅ 组件动态导入
✅ 事件委托
✅ 防抖处理
```
### 可优化项
```
⚠️ 虚拟滚动(大列表)
⚠️ 分页加载
⚠️ 图片懒加载
⚠️ 缓存策略
⚠️ 代码分割优化
```
---
## 🎓 学习路径
### 初级开发者
1. 理解 Vue 3 基础
2. 学习组件通信
3. 理解 Pinia 状态管理
4. 学习 Vue Router 路由
### 中级开发者
1. API 集成
2. 表单验证和处理
3. 错误处理
4. 权限管理
### 高级开发者
1. 性能优化
2. 安全加固
3. 自动化测试
4. 部署流程
---
## 📋 使用说明
### 快速启动
```bash
# 1. 安装依赖
npm install
# 2. 开发模式
npm run dev
# 3. 访问
http://localhost:5173
# 4. 登录信息
用户名: admin
密码: 123456
```
### 生产构建
```bash
# 构建
npm run build
# 预览
npm run preview
# 类型检查
npm run type-check
```
---
## 🛠 后续开发计划
### Phase 1: 基础功能(当前 ✅)
- [x] 登录认证
- [x] CRUD 模块
- [x] 主题设计
### Phase 2: 功能扩展(推荐)
- [ ] API 后端集成
- [ ] 权限管理系统
- [ ] 审计日志
- [ ] 批量操作
- [ ] 导出功能
### Phase 3: 用户体验(优化)
- [ ] 深浅主题切换
- [ ] 国际化多语言
- [ ] 快捷键支持
- [ ] 高级搜索
- [ ] 自定义仪表板
### Phase 4: 系统优化(性能)
- [ ] 性能监控
- [ ] 错误追踪
- [ ] 用户行为分析
- [ ] 缓存策略
- [ ] CDN 配置
---
## 💡 开发建议
### 代码组织
```
建议创建 src/api 文件夹管理所有 API 调用
建议创建 src/services 文件夹存放业务逻辑
建议创建 src/types 文件夹存放类型定义
建议创建 src/utils 文件夹存放工具函数
```
### 命名规范
```
✅ 组件文件:PascalCase(如 UserForm.vue)
✅ 页面文件:PascalCase + View 后缀(如 UsersView.vue)
✅ Store 文件:camelCase + Store 后缀(如 authStore.ts)
✅ 类名:PascalCase
✅ 变量/函数:camelCase
✅ 常量:UPPER_CASE
```
### Git 管理
```bash
# 建议分支结构
main - 生产环境
develop - 开发环境
feature/* - 功能分支
bugfix/* - 修复分支
hotfix/* - 紧急修复
```
---
## 🎉 项目成果总结
✅ **完整的后台管理系统框架**
- 5 个功能模块(仪表板、用户、产品、订单、设置)
- 2 个可复用组件(导航栏、侧边栏)
- 完整的 CRUD 示例
✅ **专业的暗黑主题设计**
- 黑白双色优雅设计
- 响应式布局
- 平滑动画效果
✅ **完善的路由和认证系统**
- 登录/登出功能
- 路由保护守卫
- localStorage 持久化
✅ **生产级代码质量**
- TypeScript 类型安全
- 完整的错误处理
- 用户友好的提示反馈
✅ **详细的文档和注释**
- 项目完整文档
- 快速开始指南
- 代码注释清晰
---
## 📞 联系方式
如有任何问题或建议,欢迎反馈。
---
**项目创建时间**: 2026-02-02
**框架版本**: Vue 3.5.27 + TypeScript 5.9.3
**主题**: 暗黑风格(黑白双色)
**状态**: ✅ 已完成且可直接使用
祝你开发愉快!🚀

170
Build_God_Admin_Frontend/Frontend/QUICK_START.md

@ -0,0 +1,170 @@
# 快速开始指南
## 系统已搭建完成 ✅
你的后台管理系统框架已经完全搭建好!以下是完成的内容:
## 📦 已安装的依赖
- ✅ Vue 3
- ✅ TypeScript
- ✅ Vue Router
- ✅ Pinia (状态管理)
- ✅ Element Plus (UI 库)
- ✅ Axios (HTTP 请求)
- ✅ Vite (构建工具)
## 🏗️ 已创建的文件结构
### 状态管理
- `src/stores/auth.ts` - 认证状态管理(用户登录/登出)
### 页面组件
- `src/views/LoginView.vue` - 登录页面
- `src/views/AdminLayout.vue` - 后台主布局
- `src/views/admin/DashboardView.vue` - 仪表板
- `src/views/admin/UsersView.vue` - 用户管理(CRUD 示例)
- `src/views/admin/ProductsView.vue` - 产品管理(CRUD 示例)
- `src/views/admin/OrdersView.vue` - 订单管理(CRUD 示例)
- `src/views/admin/SettingsView.vue` - 系统设置
### 可复用组件
- `src/components/Header.vue` - 顶部导航栏
- `src/components/Sidebar.vue` - 侧边栏菜单
### 配置文件
- `src/router/index.ts` - 路由配置(包含导航守卫)
- `src/main.ts` - 应用入口
- `src/App.vue` - 根组件
- `src/assets/main.css` - 暗黑主题样式
## 🎨 设计特点
### 暗黑主题配置
- 主色调:紫蓝色 (#667eea)
- 背景:深蓝黑 (#0a0e27)
- 卡片:深灰 (#1f2937)
- 文本:浅灰 (#e5e7eb)
### UI 特性
- 侧边栏可折叠
- 响应式设计
- 平滑过渡动画
- 深色主题优化
## 🚀 运行项目
### 开发环境
```bash
npm run dev
```
访问 http://localhost:5173
### 生产构建
```bash
npm run build
```
### 类型检查
```bash
npm run type-check
```
## 🔐 登录信息
- **用户名**: admin
- **密码**: 123456
## 📋 功能清单
✅ 登录/登出功能
✅ 仪表板(数据统计)
✅ 用户管理(增删改查)
✅ 产品管理(增删改查)
✅ 订单管理(增删改查)
✅ 系统设置
✅ 侧边栏导航
✅ 顶部导航栏
✅ 路由保护
✅ 响应式布局
## 🎯 后续开发步骤
1. **后端 API 集成**
- 替换 mock 数据为真实 API 调用
- 使用 axios 进行 HTTP 请求
- 添加错误处理和加载状态
2. **权限管理**
- 根据用户角色显示/隐藏菜单项
- 实现页面级权限控制
- 添加操作级权限检查
3. **数据验证**
- 为所有表单添加输入验证
- 实现客户端表单验证规则
- 添加服务端验证提示
4. **功能扩展**
- 添加更多管理模块
- 实现导出/导入功能
- 添加批量操作功能
- 实现高级过滤和搜索
5. **性能优化**
- 实现虚拟滚动
- 添加分页功能
- 优化图片加载
- 实现懒加载
6. **用户体验**
- 添加更多交互反馈
- 实现快捷键支持
- 添加深色/浅色主题切换
- 实现国际化多语言
## 📚 文件修改说明
### 关键文件
1. **src/router/index.ts**
- 完全重写了路由配置
- 添加了导航守卫(认证检查)
- 配置了后台管理模块路由
2. **src/main.ts**
- 集成了 Element Plus
- 初始化认证状态
3. **src/App.vue**
- 简化为只展示路由视图
4. **src/assets/main.css**
- 更新为暗黑主题样式
## 💡 开发建议
1. 所有 CRUD 页面都有完整的搜索、编辑、删除功能示例
2. 使用 Pinia store 管理全局状态
3. 表单验证可以使用 Element Plus 的表单验证功能
4. API 调用使用 axios 库,可创建 `src/api` 文件夹管理所有 API
5. 创建 `src/services` 文件夹存放业务逻辑
## 🔗 项目结构优化建议
```
src/
├── api/ # API 接口管理
│ ├── users.ts
│ ├── products.ts
│ ├── orders.ts
│ └── index.ts
├── services/ # 业务逻辑服务
├── types/ # TypeScript 类型定义
├── utils/ # 工具函数
├── hooks/ # 自定义 Hooks
├── directives/ # 自定义指令
└── ...
```
祝你开发愉快!如有任何问题,随时联系我。

42
Build_God_Admin_Frontend/Frontend/README.md

@ -0,0 +1,42 @@
# Build_God_Admin_Frontend
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Recommended Browser Setup
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
- Firefox:
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Type-Check, Compile and Minify for Production
```sh
npm run build
```

235
Build_God_Admin_Frontend/Frontend/README_API_SUMMARY.md

@ -0,0 +1,235 @@
# 🎯 Axios API 集成 - 一句话总结
## 你问的问题
> 是不是少了axios相关的东西?调用后台接口,后台接口会传递json数据上来,我应该怎么做?你只用做一个登录示例就可以了。
## 答案
**已为你完成!** 完整的 axios 配置和登录 API 示例已创建。
---
## 📂 新增文件
### API 层(前端代码)
```
src/api/
├── index.ts ← axios 实例 + 拦截器
│ • baseURL 自动添加 API 前缀
│ • 请求拦截器自动添加 token
│ • 响应拦截器自动处理错误
└── auth.ts ← 登录 API
• loginApi(username, password) - 调用 POST /api/auth/login
• logoutApi()
• getUserInfoApi()
• 完整的 TypeScript 类型定义
```
### 环境配置
```
.env.development ← API 地址:http://localhost:3000/api
.env.production ← API 地址:https://api.example.com/api
.env.local ← 本地覆盖
```
### 文档
```
WHAT_TO_DO_NEXT.md ← 👈 你接下来该做什么(先读这个)
API_IMPLEMENTATION_COMPLETE.md ← 完整总结
API_INTEGRATION_GUIDE.md ← 详细教程
BACKEND_API_EXAMPLE.md ← 后端代码示例
API_QUICK_REFERENCE.md ← 快速参考
```
---
## 🔄 工作流程
### 简单版本
```
用户登录
await authStore.login(username, password)
loginApi(credentials)
http.post('/auth/login', credentials)
axios 拦截器处理
POST http://localhost:3000/api/auth/login
后端返回 JSON
axios 拦截器提取 response.data
前端收到数据
保存 token 和 user 到 localStorage
登录成功!
```
---
## 💻 后端返回的 JSON 格式
你的后端需要返回这样的格式:
```json
{
"code": 200,
"message": "登录成功",
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": "1",
"username": "admin",
"email": "admin@example.com",
"role": "admin"
}
}
}
```
**关键点:**
- `code` 必须是 200 表示成功
- `data.token` 是 JWT,后续请求会自动在 `Authorization: Bearer <token>` 中发送
- `data.user` 包含用户信息
---
## 🚀 立即测试
### 选项 1:使用提供的后端示例
```bash
# 创建后端项目
mkdir admin-backend && cd admin-backend
npm init -y && npm install express cors jsonwebtoken
# 复制 BACKEND_API_EXAMPLE.md 中的代码到 server.js
# 然后运行
node server.js
# 在另一个终端启动前端
npm run dev
# 访问 http://localhost:5173
# 输入 admin / 123456 登录
```
### 选项 2:集成你自己的后端
1. 实现 `POST /api/auth/login` 接口
2. 返回上述格式的 JSON
3. 配置 CORS
4. 启动后端
5. 运行 `npm run dev`
6. 测试登录
---
## 📝 代码示例
### 在组件中使用(已更新)
```typescript
import { useAuthStore } from '@/stores/auth'
const authStore = useAuthStore()
// 登录
const success = await authStore.login('admin', '123456')
if (success) {
console.log('登录成功!')
}
// 获取当前用户
console.log(authStore.user)
console.log(authStore.token)
// 登出
await authStore.logout()
```
### 添加新的 API(参考登录示例)
```typescript
// src/api/users.ts
import http from './index'
export const getUsersApi = () => {
return http.get('/users')
}
export const createUserApi = (data) => {
return http.post('/users', data)
}
// 在组件中使用
const response = await getUsersApi()
if (response.code === 200) {
console.log(response.data)
}
```
---
## ✅ 检查清单
### 前端(✅ 已完成)
- [x] Axios 配置
- [x] 拦截器设置
- [x] API 服务层
- [x] 登录示例
- [x] TypeScript 类型
- [x] 环境配置
### 你需要做(⏳ 待实施)
- [ ] 实现后端登录接口
- [ ] 配置 CORS
- [ ] 启动后端
- [ ] 测试集成
---
## 📚 快速参考
| 需求 | 文件 | 说明 |
|------|------|------|
| 快速上手 | `WHAT_TO_DO_NEXT.md` | 下一步该做什么 |
| 详细教程 | `API_INTEGRATION_GUIDE.md` | 完整 API 使用指南 |
| 后端参考 | `BACKEND_API_EXAMPLE.md` | Node.js/Python 实现 |
| 代码参考 | `API_QUICK_REFERENCE.md` | 快速代码片段 |
| 原理说明 | `API_IMPLEMENTATION_COMPLETE.md` | 完整工作流程 |
---
## 🎉 总结
```
✅ 前端:完整的 API 层 + 登录示例
⏳ 你的工作:实现后端 API + 配置 CORS + 测试
预计时间:15 分钟
结果:功能完整的登录系统
```
**现在就开始吧!** 🚀
---
**相关文档快速链接:**
- 👉 [接下来该做什么](./WHAT_TO_DO_NEXT.md)
- 📖 [详细集成指南](./API_INTEGRATION_GUIDE.md)
- 💻 [后端代码示例](./BACKEND_API_EXAMPLE.md)
- ⚡ [快速参考](./API_QUICK_REFERENCE.md)

390
Build_God_Admin_Frontend/Frontend/WHAT_TO_DO_NEXT.md

@ -0,0 +1,390 @@
# 🎯 API 集成总结 - 你该做什么
## ✅ 已经为你完成的
### 1. 前端 API 架构
```
src/api/
├── index.ts - axios 实例 + 拦截器(请求时自动添加 token)
└── auth.ts - 登录相关 API 定义
```
### 2. 更新的文件
```
src/stores/auth.ts - 已更新为调用真实 API
src/views/LoginView.vue - 已更新为异步登录
```
### 3. 环境配置
```
.env.development - API 地址:http://localhost:3000/api
.env.production - API 地址:https://api.example.com/api (修改为你的)
.env.local - 本地覆盖配置
```
### 4. 完整的文档
```
API_IMPLEMENTATION_COMPLETE.md - 最完整的总结(推荐先读这个)
API_INTEGRATION_GUIDE.md - 详细 API 使用指南
BACKEND_API_EXAMPLE.md - 后端代码示例(Node.js/Python)
API_QUICK_REFERENCE.md - 快速参考卡片
```
## 🚀 你现在需要做什么
### 方案 A:使用我提供的后端示例
**第 1 步:创建后端项目**
```bash
mkdir admin-backend
cd admin-backend
npm init -y
npm install express cors jsonwebtoken
```
**第 2 步:创建 `server.js`**
参考 `BACKEND_API_EXAMPLE.md` 中的 Node.js 示例代码,复制到 `server.js`
**第 3 步:启动后端**
```bash
node server.js
```
你会看到:
```
✅ 服务器运行在 http://localhost:3000
```
**第 4 步:启动前端**
在另一个终端:
```bash
npm run dev
```
**第 5 步:测试**
1. 访问 http://localhost:5173
2. 输入 `admin / 123456`
3. 点击登录
4. 应该看到"登录成功"并跳转到仪表板 ✨
---
### 方案 B:使用你自己的后端
**需要做什么:**
1. **实现登录接口**
端点:`POST /api/auth/login`
请求:
```json
{
"username": "admin",
"password": "123456"
}
```
响应:
```json
{
"code": 200,
"message": "登录成功",
"data": {
"token": "jwt_token_here",
"user": {
"id": "1",
"username": "admin",
"email": "admin@example.com",
"role": "admin"
}
}
}
```
2. **配置 CORS**
允许前端跨域请求:
```
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
```
3. **启动后端服务**
确保监听 `3000` 端口(或修改 `.env.development` 中的地址)
4. **启动前端**
```bash
npm run dev
```
---
## 🔧 常见问题
### Q: 前端提示"网络错误"
**解决方案:**
1. 检查后端是否启动:`http://localhost:3000`
2. 检查 `.env.development` 中的 API 地址是否正确
3. 查看浏览器开发者工具的 Network 标签查看请求
### Q: 前端提示"CORS error"
**解决方案:**
- 后端必须配置 CORS,允许来自 `http://localhost:5173` 的请求
**Express 示例:**
```javascript
const cors = require('cors')
app.use(cors({
origin: 'http://localhost:5173',
credentials: true
}))
```
### Q: 登录提示"401 未授权"
**解决方案:**
- 检查后端返回的 `code` 是否为 200
- 检查用户名和密码是否匹配
- 查看后端日志
### Q: 登录后页面不跳转
**解决方案:**
1. 检查浏览器控制台是否有 JavaScript 错误
2. 检查是否成功保存到 localStorage(打开开发者工具 → Application → LocalStorage)
3. 查看 Network 标签验证响应数据格式是否正确
### Q: 如何修改 API 地址?
**开发环境:**
编辑 `.env.development`
```
VITE_API_URL=http://你的后端地址:3000/api
```
**生产环境:**
编辑 `.env.production`
```
VITE_API_URL=https://你的生产API地址/api
```
---
## 📚 后续开发
### 添加更多 API 接口
参考 `src/api/auth.ts`,在 `src/api/` 中创建新文件:
```typescript
// src/api/users.ts
import http from './index'
export const getUsersApi = () => {
return http.get('/users')
}
export const createUserApi = (data) => {
return http.post('/users', data)
}
export const updateUserApi = (id, data) => {
return http.put(`/users/${id}`, data)
}
export const deleteUserApi = (id) => {
return http.delete(`/users/${id}`)
}
```
### 在组件中使用
```typescript
import { getUsersApi } from '@/api/users'
const users = ref([])
const loading = ref(false)
const fetchUsers = async () => {
loading.value = true
try {
const response = await getUsersApi()
if (response.code === 200) {
users.value = response.data.items
}
} catch (error) {
ElMessage.error('获取失败')
} finally {
loading.value = false
}
}
onMounted(() => {
fetchUsers()
})
```
---
## 🧪 测试步骤
### 1. 测试后端接口(使用 curl)
```bash
# 登录
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"123456"}'
# 应该返回
# {
# "code": 200,
# "message": "登录成功",
# "data": {
# "token": "...",
# "user": {...}
# }
# }
```
### 2. 测试前端集成
1. 启动后端:`node server.js`
2. 启动前端:`npm run dev`
3. 打开 http://localhost:5173
4. 输入用户名和密码
5. 点击登录
6. 查看浏览器开发者工具:
- **Network** 标签:查看请求/响应
- **Application** 标签:查看 localStorage 中的 token
- **Console** 标签:查看任何错误信息
---
## 📋 文件说明
| 文件 | 说明 | 何时修改 |
|-----|------|--------|
| `src/api/index.ts` | Axios 配置和拦截器 | 很少 |
| `src/api/auth.ts` | API 接口定义 | 添加新接口时 |
| `src/stores/auth.ts` | 认证状态管理 | 很少 |
| `src/views/LoginView.vue` | 登录页面 | 很少 |
| `.env.development` | 开发环境 API 地址 | 改变后端地址时 |
| `.env.production` | 生产环境 API 地址 | 部署到生产前 |
---
## 🎓 学习资源
1. **快速参考** - 看 `API_QUICK_REFERENCE.md`
2. **详细指南** - 看 `API_INTEGRATION_GUIDE.md`
3. **后端示例** - 看 `BACKEND_API_EXAMPLE.md`
4. **完整说明** - 看 `API_IMPLEMENTATION_COMPLETE.md`
---
## ✅ 完成清单
### 前端部分(✅ 已完成)
- [x] Axios 配置
- [x] 请求拦截器
- [x] 响应拦截器
- [x] API 服务层
- [x] 认证 Store
- [x] 登录页面
- [x] 环境配置
- [x] 文档
- [x] 类型定义
### 你需要完成的(⏳ 待实施)
- [ ] 后端登录接口
- [ ] 后端 CORS 配置
- [ ] 后端数据库存储
- [ ] 其他业务接口
---
## 🚀 快速开始
### 最快的方式(使用示例后端)
```bash
# 终端 1 - 启动后端
cd admin-backend
node server.js
# 终端 2 - 启动前端
npm run dev
# 打开浏览器访问 http://localhost:5173
# 输入 admin / 123456 登录
```
---
## 💡 核心概念
### 请求流程
```
前端 → 添加 token → 发送 HTTP 请求 → 后端 ← 验证 token ← 返回数据
```
### 响应格式
```json
{
"code": 200, // HTTP 状态码
"message": "...", // 消息
"data": {...} // 实际数据
}
```
### Token 管理
```
登录时保存 → 每个请求自动添加 → 验证失败自动清除 → 跳转登录
```
---
## 📞 如果遇到问题
1. 查看浏览器控制台错误信息
2. 查看后端日志
3. 使用 curl 测试后端接口
4. 查看相关文档
5. 检查文件名和 API 路径是否正确
---
## 🎉 总结
你现在有:
✅ 完整的前端 API 架构(可直接使用)
✅ 登录示例代码(参考如何集成)
✅ 后端代码示例(参考如何实现)
✅ 详细的文档(学习和参考)
**只需 3 步即可让系统运行:**
1. 实现后端登录接口
2. 启动后端和前端
3. 在登录页测试
准备好了吗?让我们开始吧!🚀

1
Build_God_Admin_Frontend/Frontend/env.d.ts

@ -0,0 +1 @@
/// <reference types="vite/client" />

13
Build_God_Admin_Frontend/Frontend/index.html

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="" class="dark">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

3561
Build_God_Admin_Frontend/Frontend/package-lock.json

File diff suppressed because it is too large

35
Build_God_Admin_Frontend/Frontend/package.json

@ -0,0 +1,35 @@
{
"name": "build-god-admin-frontend",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build"
},
"dependencies": {
"axios": "^1.13.4",
"element-plus": "^2.7.0",
"jwt-decode": "^4.0.0",
"pinia": "^3.0.4",
"vue": "^3.5.27",
"vue-router": "^4.6.4"
},
"devDependencies": {
"@tsconfig/node24": "^24.0.4",
"@types/node": "^24.10.9",
"@vitejs/plugin-vue": "^6.0.3",
"@vue/tsconfig": "^0.8.1",
"npm-run-all2": "^8.0.4",
"typescript": "~5.9.3",
"vite": "^7.3.1",
"vite-plugin-vue-devtools": "^8.0.5",
"vue-tsc": "^3.2.4"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
}

BIN
Build_God_Admin_Frontend/Frontend/public/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

25
Build_God_Admin_Frontend/Frontend/src/App.vue

@ -0,0 +1,25 @@
<script setup lang="ts">
import { RouterView } from 'vue-router'
</script>
<template>
<RouterView />
</template>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: #0a0e27;
color: #e5e7eb;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
html {
background-color: #0a0e27;
}
</style>

70
Build_God_Admin_Frontend/Frontend/src/api/auth.ts

@ -0,0 +1,70 @@
import http from './index'
// 定义登录请求/响应的类型
export interface LoginRequest {
name: string
password: string
}
export interface User {
id: string
name: string
email: string
role: 'admin' | 'user'
}
export interface LoginResponse {
token: string
}
// 定义 API 响应的通用格式
export interface ApiResponse<T> {
code: number
message: string
data?: T
}
/**
* admin用户能登录api里得固定值
* @param credentials - { username, password }
* @returns Promise<LoginResponse>
*
*
* POST /api/god/account/login/admin
* {
* "name": "admin",
* "password": "123456"
* }
*
*
* {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
* }
*/
export const loginApi = (credentials: LoginRequest): Promise<LoginResponse> => {
return http.post('account/login/admin', credentials)
}
/**
*
* @returns Promise
*/
export const logoutApi = (): Promise<ApiResponse<null>> => {
return http.post('/auth/logout', {})
}
/**
*
* @returns Promise<ApiResponse<User>>
*/
export const getUserInfoApi = (): Promise<ApiResponse<User>> => {
return http.get('/auth/userinfo')
}
/**
* token
* @returns Promise<LoginResponse>
*/
export const refreshTokenApi = (): Promise<LoginResponse> => {
return http.post('/auth/refresh', {})
}

89
Build_God_Admin_Frontend/Frontend/src/api/bag.ts

@ -0,0 +1,89 @@
import http, { type EnumInfoDto } from './index'
export interface Bag {
id: number
name: string
rarity: number
capacity: number
description: string | null
}
export interface CharacterBag {
id: number
characterId: number
bagId: number
bagName: string | null
bagCapacity: number
}
export interface BagItem {
id: number
characterBagId: number
itemType: number
itemId: number
quantity: number
itemName: string | null
itemRarity: number | null
}
export interface AddBagItemDto {
itemType: number
itemId: number
quantity: number
}
export interface AssignBagDto {
characterId: number
bagId: number
}
// Bag配置管理
export const GetAllBags = (): Promise<Bag[]> => {
return http.get('bag/all')
}
export const GetBagById = (id: number): Promise<Bag> => {
return http.get(`bag/${id}`)
}
export const CreateBag = (data: Bag): Promise<boolean> => {
return http.post('bag', data)
}
export const UpdateBag = (id: number, data: Bag): Promise<boolean> => {
return http.put(`bag/${id}`, data)
}
export const DeleteBag = (id: number): Promise<boolean> => {
return http.delete(`bag/${id}`)
}
// 角色背包管理
export const GetCharacterBag = (characterId: number): Promise<CharacterBag | null> => {
return http.get(`bag/character/${characterId}`)
}
export const AssignBagToCharacter = (data: AssignBagDto): Promise<boolean> => {
return http.post('bag/assign', data)
}
// 背包物品管理
export const GetBagItems = (characterBagId: number): Promise<BagItem[]> => {
return http.get(`bag/${characterBagId}/items`)
}
export const AddItemToBag = (characterBagId: number, data: AddBagItemDto): Promise<boolean> => {
return http.post(`bag/${characterBagId}/items`, data)
}
export const RemoveItemFromBag = (characterBagId: number, itemId: number): Promise<boolean> => {
return http.delete(`bag/${characterBagId}/items/${itemId}`)
}
export const GetBagRarities = (): Promise<EnumInfoDto[]> => {
return http.get('bag/rarities')
}
export const GetBagItemTypes = (): Promise<EnumInfoDto[]> => {
return http.get('bag/item-types')
}

31
Build_God_Admin_Frontend/Frontend/src/api/character.ts

@ -0,0 +1,31 @@
import http from './index'
export interface Character {
id: number
name: string
accountId: number
currentExp: number
levelId: number
money: number
maxHP: number
currentHP: number
attack: number
breakthroughRate: number
spiritId: number | null
spiritFieldId: number | null
lastLogin: string
trainingOn: string | null
isLocked: boolean
}
export const GetAllCharacters = (): Promise<Character[]> => {
return http.get('character/all')
}
export const GetCharacterById = (id: number): Promise<Character> => {
return http.get(`character/${id}`)
}
export const GetCharacterByAccountId = (accountId: number): Promise<Character | null> => {
return http.get(`character/accountId/${accountId}`)
}

114
Build_God_Admin_Frontend/Frontend/src/api/equipment.ts

@ -0,0 +1,114 @@
import http, { type EnumInfoDto } from "../api/index";
export interface EquipmentTemplate {
id: number;
name: string;
description: string;
type: number;
rarity: number;
requirdLevelId: number;
setId: number | null;
money: number;
attributePool: string;
randomAttrCount: number;
maxEnhanceLevel: number;
}
export interface EquipmentAttribute {
type: string;
value: number;
}
export interface EquipmentInstance {
id: number;
characterBagId: number;
equipmentTemplateId: number;
name: string;
type: number;
rarity: number;
attributes: string;
enhanceLevel: number;
enhanceBonusPercent: number;
requirdLevelId: number;
setId: number | null;
}
export interface EnhanceConfig {
id: number;
level: number;
reqItemId: number;
reqItemCount: number;
successRate: number;
bonusPercent: number;
}
export interface EquipmentAttributePool {
type: number;
min: number;
max: number;
weight: number;
}
//获取所有的equipment
export interface PagedResult<T> {
items: T[];
totalCount: number;
}
interface SearchEquipmentDto {
pageNumber: number | undefined;
pageSize: number | undefined;
equipmentType: number | undefined;
}
export const GetEquipmentTemplateList = (
eqType?: number,
pageNumber?: number,
pageSize?: number,
): Promise<PagedResult<EquipmentTemplate> | EquipmentTemplate[]> => {
var dto: SearchEquipmentDto = {
pageNumber: pageNumber,
pageSize: pageSize,
equipmentType: eqType,
};
return http.post("equipment/all", dto);
};
//获取装备类型
export const GetEquipmentTypes = (): Promise<EnumInfoDto[]> => {
return http.get("equipment/types");
};
//获取装备稀有度
export const GetEquipmentRarities = (): Promise<EnumInfoDto[]> => {
return http.get("equipment/rarities");
};
//获取装备属性类型
export const GetEquipmentAttributeTypes = (): Promise<EnumInfoDto[]> => {
return http.get("equipment/attribute-types");
};
//添加装备模板
export const AddEquipmentTemplate = (data: EquipmentTemplate): Promise<boolean> => {
return http.post("equipment", data);
};
//修改装备模板
export const UpdateEquipmentTemplate = (data: EquipmentTemplate): Promise<boolean> => {
return http.put("equipment", data);
};
//删除装备模板
export const DeleteEquipmentTemplate = (id: number): Promise<boolean> => {
return http.delete(`equipment/${id}`);
};
//获取装备实例列表
export const GetEquipmentInstanceList = (
characterBagId?: number,
pageNumber?: number,
pageSize?: number,
): Promise<PagedResult<EquipmentInstance> | EquipmentInstance[]> => {
return http.get("equipment/instance/all", { params: { characterBagId, pageNumber, pageSize } });
};

62
Build_God_Admin_Frontend/Frontend/src/api/index.ts

@ -0,0 +1,62 @@
import axios from 'axios'
export interface EnumInfoDto{
id: number;
name: string;
displayName: string | null;
description: string | null;
}
// 创建 axios 实例
const instance = axios.create({
// 根据环境设置 API 地址
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:5091/api/god/',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
})
// 请求拦截器 - 添加 token
instance.interceptors.request.use(
(config) => {
const token = sessionStorage.getItem('auth_token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器 - 处理错误
instance.interceptors.response.use(
(response) => {
// 如果后端统一返回格式,这里可以解析
return response.data
},
(error) => {
if (error.response) {
// 服务器返回错误状态码
const { status, data } = error.response
if (status === 401) {
// 令牌过期,清除登录状态
sessionStorage.removeItem('auth_token')
sessionStorage.removeItem('user')
window.location.href = '/login'
}
return Promise.reject(data || error.message)
} else if (error.request) {
// 请求已发送但没有收到响应
return Promise.reject('网络错误,请检查连接')
} else {
// 请求配置有问题
return Promise.reject(error.message)
}
}
)
export default instance

32
Build_God_Admin_Frontend/Frontend/src/api/level.ts

@ -0,0 +1,32 @@
import http from "../api/index";
export interface Level{
id: number;
name: string;
levelId: number | null;
currentLevelMinExp: number | null;
nextLevelId: number | null;
baseBreakthroughRate: number;
failIncrement: number;
description: string;
}
//获取所有的level
export const GetLevelList = (): Promise<Level[]> => {
return http.get("level/all");
};
//添加等级
export const AddLevel = (data: Level): Promise<boolean> => {
return http.post("level", data);
}
//修改等级
export const UpdateLevel = (data: Level): Promise<boolean> => {
return http.put("level", data);
}
//删除等级
export const DeleteLevel = (id: number): Promise<boolean> => {
return http.delete(`level/${id}`);
}

82
Build_God_Admin_Frontend/Frontend/src/api/mission.ts

@ -0,0 +1,82 @@
import http, { type EnumInfoDto } from "../api/index";
export interface Mission {
id: number;
name: string;
type?: number;
title: string;
description: string;
requiredLevelId?: number;
repeatable: boolean;
isAvailable: boolean;
spendTimeMinutes?: number;
obtainPercentage?: number;
preMissionId?: number;
difficulty?: number;
rewards?: MissionReward[];
}
export interface MissionReward {
id: number;
missionId?: number;
rewardType?: number;
itemId?: number;
itemName: string;
count?: number;
}
export interface PagedResult<T> {
items: T[];
totalCount: number;
}
interface SearchMissionDto {
pageNumber: number | undefined;
pageSize: number | undefined;
missionType: number | undefined;
}
export const GetMissionList = (
type?: number,
pageNumber?: number,
pageSize?: number,
): Promise<PagedResult<Mission> | Mission[]> => {
var dto: SearchMissionDto = {
pageNumber: pageNumber,
pageSize: pageSize,
missionType: type,
};
return http.post("mission/all", dto);
};
export const GetMissionTypes = (): Promise<EnumInfoDto[]> => {
return http.get("mission/types");
};
export const GetMissionDifficultys = (): Promise<EnumInfoDto[]> => {
return http.get("mission/difficulties");
};
export const AddMission = (data: Mission): Promise<boolean> => {
return http.post("mission", data);
};
export const UpdateMission = (data: Mission): Promise<boolean> => {
return http.put("mission", data);
};
export const DeleteMission = (id: number): Promise<boolean> => {
return http.delete(`mission/${id}`);
};
export const AddReward = (data: MissionReward): Promise<boolean> => {
return http.post("mission/reward/add", data);
};
export const UpdateReward = (data: MissionReward): Promise<boolean> => {
return http.post("mission/reward/update", data);
};
export const GetRewardTypes = (): Promise<EnumInfoDto[]> => {
return http.get("mission/reward/types");
};

42
Build_God_Admin_Frontend/Frontend/src/api/missionProgress.ts

@ -0,0 +1,42 @@
import http, { type EnumInfoDto } from './index'
export interface MissionProgress {
id: number
missionId: number
targetType: number
targetItemId: number | null
targetItemName: string | null
targetCount: number
description: string | null
}
export interface CharacterMissionProgress {
id: number
characterId: number
missionId: number
missionProgressId: number
currentCount: number
isCompleted: boolean
updatedOn: string
}
// Mission Progress Config APIs
export const GetMissionProgresses = (missionId: number): Promise<MissionProgress[]> => {
return http.get(`mission/${missionId}/progresses`)
}
export const CreateMissionProgress = (missionId: number, data: MissionProgress): Promise<boolean> => {
return http.post(`mission/${missionId}/progresses`, data)
}
export const UpdateMissionProgress = (id: number, data: MissionProgress): Promise<boolean> => {
return http.put(`mission/progresses/${id}`, data)
}
export const DeleteMissionProgress = (id: number): Promise<boolean> => {
return http.delete(`mission/progresses/${id}`)
}
export const GetProgressTargetTypes = (): Promise<EnumInfoDto[]> => {
return http.get('mission/progress/target-types')
}

49
Build_God_Admin_Frontend/Frontend/src/api/pill.ts

@ -0,0 +1,49 @@
import http, { type EnumInfoDto } from "../api/index";
export interface Pill {
id: number;
name: string;
grade?: number;
type?: number;
rarity?: number;
money?: number;
description: string;
requirdLevelId?: number;
effectValue?: number;
duration?: number;
}
//获取所有
export const GetPillList = (): Promise<Pill[]> => {
return http.get("pill/all");
};
//添加丹药
export const AddPill = (data: Pill): Promise<boolean> => {
return http.post("pill", data);
}
//修改丹药
export const UpdatePill = (data: Pill): Promise<boolean> => {
return http.put("pill", data);
}
//删除丹药
export const DeletePill = (id: number): Promise<boolean> => {
return http.delete(`pill/${id}`);
}
//获取丹药等级
export const getPillGrades = (): Promise<EnumInfoDto[]> => {
return http.get('pill/grades');
}
//获取丹药类型
export const getPillTypes = (): Promise<EnumInfoDto[]> => {
return http.get('pill/types');
}
//获取丹药稀有度
export const getPillRarities = (): Promise<EnumInfoDto[]> => {
return http.get('pill/rarities');
}

27
Build_God_Admin_Frontend/Frontend/src/api/spirit.ts

@ -0,0 +1,27 @@
import http from "../api/index";
export interface Profession {
id: number;
name: string;
description: string;
attackRate: number;
defendRate: number;
healthRate: number;
criticalRate: number;
}
export const GetProfessionList = (): Promise<Profession[]> => {
return http.get("profession/all");
};
export const AddProfession = (data: Profession): Promise<boolean> => {
return http.post("profession", data);
}
export const UpdateProfession = (data: Profession): Promise<boolean> => {
return http.put("profession", data);
}
export const DeleteProfession = (id: number): Promise<boolean> => {
return http.delete(`profession/${id}`);
}

16
Build_God_Admin_Frontend/Frontend/src/api/statistics.ts

@ -0,0 +1,16 @@
import http from './index'
export interface StatisticsSummary {
equipmentCount: number
levelCount: number
professionCount: number
pillCount: number
missionCount: number
equipmentByRarity: Record<number, number>
missionByDifficulty: Record<number, number>
pillByGrade: Record<number, number>
}
export const GetStatisticsSummary = (): Promise<StatisticsSummary> => {
return http.get('statistics/summary')
}

86
Build_God_Admin_Frontend/Frontend/src/assets/base.css

@ -0,0 +1,86 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-light-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

1
Build_God_Admin_Frontend/Frontend/src/assets/logo.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

28
Build_God_Admin_Frontend/Frontend/src/assets/main.css

@ -0,0 +1,28 @@
@import './base.css';
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
background-color: #0a0e27;
color: #e5e7eb;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
html {
height: 100%;
}
body {
min-height: 100%;
}
#app {
width: 100%;
height: 100%;
background-color: #0a0e27;
}

105
Build_God_Admin_Frontend/Frontend/src/components/Header.vue

@ -0,0 +1,105 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useAuthStore } from '@/stores/auth'
const emit = defineEmits<{
toggle: []
logout: []
}>()
const authStore = useAuthStore()
const username = computed(() => authStore.user?.name || 'User')
</script>
<template>
<header class="header">
<div class="header-left">
<button class="toggle-btn" @click="emit('toggle')" title="切换菜单">
</button>
<h2 class="page-title">后台管理系统</h2>
</div>
<div class="header-right">
<div class="user-info">
<span class="username">{{ username }}</span>
<el-button type="danger" @click="emit('logout')">
退出登录
</el-button>
</div>
</div>
</header>
</template>
<style scoped lang="css">
.header {
height: 60px;
background-color: #1f2937;
border-bottom: 1px solid #374151;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
flex-shrink: 0;
}
.header-left {
display: flex;
align-items: center;
gap: 20px;
}
.toggle-btn {
background: none;
border: none;
color: #9ca3af;
font-size: 20px;
cursor: pointer;
padding: 5px;
transition: color 0.3s ease;
}
.toggle-btn:hover {
color: #e5e7eb;
}
.page-title {
margin: 0;
color: #e5e7eb;
font-size: 16px;
font-weight: 600;
}
.header-right {
display: flex;
align-items: center;
}
.user-info {
display: flex;
align-items: center;
gap: 15px;
}
.username {
color: #9ca3af;
font-size: 14px;
}
.logout-btn {
padding: 8px 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: all 0.3s ease;
}
.logout-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
}
</style>

41
Build_God_Admin_Frontend/Frontend/src/components/HelloWorld.vue

@ -0,0 +1,41 @@
<script setup lang="ts">
defineProps<{
msg: string
}>()
</script>
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3>
Youve successfully created a project with
<a href="https://vite.dev/" target="_blank" rel="noopener">Vite</a> +
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>. What's next?
</h3>
</div>
</template>
<style scoped>
h1 {
font-weight: 500;
font-size: 2.6rem;
position: relative;
top: -10px;
}
h3 {
font-size: 1.2rem;
}
.greetings h1,
.greetings h3 {
text-align: center;
}
@media (min-width: 1024px) {
.greetings h1,
.greetings h3 {
text-align: left;
}
}
</style>

192
Build_God_Admin_Frontend/Frontend/src/components/Sidebar.vue

@ -0,0 +1,192 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ICONS } from '@/constants/theme'
defineProps<{
collapsed: boolean
}>()
const router = useRouter()
const route = useRoute()
const menuItems = [
{
icon: ICONS.dashboard,
label: '管理看板',
path: '/admin/dashboard'
},
{
icon: ICONS.equipment,
label: '装备管理',
path: '/admin/equipments'
},
{
icon: ICONS.level,
label: '境界管理',
path: '/admin/levels'
},
{
icon: ICONS.spirit,
label: '职业管理',
path: '/admin/professions'
},
{
icon: ICONS.pill,
label: '丹药管理',
path: '/admin/pills'
},
{
icon: ICONS.mission,
label: '任务管理',
path: '/admin/missions'
},
{
icon: ICONS.bag,
label: '背包管理',
path: '/admin/bags'
}
]
const isActive = (path: string) => {
return route.path === path
}
const handleMenuClick = (path: string) => {
router.push(path)
}
</script>
<template>
<aside class="sidebar" :class="{ collapsed }">
<div class="logo">
<span v-if="!collapsed" class="logo-text">Admin</span>
<span v-else class="logo-icon">A</span>
</div>
<nav class="menu">
<button
v-for="item in menuItems"
:key="item.path"
class="menu-item"
:class="{ active: isActive(item.path) }"
@click="handleMenuClick(item.path)"
:title="item.label"
>
<span class="menu-icon">{{ item.icon }}</span>
<span v-if="!collapsed" class="menu-label">{{ item.label }}</span>
</button>
</nav>
</aside>
</template>
<style scoped lang="css">
.sidebar {
width: 260px;
background-color: #111827;
border-right: 1px solid #1f2937;
display: flex;
flex-direction: column;
transition: width 0.3s ease;
overflow: hidden;
}
.sidebar.collapsed {
width: 80px;
}
.logo {
height: 60px;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 1px solid #1f2937;
font-weight: 700;
font-size: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.logo-text {
font-weight: 700;
font-size: 20px;
}
.logo-icon {
font-size: 20px;
font-weight: 700;
}
.menu {
flex: 1;
padding: 20px 0;
overflow-y: auto;
}
.menu::-webkit-scrollbar {
width: 6px;
}
.menu::-webkit-scrollbar-track {
background: transparent;
}
.menu::-webkit-scrollbar-thumb {
background: #374151;
border-radius: 3px;
}
.menu::-webkit-scrollbar-thumb:hover {
background: #4b5563;
}
.menu-item {
width: 100%;
padding: 12px 20px;
display: flex;
align-items: center;
gap: 12px;
background: none;
border: none;
color: #9ca3af;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
text-align: left;
white-space: nowrap;
}
.sidebar.collapsed .menu-item {
padding: 12px;
justify-content: center;
}
.menu-item:hover {
background-color: #1f2937;
color: #e5e7eb;
}
.menu-item.active {
background-color: rgba(102, 126, 234, 0.15);
color: #667eea;
border-left: 3px solid #667eea;
padding-left: 17px;
}
.sidebar.collapsed .menu-item.active {
padding-left: 12px;
border-left: 3px solid transparent;
border-radius: 0 8px 8px 0;
}
.menu-icon {
font-size: 18px;
flex-shrink: 0;
}
.menu-label {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
}
</style>

95
Build_God_Admin_Frontend/Frontend/src/components/TheWelcome.vue

@ -0,0 +1,95 @@
<script setup lang="ts">
import WelcomeItem from './WelcomeItem.vue'
import DocumentationIcon from './icons/IconDocumentation.vue'
import ToolingIcon from './icons/IconTooling.vue'
import EcosystemIcon from './icons/IconEcosystem.vue'
import CommunityIcon from './icons/IconCommunity.vue'
import SupportIcon from './icons/IconSupport.vue'
const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md')
</script>
<template>
<WelcomeItem>
<template #icon>
<DocumentationIcon />
</template>
<template #heading>Documentation</template>
Vues
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
provides you with all information you need to get started.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<ToolingIcon />
</template>
<template #heading>Tooling</template>
This project is served and bundled with
<a href="https://vite.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
recommended IDE setup is
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a>
+
<a href="https://github.com/vuejs/language-tools" target="_blank" rel="noopener"
>Vue - Official</a
>. If you need to test your components and web pages, check out
<a href="https://vitest.dev/" target="_blank" rel="noopener">Vitest</a>
and
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a>
/
<a href="https://playwright.dev/" target="_blank" rel="noopener">Playwright</a>.
<br />
More instructions are available in
<a href="javascript:void(0)" @click="openReadmeInEditor"><code>README.md</code></a
>.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<EcosystemIcon />
</template>
<template #heading>Ecosystem</template>
Get official tools and libraries for your project:
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
you need more resources, we suggest paying
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
a visit.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<CommunityIcon />
</template>
<template #heading>Community</template>
Got stuck? Ask your question on
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>
(our official Discord server), or
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
>StackOverflow</a
>. You should also follow the official
<a href="https://bsky.app/profile/vuejs.org" target="_blank" rel="noopener">@vuejs.org</a>
Bluesky account or the
<a href="https://x.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
X account for latest news in the Vue world.
</WelcomeItem>
<WelcomeItem>
<template #icon>
<SupportIcon />
</template>
<template #heading>Support Vue</template>
As an independent project, Vue relies on community backing for its sustainability. You can help
us by
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
</WelcomeItem>
</template>

87
Build_God_Admin_Frontend/Frontend/src/components/WelcomeItem.vue

@ -0,0 +1,87 @@
<template>
<div class="item">
<i>
<slot name="icon"></slot>
</i>
<div class="details">
<h3>
<slot name="heading"></slot>
</h3>
<slot></slot>
</div>
</div>
</template>
<style scoped>
.item {
margin-top: 2rem;
display: flex;
position: relative;
}
.details {
flex: 1;
margin-left: 1rem;
}
i {
display: flex;
place-items: center;
place-content: center;
width: 32px;
height: 32px;
color: var(--color-text);
}
h3 {
font-size: 1.2rem;
font-weight: 500;
margin-bottom: 0.4rem;
color: var(--color-heading);
}
@media (min-width: 1024px) {
.item {
margin-top: 0;
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
}
i {
top: calc(50% - 25px);
left: -26px;
position: absolute;
border: 1px solid var(--color-border);
background: var(--color-background);
border-radius: 8px;
width: 50px;
height: 50px;
}
.item:before {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
bottom: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:after {
content: ' ';
border-left: 1px solid var(--color-border);
position: absolute;
left: 0;
top: calc(50% + 25px);
height: calc(50% - 25px);
}
.item:first-of-type:before {
display: none;
}
.item:last-of-type:after {
display: none;
}
}
</style>

7
Build_God_Admin_Frontend/Frontend/src/components/icons/IconCommunity.vue

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
/>
</svg>
</template>

7
Build_God_Admin_Frontend/Frontend/src/components/icons/IconDocumentation.vue

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
<path
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
/>
</svg>
</template>

7
Build_God_Admin_Frontend/Frontend/src/components/icons/IconEcosystem.vue

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
<path
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
/>
</svg>
</template>

7
Build_God_Admin_Frontend/Frontend/src/components/icons/IconSupport.vue

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
/>
</svg>
</template>

19
Build_God_Admin_Frontend/Frontend/src/components/icons/IconTooling.vue

@ -0,0 +1,19 @@
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--mdi"
width="24"
height="24"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
>
<path
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
fill="currentColor"
></path>
</svg>
</template>

41
Build_God_Admin_Frontend/Frontend/src/constants/theme.ts

@ -0,0 +1,41 @@
export const ICONS = {
dashboard: '📊',
equipment: '⚔️',
level: '📈',
spirit: '✨',
pill: '💊',
bag:'🎒',
mission: '📜',
reward: {
pill: '💊',
equipment: '⚔️',
experience: '⭐',
money: '💰',
bag:'👜',
default: '🎁'
}
}
export const COLORS = {
equipment: '#a855f7',
level: '#f59e0b',
spirit: '#06b6d4',
pill: '#22c55e',
mission: '#3b82f6',
dashboard: '#8b5cf6',
rarity: {
common: '#9ca3af',
rare: '#22c55e',
epic: '#3b82f6',
legendary: '#f59e0b'
},
difficulty: {
easy: '#22c55e',
normal: '#f59e0b',
hard: '#ef4444',
purgatory: '#dc2626'
}
}

27
Build_God_Admin_Frontend/Frontend/src/main.ts

@ -0,0 +1,27 @@
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import ElementPlus from 'element-plus'
// Element Plus default styles
import 'element-plus/dist/index.css'
// Element Plus dark css variables (provides dark theme variables)
import 'element-plus/theme-chalk/dark/css-vars.css'
import App from './App.vue'
import router from './router'
import { useAuthStore } from './stores/auth'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
// 初始化认证状态(在 router 安装之前)
const authStore = useAuthStore()
authStore.initAuth()
app.use(router)
app.use(ElementPlus)
app.mount('#app')

104
Build_God_Admin_Frontend/Frontend/src/router/index.ts

@ -0,0 +1,104 @@
import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
import { useAuthStore } from '../stores/auth'
import LoginView from '../views/LoginView.vue'
import AdminLayout from '../views/AdminLayout.vue'
const routes: RouteRecordRaw[] = [
{
path: '/login',
name: 'login',
component: LoginView,
meta: { requiresAuth: false }
},
{
path: '/admin',
component: AdminLayout,
meta: { requiresAuth: true },
children: [
{
path: 'dashboard',
name: 'dashboard',
component: () => import('../views/admin/DashboardView.vue'),
meta: { title: '仪表板' }
},
{
path: 'users',
name: 'users',
component: () => import('../views/admin/UsersView.vue'),
meta: { title: '用户管理' }
},
{
path: 'equipments',
name: 'equipments',
component: () => import('../views/admin/EquipmentsView.vue'),
meta: { title: '装备管理' }
},
{
path: 'levels',
name: 'levels',
component: () => import('../views/admin/LevelsView.vue'),
meta: { title: '境界管理' }
},
{
path: 'professions',
name: 'professions',
component: () => import('../views/admin/SpiritsView.vue'),
meta: { title: '职业管理' }
},
{
path: 'pills',
name: 'pills',
component: () => import('../views/admin/PillsView.vue'),
meta: { title: '丹药管理' }
},
{
path: 'missions',
name: 'missions',
component: () => import('../views/admin/MissionView.vue'),
meta: { title: '任务管理' }
},
{
path: 'bags',
name: 'bags',
component: () => import('../views/admin/BagsView.vue'),
meta: { title: '背包管理' }
},
{
path: 'settings',
name: 'settings',
component: () => import('../views/admin/SettingsView.vue'),
meta: { title: '系统设置' }
}
]
},
{
path: '/',
redirect: '/admin/dashboard'
},
{
path: '/:pathMatch(.*)*',
redirect: '/admin/dashboard'
}
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
})
// 导航守卫
router.beforeEach((to, from, next) => {
const authStore = useAuthStore()
const isAuthenticated = authStore.isAuthenticated
const requiresAuth = to.meta.requiresAuth !== false
if (requiresAuth && !isAuthenticated) {
next('/login')
} else if (to.path === '/login' && isAuthenticated) {
next('/admin/dashboard')
} else {
next()
}
})
export default router

73
Build_God_Admin_Frontend/Frontend/src/stores/auth.ts

@ -0,0 +1,73 @@
import { defineStore } from "pinia";
import { ref, computed } from "vue";
import { loginApi, logoutApi } from "@/api/auth";
import type { User } from "@/api/auth";
import { jwtDecode } from "jwt-decode";
import router from "@/router";
export const useAuthStore = defineStore("auth", () => {
const user = ref<User | null>(null);
const token = ref<string>("");
const isAuthenticated = computed(() => !!token.value);
/**
* - API
* @param username
* @param password
* @returns
*/
const login = async (name: string, password: string): Promise<boolean> => {
try {
const response = await loginApi({ name, password });
if (response.token) {
token.value = response.token;
sessionStorage.setItem("auth_token", token.value);
const userInfo = jwtDecode<User>(response.token);
console.log(userInfo);
user.value = userInfo
return true;
} else {
return false;
}
} catch (error) {
console.error("登录失败:", error);
return false;
}
};
/**
*
*/
const logout = async () => {
try {
//await logoutApi();
router.push('/login')
} catch (error) {
console.error("登出失败:", error);
} finally {
token.value = "";
user.value = null;
sessionStorage.removeItem("auth_token");
sessionStorage.removeItem("auth_token");
}
};
/**
* sessionStorage
*/
const initAuth = () => {
const savedToken = sessionStorage.getItem("auth_token");
if (savedToken) {
token.value = savedToken;
}
};
return {
user,
token,
isAuthenticated,
login,
logout,
initAuth
};
});

12
Build_God_Admin_Frontend/Frontend/src/stores/counter.ts

@ -0,0 +1,12 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})

15
Build_God_Admin_Frontend/Frontend/src/views/AboutView.vue

@ -0,0 +1,15 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>
<style>
@media (min-width: 1024px) {
.about {
min-height: 100vh;
display: flex;
align-items: center;
}
}
</style>

79
Build_God_Admin_Frontend/Frontend/src/views/AdminLayout.vue

@ -0,0 +1,79 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import Sidebar from '@/components/Sidebar.vue'
import Header from '@/components/Header.vue'
import { ElMessage, ElMessageBox } from 'element-plus'
const router = useRouter()
const authStore = useAuthStore()
const collapsed = ref(false)
const handleLogout = () => {
ElMessageBox.confirm('确定要退出登录吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
authStore.logout()
ElMessage.success('已退出登录')
router.push('/login')
}).catch(() => {})
}
const toggleSidebar = () => {
collapsed.value = !collapsed.value
}
</script>
<template>
<div class="admin-layout">
<Sidebar :collapsed="collapsed" />
<div class="admin-content">
<Header @toggle="toggleSidebar" @logout="handleLogout" />
<main class="admin-main">
<RouterView />
</main>
</div>
</div>
</template>
<style scoped lang="css">
.admin-layout {
display: flex;
height: 100vh;
background-color: #0a0e27;
}
.admin-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.admin-main {
flex: 1;
overflow-y: auto;
padding: 20px;
background-color: #0a0e27;
}
.admin-main::-webkit-scrollbar {
width: 8px;
}
.admin-main::-webkit-scrollbar-track {
background: #1a1f3a;
}
.admin-main::-webkit-scrollbar-thumb {
background: #333;
border-radius: 4px;
}
.admin-main::-webkit-scrollbar-thumb:hover {
background: #444;
}
</style>

9
Build_God_Admin_Frontend/Frontend/src/views/HomeView.vue

@ -0,0 +1,9 @@
<script setup lang="ts">
import TheWelcome from '../components/TheWelcome.vue'
</script>
<template>
<main>
<TheWelcome />
</main>
</template>

166
Build_God_Admin_Frontend/Frontend/src/views/LoginView.vue

@ -0,0 +1,166 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
import { ElMessage } from 'element-plus'
import { Avatar,Lock } from '@element-plus/icons-vue'
const router = useRouter()
const authStore = useAuthStore()
const username = ref('')
const password = ref('')
const loading = ref(false)
const handleLogin = async () => {
if (!username.value || !password.value) {
ElMessage.error('请输入用户名和密码')
return
}
loading.value = true
try {
const success = await authStore.login(username.value, password.value)
if (success) {
ElMessage.success('登录成功')
router.push('/admin/dashboard')
} else {
ElMessage.error('登录失败,请检查用户名和密码')
}
} catch (error: any) {
ElMessage.error(error?.message || '登录出错,请重试')
} finally {
loading.value = false
}
}
const handleKeydown = (e: KeyboardEvent) => {
if (e.key === 'Enter' && !loading.value) {
handleLogin()
}
}
</script>
<template>
<div class="login-container">
<div class="login-box">
<h1>God Admin</h1>
<div class="form-group">
<el-input
v-model="username"
:prefix-icon="Avatar"
placeholder="用户名"
@keydown="handleKeydown"
/>
</div>
<div class="form-group">
<el-input
:prefix-icon="Lock"
v-model="password"
show-password
type="password"
placeholder="密码"
@keydown="handleKeydown"
/>
</div>
<el-button
@click="handleLogin"
:loading="loading"
type="primary"
class="login-btn"
>
{{ loading ? '登录中...' : '登 录' }}
</el-button>
<div class="tips">
<el-text>创造世界的基础数据🙂</el-text>
</div>
</div>
</div>
</template>
<style scoped lang="css">
.login-container {
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
.login-box {
background: #1a1a1a;
padding: 40px;
border-radius: 8px;
width: 100%;
max-width: 400px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
border: 1px solid #333;
}
h1 {
color: #ffffff;
text-align: center;
margin-bottom: 30px;
font-size: 28px;
font-weight: 600;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
color: #b0b0b0;
margin-bottom: 8px;
font-size: 14px;
font-weight: 500;
}
.form-group input {
width: 100%;
padding: 12px;
border: 1px solid #333;
border-radius: 4px;
background: #252525;
color: #ffffff;
font-size: 14px;
transition: all 0.3s ease;
box-sizing: border-box;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
background: #2a2a2a;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.login-btn {
width: 100%;
padding: 12px;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 10px;
}
.tips {
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid #333;
text-align: center;
color: #888;
font-size: 12px;
}
.tips p {
margin: 0;
line-height: 1.6;
}
</style>

365
Build_God_Admin_Frontend/Frontend/src/views/admin/BagsView.vue

@ -0,0 +1,365 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus, Edit, Delete, Close, Search } from '@element-plus/icons-vue'
import { ICONS } from '@/constants/theme'
import {
GetAllBags,
GetBagRarities,
GetBagItems,
AddItemToBag,
RemoveItemFromBag,
CreateBag,
UpdateBag,
DeleteBag,
GetCharacterBag,
type Bag,
type BagItem
} from '@/api/bag'
import type { EnumInfoDto } from '@/api'
const bags = ref<Bag[]>([])
const rarities = ref<EnumInfoDto[]>([])
const loading = ref(true)
const searchQuery = ref('')
const showBagDialog = ref(false)
const isEditingBag = ref(false)
const bagFormData = ref<Partial<Bag>>({
name: '',
rarity: 1,
capacity: 20,
description: ''
})
const selectedCharacterId = ref<number>()
const bagItems = ref<BagItem[]>([])
const itemsLoading = ref(false)
const rarityCapacityMap: Record<number, number> = {
1: 20,
2: 40,
3: 60,
4: 100
}
const filteredBags = computed(() => {
return bags.value.filter(bag =>
bag.name.toLowerCase().includes(searchQuery.value.toLowerCase())
)
})
const loadBags = async () => {
try {
loading.value = true
bags.value = await GetAllBags()
} catch (error) {
ElMessage.error('加载背包列表失败')
} finally {
loading.value = false
}
}
const loadRarities = async () => {
try {
rarities.value = await GetBagRarities()
} catch (error) {
console.error('Failed to load rarities:', error)
}
}
const getRarityName = (rarityId: number) => {
const rarity = rarities.value.find(r => r.id === rarityId)
return rarity?.description || '未知'
}
const getRarityClass = (rarityId: number) => {
const classes: Record<number, string> = {
1: 'rarity-common',
2: 'rarity-rare',
3: 'rarity-epic',
4: 'rarity-legendary'
}
return classes[rarityId] || ''
}
const openBagDialog = (bag?: Bag) => {
if (bag) {
isEditingBag.value = true
bagFormData.value = { ...bag }
} else {
isEditingBag.value = false
bagFormData.value = {
name: '',
rarity: 1,
capacity: 20,
description: ''
}
}
showBagDialog.value = true
}
const closeBagDialog = () => {
showBagDialog.value = false
}
const handleRarityChange = (rarity: number) => {
bagFormData.value.rarity = rarity
bagFormData.value.capacity = rarityCapacityMap[rarity] || 20
}
const saveBag = async () => {
if (!bagFormData.value.name) {
ElMessage.error('请填写背包名称')
return
}
try {
if (isEditingBag.value) {
await UpdateBag(bagFormData.value.id!, bagFormData.value as Bag)
ElMessage.success('更新成功')
} else {
await CreateBag(bagFormData.value as Bag)
ElMessage.success('创建成功')
}
closeBagDialog()
await loadBags()
} catch (error: any) {
ElMessage.error(error?.message || '操作失败')
}
}
const deleteBag = async (bag: Bag) => {
try {
await ElMessageBox.confirm(
`确定删除背包配置 "${bag.name}" 吗?`,
'提示',
{ confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
)
await DeleteBag(bag.id)
ElMessage.success('删除成功')
await loadBags()
} catch (error: any) {
if (error !== 'cancel') {
ElMessage.error(error?.message || '删除失败')
}
}
}
onMounted(() => {
loadBags()
loadRarities()
})
</script>
<template>
<div class="bags-container">
<div class="header">
<h2>背包管理</h2>
</div>
<div class="search-bar">
<el-input
v-model="searchQuery"
placeholder="搜索背包名称..."
style="max-width: 300px;"
clearable
></el-input>
<el-button type="primary" @click="openBagDialog()">
<el-icon class="el-icon--left"><Plus /></el-icon>
</el-button>
</div>
<el-table :data="filteredBags" style="width: 100%;" stripe v-loading="loading">
<el-table-column label="背包名称" prop="name"/>
<el-table-column label="稀有度">
<template #default="scoped">
<span class="rarity-badge" :class="getRarityClass(scoped.row.rarity)">
{{ getRarityName(scoped.row.rarity) }}
</span>
</template>
</el-table-column>
<el-table-column label="容量" prop="capacity"/>
<el-table-column label="描述" prop="description" show-overflow-tooltip/>
<el-table-column label="操作" width="150">
<template #default="scoped">
<el-button type="primary" @click="openBagDialog(scoped.row)" :icon="Edit" circle />
<el-button type="danger" @click="deleteBag(scoped.row)" :icon="Delete" circle />
</template>
</el-table-column>
</el-table>
<!-- Bag Config Dialog -->
<div v-if="showBagDialog" class="dialog-overlay">
<el-form class="dialog" :model="bagFormData" label-position="top">
<div class="dialog-header">
<h3>{{ isEditingBag ? '编辑背包' : '添加背包' }}</h3>
<el-button @click="closeBagDialog" :icon="Close" circle></el-button>
</div>
<el-form-item label="背包名称">
<el-input v-model="bagFormData.name" placeholder="请输入背包名称" clearable></el-input>
</el-form-item>
<el-form-item label="稀有度">
<el-select v-model="bagFormData.rarity" placeholder="请选择稀有度" style="width: 100%;" @change="handleRarityChange">
<el-option
v-for="rarity in rarities"
:key="rarity.id"
:label="`${rarity.description} (容量: ${rarityCapacityMap[rarity.id]})`"
:value="rarity.id"
/>
</el-select>
</el-form-item>
<el-form-item label="容量">
<el-input-number v-model="bagFormData.capacity" :min="1" :max="1000" style="width: 100%;"></el-input-number>
</el-form-item>
<el-form-item label="描述">
<el-input v-model="bagFormData.description" placeholder="请输入描述" type="textarea" :rows="3" clearable></el-input>
</el-form-item>
<div class="dialog-footer">
<el-button type="info" @click="closeBagDialog">取消</el-button>
<el-button type="primary" @click="saveBag">保存</el-button>
</div>
</el-form>
</div>
</div>
</template>
<style scoped lang="css">
.bags-container {
background: linear-gradient(135deg, #1f2937 0%, #111827 100%);
padding: 20px;
border-radius: 8px;
}
.header {
margin-bottom: 20px;
}
.header h2 {
margin: 0;
color: #e5e7eb;
font-size: 20px;
}
.section {
margin-top: 40px;
}
.section h3 {
margin: 0 0 16px 0;
color: #e5e7eb;
font-size: 18px;
}
.items-management {
display: flex;
gap: 12px;
align-items: center;
}
.search-bar {
display: flex;
gap: 12px;
margin-bottom: 20px;
}
.rarity-badge {
display: inline-block;
padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
}
.rarity-common {
background: rgba(156, 163, 175, 0.2);
color: #9ca3af;
}
.rarity-rare {
background: rgba(34, 197, 94, 0.2);
color: #22c55e;
}
.rarity-epic {
background: rgba(59, 130, 246, 0.2);
color: #3b82f6;
}
.rarity-legendary {
background: rgba(245, 158, 11, 0.2);
color: #f59e0b;
}
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.dialog {
background: #1f2937;
border: 1px solid #374151;
border-radius: 8px;
width: 100%;
max-width: 500px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
padding: 20px;
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #374151;
}
.dialog-header h3 {
margin: 0;
color: #e5e7eb;
font-size: 16px;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 20px;
padding-top: 15px;
border-top: 1px solid #374151;
}
.items-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
color: #e5e7eb;
}
.item-cell {
display: flex;
align-items: center;
gap: 8px;
}
.item-icon {
font-size: 18px;
}
.empty-text {
text-align: center;
color: #9ca3af;
padding: 40px;
}
</style>

460
Build_God_Admin_Frontend/Frontend/src/views/admin/DashboardView.vue

@ -0,0 +1,460 @@
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { useAuthStore } from '@/stores/auth'
import { GetStatisticsSummary, type StatisticsSummary } from '@/api/statistics'
import { ICONS, COLORS } from '@/constants/theme'
import { ElSkeleton } from 'element-plus'
const authStore = useAuthStore()
const loading = ref(true)
const data = ref<StatisticsSummary | null>(null)
const stats = computed(() => {
if (!data.value) return []
return [
{ label: '装备总数', value: data.value.equipmentCount, icon: ICONS.equipment, color: COLORS.equipment },
{ label: '境界总数', value: data.value.levelCount, icon: ICONS.level, color: COLORS.level },
{ label: '职业总数', value: data.value.professionCount, icon: ICONS.spirit, color: COLORS.spirit },
{ label: '丹药总数', value: data.value.pillCount, icon: ICONS.pill, color: COLORS.pill },
{ label: '任务总数', value: data.value.missionCount, icon: ICONS.mission, color: COLORS.mission }
]
})
const rarityMap: Record<number, string> = {
0: '普通',
1: '稀有',
2: '史诗',
3: '传说'
}
const difficultyMap: Record<number, string> = {
1: '简单',
2: '中等',
3: '困难',
4: '炼狱'
}
const gradeMap: Record<number, string> = {
0: '一阶',
1: '二阶',
2: '三阶',
3: '四阶'
}
const equipmentRarityData = computed(() => {
if (!data.value?.equipmentByRarity) return []
return Object.entries(data.value.equipmentByRarity).map(([key, value]) => ({
label: rarityMap[Number(key)] || `稀有度${key}`,
value,
color: getRarityColor(Number(key))
}))
})
const missionDifficultyData = computed(() => {
if (!data.value?.missionByDifficulty) return []
return Object.entries(data.value.missionByDifficulty).map(([key, value]) => ({
label: difficultyMap[Number(key)] || `难度${key}`,
value,
color: getDifficultyColor(Number(key))
}))
})
const pillGradeData = computed(() => {
if (!data.value?.pillByGrade) return []
return Object.entries(data.value.pillByGrade).map(([key, value]) => ({
label: gradeMap[Number(key)] || `等级${key}`,
value,
color: getGradeColor(Number(key))
}))
})
function getRarityColor(rarity: number): string {
const colors: Record<number, string> = {
1: '#9ca3af',
2: '#22c55e',
3: '#3b82f6',
4: '#f59e0b'
}
return colors[rarity] || '#9ca3af'
}
function getDifficultyColor(difficulty: number): string {
const colors: Record<number, string> = {
1: '#22c55e',
2: '#f59e0b',
3: '#ef4444',
4: '#dc2626'
}
return colors[difficulty] || '#9ca3af'
}
function getGradeColor(grade: number): string {
const colors: Record<number, string> = {
0: '#9ca3af',
1: '#22c55e',
2: '#3b82f6',
3: '#a855f7'
}
return colors[grade] || '#9ca3af'
}
function getMaxValue(arr: { value: number }[]): number {
return Math.max(...arr.map(item => item.value), 1)
}
onMounted(async () => {
try {
data.value = await GetStatisticsSummary()
} catch (error) {
console.error('Failed to load statistics:', error)
} finally {
loading.value = false
}
})
</script>
<template>
<div class="dashboard">
<div class="welcome-section">
<h1>欢迎{{ authStore.user?.name || '修仙者' }}</h1>
<p>掌控修仙世界的数据中心</p>
</div>
<div class="stats-grid">
<template v-if="loading">
<div v-for="i in 5" :key="i" class="stat-card">
<el-skeleton animated :rows="1" />
</div>
</template>
<template v-else>
<div
v-for="stat in stats"
:key="stat.label"
class="stat-card"
:style="{ '--accent-color': stat.color }"
>
<div class="stat-icon">{{ stat.icon }}</div>
<div class="stat-content">
<div class="stat-value">{{ stat.value }}</div>
<div class="stat-label">{{ stat.label }}</div>
</div>
<div class="stat-glow"></div>
</div>
</template>
</div>
<div class="charts-section">
<h2 class="section-title">数据分布</h2>
<div class="charts-grid">
<div class="chart-card">
<div class="chart-header">
<span class="chart-icon">{{ ICONS.equipment }}</span>
<span class="chart-title">装备稀有度分布</span>
</div>
<div class="chart-content">
<template v-if="loading">
<el-skeleton animated :rows="4" />
</template>
<template v-else-if="equipmentRarityData.length === 0">
<div class="empty-data">暂无数据</div>
</template>
<template v-else>
<div
v-for="item in equipmentRarityData"
:key="item.label"
class="bar-item"
>
<div class="bar-label">{{ item.label }}</div>
<div class="bar-wrapper">
<div
class="bar-fill"
:style="{
width: `${(item.value / getMaxValue(equipmentRarityData)) * 100}%`,
backgroundColor: item.color
}"
></div>
</div>
<div class="bar-value">{{ item.value }}</div>
</div>
</template>
</div>
</div>
<div class="chart-card">
<div class="chart-header">
<span class="chart-icon">{{ ICONS.mission }}</span>
<span class="chart-title">任务难度分布</span>
</div>
<div class="chart-content">
<template v-if="loading">
<el-skeleton animated :rows="4" />
</template>
<template v-else-if="missionDifficultyData.length === 0">
<div class="empty-data">暂无数据</div>
</template>
<template v-else>
<div
v-for="item in missionDifficultyData"
:key="item.label"
class="bar-item"
>
<div class="bar-label">{{ item.label }}</div>
<div class="bar-wrapper">
<div
class="bar-fill"
:style="{
width: `${(item.value / getMaxValue(missionDifficultyData)) * 100}%`,
backgroundColor: item.color
}"
></div>
</div>
<div class="bar-value">{{ item.value }}</div>
</div>
</template>
</div>
</div>
<div class="chart-card">
<div class="chart-header">
<span class="chart-icon">{{ ICONS.pill }}</span>
<span class="chart-title">丹药等级分布</span>
</div>
<div class="chart-content">
<template v-if="loading">
<el-skeleton animated :rows="4" />
</template>
<template v-else-if="pillGradeData.length === 0">
<div class="empty-data">暂无数据</div>
</template>
<template v-else>
<div
v-for="item in pillGradeData"
:key="item.label"
class="bar-item"
>
<div class="bar-label">{{ item.label }}</div>
<div class="bar-wrapper">
<div
class="bar-fill"
:style="{
width: `${(item.value / getMaxValue(pillGradeData)) * 100}%`,
backgroundColor: item.color
}"
></div>
</div>
<div class="bar-value">{{ item.value }}</div>
</div>
</template>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="css">
.dashboard {
max-width: 1400px;
margin: 0 auto;
}
.welcome-section {
margin-bottom: 40px;
}
.welcome-section h1 {
color: #e5e7eb;
font-size: 28px;
margin: 0 0 10px 0;
}
.welcome-section p {
color: #9ca3af;
font-size: 14px;
margin: 0;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 16px;
margin-bottom: 40px;
}
@media (max-width: 1200px) {
.stats-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 768px) {
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
}
.stat-card {
background: linear-gradient(135deg, #1a1a2e 0%, #16162a 100%);
padding: 20px;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.05);
display: flex;
align-items: center;
gap: 16px;
position: relative;
overflow: hidden;
transition: all 0.3s ease;
}
.stat-card:hover {
transform: translateY(-4px);
border-color: var(--accent-color);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4);
}
.stat-card:hover .stat-glow {
opacity: 1;
}
.stat-glow {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: var(--accent-color);
opacity: 0.6;
transition: opacity 0.3s ease;
}
.stat-icon {
font-size: 36px;
line-height: 1;
}
.stat-content {
flex: 1;
z-index: 1;
}
.stat-value {
font-size: 28px;
font-weight: 700;
color: #fff;
margin-bottom: 4px;
line-height: 1.2;
}
.stat-label {
font-size: 13px;
color: #9ca3af;
}
.charts-section {
margin-top: 40px;
}
.section-title {
color: #e5e7eb;
font-size: 20px;
margin: 0 0 20px 0;
padding-left: 12px;
border-left: 3px solid #8b5cf6;
}
.charts-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
@media (max-width: 1024px) {
.charts-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 640px) {
.charts-grid {
grid-template-columns: 1fr;
}
}
.chart-card {
background: linear-gradient(135deg, #1a1a2e 0%, #16162a 100%);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.05);
overflow: hidden;
}
.chart-header {
padding: 16px 20px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
display: flex;
align-items: center;
gap: 10px;
}
.chart-icon {
font-size: 20px;
}
.chart-title {
color: #e5e7eb;
font-size: 15px;
font-weight: 600;
}
.chart-content {
padding: 20px;
}
.bar-item {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 14px;
}
.bar-item:last-child {
margin-bottom: 0;
}
.bar-label {
width: 40px;
color: #9ca3af;
font-size: 13px;
flex-shrink: 0;
}
.bar-wrapper {
flex: 1;
height: 20px;
background: rgba(255, 255, 255, 0.05);
border-radius: 4px;
overflow: hidden;
}
.bar-fill {
height: 100%;
border-radius: 4px;
transition: width 0.5s ease;
}
.bar-value {
width: 30px;
color: #e5e7eb;
font-size: 14px;
font-weight: 600;
text-align: right;
flex-shrink: 0;
}
.empty-data {
color: #6b7280;
text-align: center;
padding: 40px 0;
font-size: 14px;
}
</style>

640
Build_God_Admin_Frontend/Frontend/src/views/admin/EquipmentsView.vue

@ -0,0 +1,640 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { AddEquipmentTemplate, DeleteEquipmentTemplate, GetEquipmentTemplateList, GetEquipmentRarities, GetEquipmentTypes, GetEquipmentAttributeTypes, UpdateEquipmentTemplate, type EquipmentTemplate, type EquipmentAttributePool } from '@/api/equipment'
import type { EnumInfoDto } from '@/api'
import { GetLevelList, type Level } from '@/api/level'
import { Plus, Edit, Delete, Close } from '@element-plus/icons-vue'
//
const equipmentTemplates = ref<EquipmentTemplate[]>([])
//
const equipmentTypes = ref<EnumInfoDto[]>([])
//
const equipmentRarities = ref<EnumInfoDto[]>([])
//
const levelData = ref<Level[]>([])
//
const attributeTypes = ref<EnumInfoDto[]>([])
//
const currentPage = ref(1)
const pageSize = ref(10)
const totalCount = ref(0)
const pageSizes = [5, 10, 20, 50]
//
const eqFileterType = ref<number | undefined>(undefined)
const showDialog = ref(false)
const isEditing = ref(false)
const searchQuery = ref('')
const formData = ref<Partial<EquipmentTemplate>>({
name: '',
description: '',
type: 0,
rarity: 0,
requirdLevelId: 0,
setId: null,
money: 0,
attributePool: '[]',
randomAttrCount: 4,
maxEnhanceLevel: 10
})
const attributePoolList = ref<EquipmentAttributePool[]>([])
const filteredEquipments = computed(() => {
return equipmentTemplates.value.filter(eq =>
eq.name.toLowerCase().includes(searchQuery.value.toLowerCase())
)
})
const totalPages = computed(() => {
return Math.max(1, Math.ceil(totalCount.value / pageSize.value))
})
const translateEquipmentType = (typeId: number) => {
const type = equipmentTypes.value.find(t => t.id === typeId)
return type ? type.description : '未知类型'
}
const translateRarity = (id: number) => {
const item = equipmentRarities.value.find(x => x.id == id);
return item ? item.description : '未知'
}
const translateLevel = (levelId: number) => {
var item = levelData.value.find(x => x.levelId == levelId)
return item ? item.name : '未知';
}
const addAttributePool = () => {
attributePoolList.value.push({
type: 1,
min: 0,
max: 100,
weight: 1
})
updateAttributePool()
}
const removeAttributePool = (index: number) => {
attributePoolList.value.splice(index, 1)
updateAttributePool()
}
const updateAttributePool = () => {
formData.value.attributePool = JSON.stringify(attributePoolList.value)
}
const parseAttributePool = (json: string) => {
try {
return JSON.parse(json) as EquipmentAttributePool[]
} catch {
return []
}
}
const openDialog = (eq?: EquipmentTemplate) => {
if (eq != undefined) {
isEditing.value = true
formData.value = { ...eq }
attributePoolList.value = parseAttributePool(eq.attributePool || '[]')
} else {
isEditing.value = false
formData.value = {
name: '',
description: '',
type: undefined,
rarity: undefined,
requirdLevelId: undefined,
setId: null,
money: undefined,
attributePool: '[]',
randomAttrCount: 4,
maxEnhanceLevel: 10
}
attributePoolList.value = []
}
showDialog.value = true
}
const closeDialog = () =>
{
showDialog.value = false
}
const saveEquipment = async () => {
if (formData.value.name == undefined
|| formData.value.description == undefined
|| formData.value.type == undefined
|| formData.value.rarity == undefined
|| formData.value.requirdLevelId == undefined
|| formData.value.money == undefined
) {
ElMessage.error('请填写必填项')
return;
}
updateAttributePool()
if (isEditing.value) {
const index = equipmentTemplates.value.findIndex(p => p.id === formData.value.id)
if (index > -1) {
equipmentTemplates.value[index] = { ...equipmentTemplates.value[index], ...formData.value } as EquipmentTemplate
var updateItem = equipmentTemplates.value[index];
var result = await UpdateEquipmentTemplate(updateItem)
if (result) {
ElMessage.success('装备模板更新成功')
closeDialog()
await refreshEquipments()
}
else {
ElMessage.error('装备模板更新失败')
}
}
} else {
const newOne: EquipmentTemplate = {
id: 1,
name: formData.value.name || '',
description: formData.value.description || '',
type: formData.value.type || 1,
rarity: formData.value.rarity || 1,
requirdLevelId: formData.value.requirdLevelId || 1,
setId: formData.value.setId ?? null,
money: formData.value.money || 0,
attributePool: formData.value.attributePool || '[]',
randomAttrCount: formData.value.randomAttrCount || 4,
maxEnhanceLevel: formData.value.maxEnhanceLevel || 10,
}
var result = await AddEquipmentTemplate(newOne)
if (result) {
ElMessage.success('装备模板添加成功')
closeDialog()
await refreshEquipments()
}
else {
ElMessage.success('添加失败')
}
}
}
const deleteEquipment = (eq: EquipmentTemplate) => {
ElMessageBox.confirm(`确定删除装备模板 "${eq.name}" 吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(async () => {
var id = eq.id
var result = await DeleteEquipmentTemplate(id)
if (result) {
ElMessage.success('装备模板删除成功')
await refreshEquipments()
}
else {
ElMessage.error('删除失败')
}
}).catch(() => { })
}
const getEqTypeClass = (eqType: number) => {
switch (eqType) {
case 1:
return 'eqType-weapon'
case 2:
return 'eqType-Armor'
case 3:
return 'eqType-Helmet'
case 4:
return 'eqType-Necklace'
case 5:
return 'eqType-Ring'
case 6:
return 'eqType-Boots'
default:
return ''
}
}
//
onMounted(async () => {
await refreshEquipments()
await fetchLevels()
await fetchRarities()
await fetchAttributeTypes()
})
const refreshEquipments = async (page?: number) => {
console.log('current-page', page)
if (page !== undefined) currentPage.value = page
const res = await GetEquipmentTemplateList(eqFileterType.value, currentPage.value, pageSize.value)
// backend may return either an array or a paged object
if (Array.isArray(res)) {
equipmentTemplates.value = res
totalCount.value = res.length
} else {
equipmentTemplates.value = res.items || []
totalCount.value = res.totalCount || (res.items ? res.items.length : 0)
}
var eqTypes = await GetEquipmentTypes();
equipmentTypes.value = eqTypes;
}
const changePageSize = async () => {
currentPage.value = 1
await refreshEquipments(1)
}
const fetchLevels = async () => {
var data = await GetLevelList()
levelData.value = data;
}
const fetchRarities = async () => {
var data = await GetEquipmentRarities()
equipmentRarities.value = data
}
const fetchAttributeTypes = async () => {
var data = await GetEquipmentAttributeTypes()
attributeTypes.value = data
}
</script>
<template>
<div class="equipments-container">
<div class="header">
<h2>装备模板管理</h2>
</div>
<div class="search-bar">
<el-input v-model="searchQuery" placeholder="搜索装备名称..." style="max-width: 300px;"></el-input>
<el-select v-model="eqFileterType" style="max-width: 200px;" @change="refreshEquipments(undefined)"
placeholder="搜索装备类型..." clearable>
<el-option v-for="(value, index) in equipmentTypes" :key="index" :value="value.id" :label="value.description" />
</el-select>
<el-button type="primary" @click="openDialog(undefined)">
<el-icon class="el-icon--left">
<Plus />
</el-icon>
</el-button>
</div>
<el-table :data="filteredEquipments" style="width: 100%;" stripe>
<el-table-column label="名称" prop="name" width="150"></el-table-column>
<el-table-column label="描述" prop="description" show-overflow-tooltip></el-table-column>
<el-table-column label="类型">
<template #default="scoped">
<span class="eqType" :class="getEqTypeClass(scoped.row.type)">
{{ translateEquipmentType(scoped.row.type) }}
</span>
</template>
</el-table-column>
<el-table-column label="稀有度">
<template #default="scoped">
<span>
{{ translateRarity(scoped.row.rarity) }}
</span>
</template>
</el-table-column>
<el-table-column label="需求等级">
<template #default="scoped">
<span>
{{ translateLevel(scoped.row.requirdLevelId) }}
</span>
</template>
</el-table-column>
<el-table-column label="随机属性数" prop="randomAttrCount" width="100"></el-table-column>
<el-table-column label="最大强化" prop="maxEnhanceLevel" width="100"></el-table-column>
<el-table-column label="价格" prop="money" width="80"></el-table-column>
<el-table-column label="编辑" fixed="right">
<template #default="scoped">
<el-button type="primary" @click="openDialog(scoped.row)" :icon="Edit" circle />
<el-button type="danger" @click="deleteEquipment(scoped.row)" :icon="Delete" circle />
</template>
</el-table-column>
</el-table>
<div class="pagination" v-if="totalCount > 0">
<el-pagination layout="prev, pager, next" :total="totalCount" :current-page="currentPage"
@current-change="refreshEquipments" />
</div>
<!-- Dialog -->
<div v-if="showDialog" class="dialog-overlay">
<el-form :inline="true" :model="formData" class="dialog" label-position="top">
<div class="dialog-header">
<h3>{{ isEditing ? '编辑装备模板' : '添加装备模板' }}</h3>
<el-button @click="closeDialog" :icon="Close" circle />
</div>
<el-form-item label="装备名称">
<el-input v-model="formData.name" placeholder="装备名称" clearable />
</el-form-item>
<el-form-item label="分类">
<el-select v-model="formData.type">
<el-option v-for="(value, index) in equipmentTypes" :key="index" :value="value.id" :label="value.description">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="稀有度">
<el-select v-model="formData.rarity">
<el-option v-for="(value, index) in equipmentRarities" :key="index" :value="value.id" :label="value.description">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="需求等级">
<el-select v-model="formData.requirdLevelId">
<el-option v-for="(value, index) in levelData" :key="index" :value="value.levelId" :label="value.name">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="价格">
<el-input v-model="formData.money" placeholder="价格" clearable />
</el-form-item>
<el-form-item label="随机属性数量">
<el-input-number v-model="formData.randomAttrCount" :min="1" :max="10" />
</el-form-item>
<el-form-item label="最大强化等级">
<el-input-number v-model="formData.maxEnhanceLevel" :min="1" :max="20" />
</el-form-item>
<!-- 属性池配置 -->
<div class="attribute-pool-section">
<div class="pool-header">
<span>属性池配置</span>
<el-button type="primary" size="small" @click="addAttributePool">添加属性</el-button>
</div>
<el-table :data="attributePoolList" border size="small">
<el-table-column label="属性类型" width="350">
<template #default="scoped">
<el-select v-model="scoped.row.type" size="small" @change="updateAttributePool">
<el-option v-for="(value, index) in attributeTypes" :key="index" :value="value.id" :label="value.description"/>
</el-select>
</template>
</el-table-column>
<el-table-column label="最小值" width="150">
<template #default="scoped">
<el-input-number v-model="scoped.row.min" :min="0" size="small" controls-position="right" />
</template>
</el-table-column>
<el-table-column label="最大值" width="150">
<template #default="scoped">
<el-input-number v-model="scoped.row.max" :min="0" size="small" controls-position="right" />
</template>
</el-table-column>
<el-table-column label="权重" width="150">
<template #default="scoped">
<el-input-number v-model="scoped.row.weight" :min="1" size="small" controls-position="right" />
</template>
</el-table-column>
<el-table-column label="操作" width="80">
<template #default="scoped">
<el-button type="danger" size="small" @click="removeAttributePool(scoped.$index)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<el-form-item label="描述" style="width: 100%;">
<el-input placeholder="描述" type="textarea" style="width: 100%;" v-model="formData.description"></el-input>
</el-form-item>
<div class="dialog-footer">
<el-button type="info" @click="closeDialog">取消</el-button>
<el-button type="primary" @click="saveEquipment">保存</el-button>
</div>
</el-form>
</div>
</div>
</template>
<style scoped lang="css">
.equipments-container {
background: linear-gradient(135deg, #1f2937 0%, #111827 100%);
padding: 20px;
border-radius: 8px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.header h2 {
margin: 0;
color: #e5e7eb;
font-size: 20px;
}
.search-bar {
display: flex;
margin-bottom: 20px;
gap: 12px;
}
.table {
width: 100%;
border-collapse: collapse;
overflow-x: auto;
}
.table thead tr {
border-bottom: 2px solid #374151;
}
.table th {
text-align: left;
padding: 12px;
color: #9ca3af;
font-weight: 600;
font-size: 13px;
}
.table td {
padding: 12px;
color: #e5e7eb;
font-size: 14px;
border-bottom: 1px solid #374151;
}
.table tbody tr:hover {
background-color: rgba(102, 126, 234, 0.05);
}
.eqType {
display: inline-block;
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
}
.eqType-Boots {
background-color: #7b4ee3;
}
.eqType-Ring {
background-color: #b2c522;
}
.eqType-Necklace {
background-color: #2281c5;
}
.eqType-Helmet {
background-color: #22c55e;
}
.eqType-Armor {
background-color: #f59e0b;
}
.eqType-weapon {
background-color: #ef4444;
}
.actions {
display: flex;
gap: 8px;
}
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.dialog {
background: #1f2937;
border: 1px solid #374151;
border-radius: 8px;
width: 100%;
max-width: 1100px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
padding: 10px;
}
.dialog .el-input {
--el-input-width: 300px;
}
.dialog .el-select {
--el-select-width: 300px;
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #374151;
width: 100%;
margin-bottom: 10px;
padding: 10px 10px 10px 0px;
}
.dialog-header h3 {
margin: 0;
color: #e5e7eb;
font-size: 16px;
}
.close-btn {
background: none;
border: none;
color: #9ca3af;
font-size: 20px;
cursor: pointer;
padding: 0;
}
.pagination {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
.close-btn:hover {
color: #e5e7eb;
}
.dialog-content {
padding: 20px;
}
.form-group {
margin-bottom: 16px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #e5e7eb;
font-size: 14px;
font-weight: 500;
}
.input {
width: 100%;
padding: 10px;
background: #374151;
border: 1px solid #4b5563;
border-radius: 4px;
color: #e5e7eb;
font-size: 14px;
}
.input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
border-top: 1px solid #374151;
width: 100%;
padding: 10px 10px 0 0;
}
.attribute-pool-section {
width: 100%;
margin: 10px 0;
padding: 10px;
background: #374151;
border-radius: 4px;
}
.pool-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
color: #e5e7eb;
font-weight: 500;
}
</style>

433
Build_God_Admin_Frontend/Frontend/src/views/admin/LevelsView.vue

@ -0,0 +1,433 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { AddLevel, DeleteLevel, GetLevelList, UpdateLevel, type Level } from '@/api/level'
import { Plus,Edit,Delete,Close } from '@element-plus/icons-vue'
const levels = ref<Level[]>([])
const showDialog = ref(false)
const isEditing = ref(false)
const searchQuery = ref('')
const statusFilter = ref<string>('')
const formData = ref<Partial<Level>>({
name: '',
levelId: null,
currentLevelMinExp: null,
nextLevelId: null,
baseBreakthroughRate: 0,
failIncrement: 0,
description: ''
})
const filteredLevels = computed(() => {
return levels.value.filter(level => {
const matchSearch = level.name.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
level.description.toLowerCase().includes(searchQuery.value.toLowerCase())
const matchStatus = !statusFilter.value || level.nextLevelId === null === (statusFilter.value === '已满级')
return matchSearch && matchStatus
})
})
const openDialog = (level?: Level) => {
if (level!=undefined) {
isEditing.value = true
formData.value = { ...level }
} else {
isEditing.value = false
formData.value = {
name: '',
levelId: 0,
currentLevelMinExp: 0,
nextLevelId: null,
baseBreakthroughRate: 0,
failIncrement: 0,
description: ''
}
}
showDialog.value = true
}
const closeDialog = () => {
showDialog.value = false
}
const saveLevel = async () => {
if (!formData.value.name || !formData.value.currentLevelMinExp || !formData.value.levelId) {
ElMessage.error('请填写必填项')
return
}
if (isEditing.value) {
if (formData.value !== undefined) {
const ret = await UpdateLevel(formData.value as Level);
if (!ret) {
ElMessage.error('更新错误');
return;
}
}
else {
ElMessage.error('更新错误,代码出问题了')
return;
}
ElMessage.success('境界更新成功')
closeDialog()
await RefreshLevels();
} else {
const newLevel: Partial<Level> = {
levelId: formData.value.levelId || null,
name: formData.value.name!,
currentLevelMinExp: formData.value.currentLevelMinExp || null,
nextLevelId: formData.value.nextLevelId || null,
baseBreakthroughRate: formData.value.baseBreakthroughRate || 0,
failIncrement: formData.value.failIncrement || 0,
description: formData.value.description || ''
}
const result = await AddLevel(newLevel as Level)
if (result) {
ElMessage.success('添加成功')
RefreshLevels()
closeDialog()
}
else {
ElMessage.error('添加错误')
}
}
}
const deleteLevel = async (level: Level) => {
ElMessageBox.confirm(`确定删除境界 "${level.levelId}" 吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(async () => {
const result = await DeleteLevel(level.id);
if (result) {
ElMessage.success('删除境界成功')
await RefreshLevels()
}
}).catch(() => {
})
}
const translateLevelName = (nextLevelId:number)=> {
var nextLevel = levels.value.find(x=>x.levelId == nextLevelId)
return nextLevel ? nextLevel.name : '未知';
}
onMounted(async () => {
RefreshLevels();
})
const RefreshLevels = async () => {
const result = await GetLevelList();
levels.value = result;
}
</script>
<template>
<div class="levels-container">
<div class="header">
<h2>境界管理</h2>
</div>
<div class="search-bar">
<el-input v-model="searchQuery" placeholder="搜索境界名称..." style="max-width: 300px;"></el-input>
<el-button type="primary" @click="openDialog(undefined)">
<el-icon class="el-icon--left"><Plus /></el-icon>
</el-button>
</div>
<el-table :data="filteredLevels" style="width: 100%;" stripe>
<el-table-column label="境界ID" prop="levelId"/>
<el-table-column label="境界名称" prop="name"/>
<el-table-column label="当前境界最低经验" prop="currentLevelMinExp"/>
<el-table-column label="下一境界ID">
<template #default="scoped">
<span>
{{ translateLevelName(scoped.row.nextLevelId) }}
</span>
</template>
</el-table-column>
<el-table-column label="基础突破概率" prop="baseBreakthroughRate">
<template #default="scoped">
<span>{{ scoped.row.baseBreakthroughRate }}%</span>
</template>
</el-table-column>
<el-table-column label="失败增量" prop="failIncrement">
<template #default="scoped">
<span>{{ scoped.row.failIncrement }}%</span>
</template>
</el-table-column>
<el-table-column label="描述" prop="description" show-overflow-tooltip/>
<el-table-column table="操作">
<template #default="scoped">
<el-button type="primary" @click="openDialog(scoped.row)" :icon="Edit" circle />
<el-button type="danger" @click="deleteLevel(scoped.row)" :icon="Delete" circle />
</template>
</el-table-column>
</el-table>
<!-- Dialog -->
<!--@click.self="closeDialog"-->
<div v-if="showDialog" class="dialog-overlay">
<el-form class="dialog" :model="formData" label-position="top">
<div class="dialog-header">
<h3>{{ isEditing ? '编辑境界' : '添加境界' }}</h3>
<el-button @click="closeDialog" :icon="Close" circle></el-button>
</div>
<el-form-item label="境界ID">
<el-input v-model="formData.levelId" placeholder="境界ID" clearable></el-input>
</el-form-item>
<el-form-item label="境界名称">
<el-input v-model="formData.name" placeholder="境界名称" clearable></el-input>
</el-form-item>
<el-form-item label="当前境界最低经验">
<el-input v-model="formData.currentLevelMinExp" placeholder="当前境界最低经验" clearable></el-input>
</el-form-item>
<el-form-item label="下一境界ID">
<el-input v-model="formData.nextLevelId" placeholder="下一境界ID" clearable></el-input>
</el-form-item>
<el-form-item label="基础突破概率[0-100]">
<el-input v-model="formData.baseBreakthroughRate" placeholder="基础突破概率" clearable></el-input>
</el-form-item>
<el-form-item label="失败增量[0-100]">
<el-input v-model="formData.failIncrement" placeholder="失败后增加的概率" clearable></el-input>
</el-form-item>
<el-form-item label="描述">
<el-input v-model="formData.description" placeholder="描述" type="textarea" clearable></el-input>
</el-form-item>
<div class="dialog-footer">
<el-button type="info" @click="closeDialog">取消</el-button>
<el-button type="primary" @click="saveLevel">保存</el-button>
</div>
</el-form>
</div>
</div>
</template>
<style scoped lang="css">
.levels-container {
background: linear-gradient(135deg, #1f2937 0%, #111827 100%);
padding: 20px;
border-radius: 8px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.header h2 {
margin: 0;
color: #e5e7eb;
font-size: 20px;
}
.filters {
display: flex;
gap: 12px;
margin-bottom: 20px;
}
.search-bar {
display: flex;
margin-bottom: 20px;
gap: 12px;
}
.search-input,
.status-filter {
width: 100%;
max-width: 400px;
padding: 10px 12px;
color: #e5e7eb;
background: #374151;
font-size: 14px;
border-radius: 4px;
border:1px solid #4b5563
}
.search-input {
flex: 1;
max-width: 400px;
}
.search-input::placeholder {
color: #9ca3af;
}
.search-input:focus,
.status-filter:focus {
outline: none;
blevel-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.table {
width: 100%;
blevel-collapse: collapse;
overflow-x: auto;
}
.table thead tr {
blevel-bottom: 2px solid #374151;
}
.table th {
text-align: left;
padding: 12px;
color: #9ca3af;
font-weight: 600;
font-size: 13px;
}
.table td {
padding: 12px;
color: #e5e7eb;
font-size: 14px;
blevel-bottom: 1px solid #374151;
}
.table tbody tr:hover {
background-color: rgba(102, 126, 234, 0.05);
}
.status {
display: inline-block;
padding: 4px 12px;
blevel-radius: 4px;
font-size: 12px;
font-weight: 600;
}
.status-completed {
background-color: rgba(34, 197, 94, 0.2);
color: #22c55e;
}
.status-processing {
background-color: rgba(59, 130, 246, 0.2);
color: #3b82f6;
}
.status-cancelled {
background-color: rgba(239, 68, 68, 0.2);
color: #ef4444;
}
.actions {
display: flex;
gap: 8px;
}
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.dialog {
background: #1f2937;
border: 1px solid #374151;
border-radius: 8px;
width: 100%;
max-width: 500px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
padding: 10px;
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #374151;
width: 100%;
margin-bottom: 10px;
padding: 10px 0px 10px 0px;
}
.dialog-header h3 {
margin: 0;
color: #e5e7eb;
font-size: 16px;
}
.close-btn {
background: none;
color: #9ca3af;
font-size: 20px;
cursor: pointer;
padding: 0;
}
.close-btn:hover {
color: #e5e7eb;
}
.dialog-content {
padding: 20px;
}
.form-group {
margin-bottom: 16px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #e5e7eb;
font-size: 14px;
font-weight: 500;
}
.input {
width: 100%;
padding: 10px;
background: #374151;
blevel: 1px solid #4b5563;
blevel-radius: 4px;
color: #e5e7eb;
font-size: 14px;
}
.input:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.input:focus {
outline: none;
blevel-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
border-top: 1px solid #374151;
width: 100%;
padding: 10px 0px 0 0;
}
</style>

1042
Build_God_Admin_Frontend/Frontend/src/views/admin/MissionView.vue

File diff suppressed because it is too large

542
Build_God_Admin_Frontend/Frontend/src/views/admin/PillsView.vue

@ -0,0 +1,542 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { AddPill, DeletePill, getPillGrades, GetPillList, getPillRarities, getPillTypes, UpdatePill, type Pill } from '@/api/pill'
import type { EnumInfoDto } from '@/api'
import { GetLevelList, type Level } from '@/api/level'
import { Plus, Edit, Delete, Close } from '@element-plus/icons-vue'
const pills = ref<Pill[]>([])
//
const grades = ref<EnumInfoDto[]>([])
//
const types = ref<EnumInfoDto[]>([])
//
const rarities = ref<EnumInfoDto[]>([])
//
const levels = ref<Level[]>([])
const showDialog = ref(false)
const isEditing = ref(false)
const searchQuery = ref('')
const formData = ref<Pill>({
id: 0,
name: '',
grade: undefined,
type: undefined,
rarity: undefined,
money: undefined,
description: '',
requirdLevelId: undefined,
effectValue: undefined,
duration: undefined,
})
const filteredPills = computed(() => {
if (searchQuery.value == '') {
return pills.value
}
return pills.value.filter(pill => {
const matchSearch = pill.name.toLowerCase().includes(searchQuery.value.toLowerCase())
return matchSearch
})
})
const translateGrade = (id: number) => {
var item = grades.value.find(x => x.id == id);
if (item) {
return item.description
}
return '未知'
}
const translateType = (id: number) => {
var item = types.value.find(x => x.id == id);
if (item) {
return item.description
}
return '未知'
}
const translateRarity = (id: number) => {
var item = rarities.value.find(x => x.id == id);
if (item) {
return item.description
}
return '未知'
}
const translateLevel = (id: number) => {
var item = levels.value.find(x => x.levelId == id);
if (item) {
return item.name
}
return '未知'
}
const openDialog = (pill?: Pill) => {
if (pill != undefined) {
isEditing.value = true
formData.value = { ...pill }
} else {
isEditing.value = false
formData.value = {
id: 0,
name: '',
grade: undefined,
type: undefined,
rarity: undefined,
money: undefined,
description: '',
requirdLevelId: undefined,
effectValue: undefined,
duration: undefined,
}
}
showDialog.value = true
}
const closeDialog = () => {
showDialog.value = false
}
const savePill = async () => {
console.log('formdata', formData.value)
if (!formData.value.name
|| !formData.value.grade
|| !formData.value.type
|| !formData.value.rarity
|| !formData.value.money
|| !formData.value.description
|| !formData.value.requirdLevelId
|| !formData.value.effectValue
|| !formData.value.duration) {
ElMessage.error('请填写必填项')
return
}
if (isEditing.value) {
const index = pills.value.findIndex(p => p.id === formData.value.id)
if (index > -1) {
pills.value[index] = { ...pills.value[index], ...formData.value } as Pill
var updateItem = pills.value[index];
var result = await UpdatePill(updateItem)
if (result) {
ElMessage.success('更新成功')
closeDialog()
await fetchPills()
}
else {
ElMessage.error('更新失败')
}
}
} else {
const newOne: Pill = {
id: 1,
name: formData.value.name,
grade: formData.value.grade,
type: formData.value.type,
rarity: formData.value.rarity,
money: formData.value.money,
description: formData.value.description,
requirdLevelId: formData.value.requirdLevelId,
effectValue: formData.value.effectValue,
duration: formData.value.duration,
}
var result = await AddPill(newOne)
if (result) {
ElMessage.success('添加成功')
await fetchPills()
closeDialog()
}
else {
ElMessage.error("添加失败")
}
}
}
const deletePill = async (pill: Pill) => {
ElMessageBox.confirm(`确定删除丹药 "${pill.name}" 吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(async () => {
var id = pill.id
var result = await DeletePill(id)
if (result == true) {
ElMessage.success('丹药删除成功')
await fetchPills()
}
else {
ElMessage.error("删除丹药失败")
}
}).catch(() => {
})
}
onMounted(async () => {
await fetchPills()
await fetchPillGrades()
await fetchPillTypes()
await fetchPillRarities()
await fetchLevels()
})
const fetchPills = async () => {
pills.value = await GetPillList()
}
const fetchPillGrades = async () => {
grades.value = await getPillGrades()
}
const fetchPillTypes = async () => {
types.value = await getPillTypes()
}
const fetchPillRarities = async () => {
rarities.value = await getPillRarities()
}
const fetchLevels = async () => {
levels.value = await GetLevelList()
}
</script>
<template>
<div class="pills-container">
<div class="header">
<h2>丹药管理</h2>
</div>
<div class="filters">
<el-input v-model="searchQuery" type="text" placeholder="搜索丹药名..." style="max-width: 300px;" />
<el-button type="primary" @click="openDialog(undefined)">
<el-icon>
<Plus></Plus>
</el-icon>
</el-button>
</div>
<el-table :data="filteredPills" style="width: 100%;" stripe>
<el-table-column label="名称" prop="name" />
<el-table-column label="等级">
<template #default="scoped">
<span>
{{ translateGrade(scoped.row.grade) }}
</span>
</template>
</el-table-column>
<el-table-column label="类型">
<template #default="scoped">
<span>
{{ translateType(scoped.row.type) }}
</span>
</template>
</el-table-column>
<el-table-column label="稀有度">
<template #default="scoped">
<span>
{{ translateRarity(scoped.row.rarity) }}
</span>
</template>
</el-table-column>
<el-table-column label="价格" prop="money" />
<el-table-column label="描述" prop="description" show-overflow-tooltip />
<el-table-column label="所需等级">
<template #default="scoped">
<span>
{{ translateLevel(scoped.row.requirdLevelId) }}
</span>
</template>
</el-table-column>
<el-table-column label="提升效果" prop="effectValue"/>
<el-table-column label="持续时间" prop="duration" />
<el-table-column label="编辑" fixed="right">
<template #default="scoped">
<el-button type="primary" @click="openDialog(scoped.row)" :icon="Edit" circle />
<el-button type="danger" @click="deletePill(scoped.row)" :icon="Delete" circle />
</template>
</el-table-column>
</el-table>
<!-- Dialog -->
<div v-if="showDialog" class="dialog-overlay">
<el-form :inline="true" :model="formData" class="dialog" label-position="top">
<div class="dialog-header">
<h3>{{ isEditing ? '编辑丹药' : '添加丹药' }}</h3>
<el-button @click="closeDialog" :icon="Close" circle />
</div>
<el-form-item label="丹药名称">
<el-input v-model="formData.name" clearable></el-input>
</el-form-item>
<el-form-item label="等级">
<el-select v-model="formData.grade">
<el-option v-for="(value, index) in grades" :key="index" :value="value.id" :label="value.description">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="类型">
<el-select v-model="formData.type">
<el-option v-for="(value, index) in types" :key="index" :value="value.id" :label="value.description">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="稀有度">
<el-select v-model="formData.rarity">
<el-option v-for="(value, index) in rarities" :key="index" :value="value.id" :label="value.description">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="需求等级">
<el-select v-model="formData.requirdLevelId">
<el-option v-for="(value, index) in levels" :key="index" :value="value.id" :label="value.name">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="价格">
<el-input v-model="formData.money" clearable></el-input>
</el-form-item>
<el-form-item label="影响效果">
<el-input v-model="formData.effectValue" placeholder="(比如增加1000血、提升百分之20的修炼效率)" clearable></el-input>
</el-form-item>
<el-form-item label="持续时间(s)">
<el-input v-model="formData.duration" clearable placeholder="如果是0就表示是永久"></el-input>
</el-form-item>
<el-form-item label="描述" style="width: 100%;">
<el-input v-model="formData.description" type="textarea" clearable></el-input>
</el-form-item>
<div class="dialog-footer">
<el-button type="info" @click="closeDialog">取消</el-button>
<el-button type="primary" @click="savePill">保存</el-button>
</div>
</el-form>
</div>
</div>
</template>
<style scoped lang="css">
.pills-container {
background: linear-gradient(135deg, #1f2937 0%, #111827 100%);
padding: 20px;
border-radius: 8px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.header h2 {
margin: 0;
color: #e5e7eb;
font-size: 20px;
}
.filters {
display: flex;
gap: 12px;
margin-bottom: 20px;
}
.search-input,
.status-filter {
padding: 10px 12px;
background: #374151;
border: 1px solid #4b5563;
border-radius: 4px;
color: #e5e7eb;
font-size: 14px;
}
.search-input {
flex: 1;
max-width: 400px;
}
.search-input::placeholder {
color: #9ca3af;
}
.search-input:focus,
.status-filter:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.table {
width: 100%;
border-collapse: collapse;
overflow-x: auto;
}
.table thead tr {
border-bottom: 2px solid #374151;
}
.table th {
text-align: left;
padding: 12px;
color: #9ca3af;
font-weight: 600;
font-size: 13px;
}
.table td {
padding: 12px;
color: #e5e7eb;
font-size: 14px;
border-bottom: 1px solid #374151;
}
.table tbody tr:hover {
background-color: rgba(102, 126, 234, 0.05);
}
.status {
display: inline-block;
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
}
.status-completed {
background-color: rgba(34, 197, 94, 0.2);
color: #22c55e;
}
.status-processing {
background-color: rgba(59, 130, 246, 0.2);
color: #3b82f6;
}
.status-cancelled {
background-color: rgba(239, 68, 68, 0.2);
color: #ef4444;
}
.actions {
display: flex;
gap: 8px;
}
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.dialog {
background: #1f2937;
border: 1px solid #374151;
border-radius: 8px;
width: 100%;
max-width: 690px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
padding: 10px;
}
.dialog .el-input {
--el-input-width: 300px;
}
.dialog .el-select {
--el-select-width: 300px;
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #374151;
width: 100%;
margin-bottom: 10px;
padding: 10px 10px 10px 0px;
}
.dialog-header h3 {
margin: 0;
color: #e5e7eb;
font-size: 16px;
}
.close-btn {
background: none;
border: none;
color: #9ca3af;
font-size: 20px;
cursor: pointer;
padding: 0;
}
.close-btn:hover {
color: #e5e7eb;
}
.dialog-content {
padding: 20px;
}
.form-group {
margin-bottom: 16px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #e5e7eb;
font-size: 14px;
font-weight: 500;
}
.input {
width: 100%;
padding: 10px;
background: #374151;
border: 1px solid #4b5563;
border-radius: 4px;
color: #e5e7eb;
font-size: 14px;
}
.input:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
border-top: 1px solid #374151;
width: 100%;
padding: 10px 10px 0 0;
}
</style>

296
Build_God_Admin_Frontend/Frontend/src/views/admin/SettingsView.vue

@ -0,0 +1,296 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
const settings = ref({
systemName: '后台管理系统',
maxLoginAttempts: 5,
sessionTimeout: 30,
enableTwoFactor: false,
enableAuditLog: true,
backupFrequency: 'daily',
maxUploadSize: 100,
allowedFileTypes: ['jpg', 'png', 'pdf', 'doc', 'docx']
})
const newFileType = ref('')
const saveSettings = () => {
ElMessage.success('设置保存成功')
}
const addFileType = () => {
if (newFileType.value && !settings.value.allowedFileTypes.includes(newFileType.value)) {
settings.value.allowedFileTypes.push(newFileType.value)
newFileType.value = ''
ElMessage.success('文件类型已添加')
}
}
const removeFileType = (index: number) => {
settings.value.allowedFileTypes.splice(index, 1)
}
const resetSettings = () => {
ElMessage.warning('将重置所有设置为默认值')
}
</script>
<template>
<div class="settings-container">
<div class="header">
<h2>系统设置</h2>
</div>
<div class="settings-content">
<!-- 基本设置 -->
<section class="settings-section">
<h3>基本设置</h3>
<div class="settings-grid">
<div class="setting-item">
<label>系统名称</label>
<input v-model="settings.systemName" type="text" class="input" />
</div>
<div class="setting-item">
<label>会话超时时间分钟</label>
<input v-model.number="settings.sessionTimeout" type="number" class="input" />
</div>
</div>
</section>
<!-- 安全设置 -->
<section class="settings-section">
<h3>安全设置</h3>
<div class="settings-grid">
<div class="setting-item">
<label>最大登录失败次数</label>
<input v-model.number="settings.maxLoginAttempts" type="number" class="input" />
</div>
<div class="setting-item checkbox-item">
<label class="checkbox-label">
<input v-model="settings.enableTwoFactor" type="checkbox" />
<span>启用二次验证</span>
</label>
</div>
<div class="setting-item checkbox-item">
<label class="checkbox-label">
<input v-model="settings.enableAuditLog" type="checkbox" />
<span>启用审计日志</span>
</label>
</div>
</div>
</section>
<!-- 备份设置 -->
<section class="settings-section">
<h3>备份设置</h3>
<div class="settings-grid">
<div class="setting-item">
<label>备份频率</label>
<select v-model="settings.backupFrequency" class="input">
<option value="hourly">每小时</option>
<option value="daily">每天</option>
<option value="weekly">每周</option>
<option value="monthly">每月</option>
</select>
</div>
</div>
</section>
<!-- 文件上传设置 -->
<section class="settings-section">
<h3>文件上传设置</h3>
<div class="settings-grid">
<div class="setting-item">
<label>最大上传文件大小MB</label>
<input v-model.number="settings.maxUploadSize" type="number" class="input" />
</div>
</div>
<div class="file-types">
<label>允许的文件类型</label>
<div class="file-type-list">
<span v-for="(type, index) in settings.allowedFileTypes" :key="index" class="file-type-tag">
{{ type }}
<button @click="removeFileType(index)" class="remove-btn"></button>
</span>
</div>
<div class="add-file-type">
<input
v-model="newFileType"
type="text"
placeholder="输入文件类型 (如: zip, rar)"
class="input"
@keyup.enter="addFileType"
/>
<button @click="addFileType" class="btn btn-sm btn-add">添加</button>
</div>
</div>
</section>
<!-- 操作按钮 -->
<div class="settings-actions">
<button @click="saveSettings" class="btn btn-primary">💾 保存设置</button>
<button @click="resetSettings" class="btn btn-default"> 重置为默认值</button>
</div>
</div>
</div>
</template>
<style scoped lang="css">
.settings-container {
background: linear-gradient(135deg, #1f2937 0%, #111827 100%);
padding: 20px;
border-radius: 8px;
max-width: 1000px;
}
.header {
margin-bottom: 30px;
}
.header h2 {
margin: 0;
color: #e5e7eb;
font-size: 20px;
}
.settings-content {
display: flex;
flex-direction: column;
gap: 30px;
}
.settings-section {
background: rgba(31, 41, 55, 0.5);
padding: 20px;
border: 1px solid #374151;
border-radius: 8px;
}
.settings-section h3 {
margin: 0 0 20px 0;
color: #e5e7eb;
font-size: 16px;
font-weight: 600;
padding-bottom: 10px;
border-bottom: 1px solid #374151;
}
.settings-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.setting-item {
display: flex;
flex-direction: column;
}
.setting-item label {
margin-bottom: 8px;
color: #9ca3af;
font-size: 13px;
font-weight: 500;
}
.input {
padding: 10px;
background: #374151;
border: 1px solid #4b5563;
border-radius: 4px;
color: #e5e7eb;
font-size: 14px;
}
.input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.checkbox-item label {
margin-bottom: 0;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
color: #e5e7eb;
font-weight: normal;
}
.checkbox-label input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
accent-color: #667eea;
}
.file-types {
margin-top: 20px;
}
.file-types > label {
display: block;
margin-bottom: 12px;
color: #9ca3af;
font-size: 13px;
font-weight: 500;
}
.file-type-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 12px;
}
.file-type-tag {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
background: rgba(102, 126, 234, 0.2);
color: #667eea;
border-radius: 4px;
font-size: 13px;
font-weight: 500;
}
.remove-btn {
background: none;
border: none;
color: #667eea;
cursor: pointer;
padding: 0;
font-size: 14px;
}
.remove-btn:hover {
opacity: 0.8;
}
.add-file-type {
display: flex;
gap: 8px;
}
.add-file-type .input {
flex: 1;
}
.btn-add {
padding: 10px 20px;
}
.settings-actions {
display: flex;
gap: 12px;
margin-top: 20px;
}
</style>

375
Build_God_Admin_Frontend/Frontend/src/views/admin/SpiritsView.vue

@ -0,0 +1,375 @@
<script setup lang="ts">
import { ref, computed, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { AddProfession, DeleteProfession, GetProfessionList, UpdateProfession, type Profession } from '@/api/spirit'
import { Plus, Edit, Delete, Close } from '@element-plus/icons-vue'
const professions = reactive<Profession[]>([])
const showDialog = ref(false)
const isEditing = ref(false)
const searchQuery = ref('')
const formData = ref<Partial<Profession>>({
id: 0,
name: '',
description: '',
attackRate: 1.0,
defendRate: 1.0,
healthRate: 1.0,
criticalRate: 1.0
})
const filteredProfessions = computed(() => {
return professions.filter(profession =>
profession.name.toLowerCase().includes(searchQuery.value.toLowerCase())
)
})
const openDialog = (sp?: Profession) => {
if (sp!=undefined) {
isEditing.value = true
formData.value = { ...sp }
} else {
isEditing.value = false
formData.value = {
id: 0,
name: '',
description: '',
attackRate: 1.0,
defendRate: 1.0,
healthRate: 1.0,
criticalRate: 1.0
}
}
showDialog.value = true
}
const closeDialog = () => {
showDialog.value = false
}
const saveProfession = async () => {
if (!formData.value?.name || !formData.value?.description) {
ElMessage.error('请填写必填项')
return
}
if (isEditing.value) {
const index = professions.findIndex(u => u.id === formData.value.id)
if (index > -1) {
var bo = await UpdateProfession(formData.value as Profession)
if (bo) {
ElMessage.success('更新成功')
await fetchProfessions()
closeDialog()
}
else {
ElMessage.error('更新失败')
}
}
} else {
const newOne: Profession = {
id: 0,
name: formData.value.name || '',
description: formData.value.description || '',
attackRate: formData.value.attackRate || 1.0,
defendRate: formData.value.defendRate || 1.0,
healthRate: formData.value.healthRate || 1.0,
criticalRate: formData.value.criticalRate || 1.0
}
var bo = await AddProfession(newOne)
if (bo) {
ElMessage.success('添加成功')
await fetchProfessions()
closeDialog()
}
else {
ElMessage.error('添加失败')
}
}
}
const deleteProfession = async (sp: Profession) => {
ElMessageBox.confirm(`确定删除职业 "${sp.name}" 吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(async () => {
var id = sp.id
var bo = await DeleteProfession(id)
if (bo) {
ElMessage.success('删除成功')
await fetchProfessions()
}
else {
ElMessage.error('删除失败')
}
}).catch(() => { })
}
onMounted(async () => {
await fetchProfessions()
})
const fetchProfessions = async () => {
var arr = await GetProfessionList()
professions.splice(0, professions.length, ...arr)
}
</script>
<template>
<div class="professions-container">
<div class="header">
<h2>职业管理</h2>
</div>
<div class="search-bar">
<el-input v-model="searchQuery" type="text" placeholder="搜索职业名..." style="max-width: 300px;" />
<el-button type="primary" @click="openDialog(undefined)">
<el-icon class="el-icon--left"><Plus></Plus></el-icon>
</el-button>
</div>
<el-table :data="filteredProfessions" stripe style="width: 100%;">
<el-table-column label="名称" prop="name"/>
<el-table-column label="攻击系数" prop="attackRate"/>
<el-table-column label="防御系数" prop="defendRate"/>
<el-table-column label="生命系数" prop="healthRate"/>
<el-table-column label="暴击系数" prop="criticalRate"/>
<el-table-column label="描述" prop="description" show-overflow-tooltip/>
<el-table-column label="操作">
<template #default="scoped">
<el-button type="primary" @click="openDialog(scoped.row)" :icon="Edit" circle />
<el-button type="danger" @click="deleteProfession(scoped.row)" :icon="Delete" circle />
</template>
</el-table-column>
</el-table>
<!-- Dialog -->
<div v-if="showDialog" class="dialog-overlay">
<el-form class="dialog" :model="formData" label-position="top">
<div class="dialog-header">
<h3>{{ isEditing ? '编辑职业' : '添加职业' }}</h3>
<button class="close-btn" @click="closeDialog"></button>
</div>
<el-form-item label="职业名称">
<el-input v-model="formData.name" clearable></el-input>
</el-form-item>
<el-form-item label="攻击系数">
<el-input-number v-model="formData.attackRate" :step="0.1" :min="0" :max="10" />
</el-form-item>
<el-form-item label="防御系数">
<el-input-number v-model="formData.defendRate" :step="0.1" :min="0" :max="10" />
</el-form-item>
<el-form-item label="生命系数">
<el-input-number v-model="formData.healthRate" :step="0.1" :min="0" :max="10" />
</el-form-item>
<el-form-item label="暴击系数">
<el-input-number v-model="formData.criticalRate" :step="0.1" :min="0" :max="10" />
</el-form-item>
<el-form-item label="描述">
<el-input type="textarea" v-model="formData.description"></el-input>
</el-form-item>
<div class="dialog-footer">
<el-button type="info" @click="closeDialog">取消</el-button>
<el-button type="primary" @click="saveProfession">保存</el-button>
</div>
</el-form>
</div>
</div>
</template>
<style scoped lang="css">
.professions-container {
background: linear-gradient(135deg, #1f2937 0%, #111827 100%);
padding: 20px;
border-radius: 8px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.header h2 {
margin: 0;
color: #e5e7eb;
font-size: 20px;
}
.search-bar {
display: flex;
gap: 12px;
margin-bottom: 20px;
}
.search-input {
width: 100%;
max-width: 400px;
padding: 10px 12px;
background: #374151;
border: 1px solid #4b5563;
border-radius: 4px;
color: #e5e7eb;
font-size: 14px;
}
.search-input::placeholder {
color: #9ca3af;
}
.search-input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.table {
width: 100%;
border-collapse: collapse;
overflow-x: auto;
}
.table thead tr {
border-bottom: 2px solid #374151;
}
.table th {
text-align: left;
padding: 12px;
color: #9ca3af;
font-weight: 600;
font-size: 13px;
}
.table td {
padding: 12px;
color: #e5e7eb;
font-size: 14px;
border-bottom: 1px solid #374151;
}
.table tbody tr:hover {
background-color: rgba(102, 126, 234, 0.05);
}
.status {
display: inline-block;
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
}
.status-enabled {
background-color: rgba(34, 197, 94, 0.2);
color: #22c55e;
}
.status-disabled {
background-color: rgba(239, 68, 68, 0.2);
color: #ef4444;
}
.actions {
display: flex;
gap: 8px;
}
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.dialog {
background: #1f2937;
border: 1px solid #374151;
border-radius: 8px;
width: 100%;
max-width: 490px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
padding: 10px;
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #374151;
width: 100%;
margin-bottom: 10px;
padding: 10px 10px 10px 0px;
}
.dialog-header h3 {
margin: 0;
color: #e5e7eb;
font-size: 16px;
}
.close-btn {
background: none;
border: none;
color: #9ca3af;
font-size: 20px;
cursor: pointer;
padding: 0;
}
.close-btn:hover {
color: #e5e7eb;
}
.dialog-content {
padding: 20px;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #e5e7eb;
font-size: 14px;
font-weight: 500;
}
.input {
width: 100%;
padding: 10px;
background: #374151;
border: 1px solid #4b5563;
border-radius: 4px;
color: #e5e7eb;
font-size: 14px;
}
.input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
border-top: 1px solid #374151;
width: 100%;
padding: 10px 10px 0 0;
}
</style>

426
Build_God_Admin_Frontend/Frontend/src/views/admin/UsersView.vue

@ -0,0 +1,426 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
interface User {
id: number
username: string
email: string
role: 'admin' | 'user'
status: '启用' | '禁用'
createdAt: string | undefined
}
const users = ref<User[]>([
{ id: 1, username: 'admin', email: 'admin@example.com', role: 'admin', status: '启用', createdAt: '2024-01-15' },
{ id: 2, username: 'user1', email: 'user1@example.com', role: 'user', status: '启用', createdAt: '2024-01-16' },
{ id: 3, username: 'user2', email: 'user2@example.com', role: 'user', status: '禁用', createdAt: '2024-01-17' },
{ id: 4, username: 'user3', email: 'user3@example.com', role: 'user', status: '启用', createdAt: '2024-01-18' },
])
const showDialog = ref(false)
const isEditing = ref(false)
const searchQuery = ref('')
const formData = ref<Partial<User>>({
username: '',
email: '',
role: 'user',
status: '启用'
})
const filteredUsers = computed(() => {
return users.value.filter(user =>
user.username.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
user.email.toLowerCase().includes(searchQuery.value.toLowerCase())
)
})
const openDialog = (user?: User) => {
if (user) {
isEditing.value = true
formData.value = { ...user }
} else {
isEditing.value = false
formData.value = {
username: '',
email: '',
role: 'user',
status: '启用'
}
}
showDialog.value = true
}
const closeDialog = () => {
showDialog.value = false
}
const saveUser = () => {
if (!formData.value.username || !formData.value.email) {
ElMessage.error('请填写必填项')
return
}
if (isEditing.value) {
const index = users.value.findIndex(u => u.id === formData.value.id)
if (index > -1) {
users.value[index] = { ...users.value[index], ...formData.value } as User
ElMessage.success('用户更新成功')
}
} else {
const newUser: User = {
id: Math.max(...users.value.map(u => u.id), 0) + 1,
username: formData.value.username!,
email: formData.value.email!,
role: formData.value.role || 'user',
status: formData.value.status || '启用',
createdAt: new Date().toISOString().split('T')[0]
}
users.value.push(newUser)
ElMessage.success('用户添加成功')
}
closeDialog()
}
const deleteUser = (user: User) => {
ElMessageBox.confirm(`确定删除用户 "${user.username}" 吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
users.value = users.value.filter(u => u.id !== user.id)
ElMessage.success('用户删除成功')
}).catch(() => {})
}
const getStatusClass = (status: string) => {
return status === '启用' ? 'status-enabled' : 'status-disabled'
}
</script>
<template>
<div class="users-container">
<div class="header">
<h2>用户管理</h2>
</div>
<div class="search-bar">
<input
v-model="searchQuery"
type="text"
placeholder="搜索用户名或邮箱..."
class="search-input"
/>
<button class="btn btn-primary" @click="openDialog()">
添加用户
</button>
</div>
<table class="table">
<thead>
<tr>
<th>用户名</th>
<th>邮箱</th>
<th>角色</th>
<th>状态</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="user in filteredUsers" :key="user.id">
<td>{{ user.username }}</td>
<td>{{ user.email }}</td>
<td>{{ user.role === 'admin' ? '管理员' : '普通用户' }}</td>
<td>
<span class="status" :class="getStatusClass(user.status)">
{{ user.status }}
</span>
</td>
<td>{{ user.createdAt }}</td>
<td class="actions">
<button class="btn btn-sm btn-edit" @click="openDialog(user)">编辑</button>
<button class="btn btn-sm btn-delete" @click="deleteUser(user)">删除</button>
</td>
</tr>
</tbody>
</table>
<!-- Dialog -->
<div v-if="showDialog" class="dialog-overlay" @click.self="closeDialog">
<div class="dialog">
<div class="dialog-header">
<h3>{{ isEditing ? '编辑用户' : '添加用户' }}</h3>
<button class="close-btn" @click="closeDialog"></button>
</div>
<div class="dialog-content">
<div class="form-group">
<label>用户名 *</label>
<input v-model="formData.username" type="text" class="input" />
</div>
<div class="form-group">
<label>邮箱 *</label>
<input v-model="formData.email" type="email" class="input" />
</div>
<div class="form-group">
<label>角色</label>
<select v-model="formData.role" class="input">
<option value="user">普通用户</option>
<option value="admin">管理员</option>
</select>
</div>
<div class="form-group">
<label>状态</label>
<select v-model="formData.status" class="input">
<option value="启用">启用</option>
<option value="禁用">禁用</option>
</select>
</div>
</div>
<div class="dialog-footer">
<button class="btn btn-default" @click="closeDialog">取消</button>
<button class="btn btn-primary" @click="saveUser">保存</button>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="css">
.users-container {
background: linear-gradient(135deg, #1f2937 0%, #111827 100%);
padding: 20px;
border-radius: 8px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.header h2 {
margin: 0;
color: #e5e7eb;
font-size: 20px;
}
.search-bar {
display: flex;
gap: 12px;
margin-bottom: 20px;
}
.search-input {
width: 100%;
max-width: 400px;
padding: 10px 12px;
background: #374151;
border: 1px solid #4b5563;
border-radius: 4px;
color: #e5e7eb;
font-size: 14px;
}
.search-input::placeholder {
color: #9ca3af;
}
.search-input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.table {
width: 100%;
border-collapse: collapse;
overflow-x: auto;
}
.table thead tr {
border-bottom: 2px solid #374151;
}
.table th {
text-align: left;
padding: 12px;
color: #9ca3af;
font-weight: 600;
font-size: 13px;
}
.table td {
padding: 12px;
color: #e5e7eb;
font-size: 14px;
border-bottom: 1px solid #374151;
}
.table tbody tr:hover {
background-color: rgba(102, 126, 234, 0.05);
}
.status {
display: inline-block;
padding: 4px 12px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
}
.status-enabled {
background-color: rgba(34, 197, 94, 0.2);
color: #22c55e;
}
.status-disabled {
background-color: rgba(239, 68, 68, 0.2);
color: #ef4444;
}
.actions {
display: flex;
gap: 8px;
}
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.dialog {
background: #1f2937;
border: 1px solid #374151;
border-radius: 8px;
width: 100%;
max-width: 500px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #374151;
}
.dialog-header h3 {
margin: 0;
color: #e5e7eb;
font-size: 16px;
}
.close-btn {
background: none;
border: none;
color: #9ca3af;
font-size: 20px;
cursor: pointer;
padding: 0;
}
.close-btn:hover {
color: #e5e7eb;
}
.dialog-content {
padding: 20px;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #e5e7eb;
font-size: 14px;
font-weight: 500;
}
.input {
width: 100%;
padding: 10px;
background: #374151;
border: 1px solid #4b5563;
border-radius: 4px;
color: #e5e7eb;
font-size: 14px;
}
.input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 20px;
border-top: 1px solid #374151;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
}
.btn-sm {
padding: 4px 10px;
font-size: 12px;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5a67d8;
}
.btn-default {
background: #374151;
color: #e5e7eb;
}
.btn-default:hover {
background: #4b5563;
}
.btn-edit {
background: rgba(245, 158, 11, 0.2);
color: #f59e0b;
}
.btn-edit:hover {
background: rgba(245, 158, 11, 0.3);
}
.btn-delete {
background: rgba(239, 68, 68, 0.2);
color: #ef4444;
}
.btn-delete:hover {
background: rgba(239, 68, 68, 0.3);
}
</style>

12
Build_God_Admin_Frontend/Frontend/tsconfig.app.json

@ -0,0 +1,12 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"paths": {
"@/*": ["./src/*"]
}
}
}

11
Build_God_Admin_Frontend/Frontend/tsconfig.json

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

19
Build_God_Admin_Frontend/Frontend/tsconfig.node.json

@ -0,0 +1,19 @@
{
"extends": "@tsconfig/node24/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*",
"eslint.config.*"
],
"compilerOptions": {
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}

18
Build_God_Admin_Frontend/Frontend/vite.config.ts

@ -0,0 +1,18 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
})

30
Build_God_Api/.dockerignore

@ -0,0 +1,30 @@
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
!**/.gitignore
!.git/HEAD
!.git/config
!.git/packed-refs
!.git/refs/heads/**

52
Build_God_Api/.gitignore

@ -0,0 +1,52 @@
# ===================== 核心:编译/构建产物 =====================
bin/
obj/
*.dll
*.pdb
publish/
*.exe
*.manifest
*.resources
*.cache
# ===================== IDE/编辑器配置 =====================
# Visual Studio
.vs/
*.suo
*.user
*.sln.docstates
*.vcxproj.user
*.vcxproj.filters
# Rider
.idea/
*.iml
*.sln.iml
# VS Code
.vscode/
# ===================== 运行时/依赖缓存 =====================
project.assets.json
packages/
.nuget/
*.nuspec
dotnet-*.json
# ===================== 环境配置/敏感信息 =====================
appsettings.Development.json
appsettings.Staging.json
appsettings.Production.json
launchSettings.json
.user-secrets/
# ===================== 临时文件/日志 =====================
*.tmp
*.temp
*.bak
*.swp
~*
*.log
logs/
temp/
tmp/

25
Build_God_Api/Build_God_Api.sln

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.14.36811.4 d17.14
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Build_God_Api", "Build_God_Api\Build_God_Api.csproj", "{A69A7FB4-26C9-4BAC-8A1C-4912FD062DF4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A69A7FB4-26C9-4BAC-8A1C-4912FD062DF4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A69A7FB4-26C9-4BAC-8A1C-4912FD062DF4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A69A7FB4-26C9-4BAC-8A1C-4912FD062DF4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A69A7FB4-26C9-4BAC-8A1C-4912FD062DF4}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C243A1B1-A5E8-40A6-B46C-35138EAC6C53}
EndGlobalSection
EndGlobal

25
Build_God_Api/Build_God_Api/Build_God_Api.csproj

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>34d14f51-4f4c-4f8e-81d7-17b45add3813</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Controllers\新文件夹\**" />
<Content Remove="Controllers\新文件夹\**" />
<EmbeddedResource Remove="Controllers\新文件夹\**" />
<None Remove="Controllers\新文件夹\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.14" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
<PackageReference Include="SqlSugarCore" Version="5.1.4.211" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
</ItemGroup>
</Project>

6
Build_God_Api/Build_God_Api/Build_God_Api.http

@ -0,0 +1,6 @@
@Build_God_Api_HostAddress = http://localhost:5091
GET {{Build_God_Api_HostAddress}}/weatherforecast/
Accept: application/json
###

41
Build_God_Api/Build_God_Api/Common/EnumHelper.cs

@ -0,0 +1,41 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Reflection;
namespace Build_God_Api.Common
{
public class EnumHelper
{
public static List<EnumInfoDto> GetEnumList<T>() where T : Enum
{
var result = new List<EnumInfoDto>();
foreach (var value in Enum.GetValues(typeof(T)).Cast<T>())
{
var fieldInfo = typeof(T).GetField(value.ToString());
var description = fieldInfo?.GetCustomAttribute<DescriptionAttribute>()?.Description;
var displayName = fieldInfo?.GetCustomAttribute<DisplayAttribute>()?.Name;
result.Add(new EnumInfoDto
{
Id = Convert.ToInt32(value),
Name = value.ToString(),
DisplayName = displayName ?? value.ToString(),
Description = description ?? value.ToString()
});
}
return result;
}
}
// 枚举信息DTO
public class EnumInfoDto
{
public int Id { get; set; }
public string? Name { get; set; }
public string? DisplayName { get; set; }
public string? Description { get; set; }
}
}

94
Build_God_Api/Build_God_Api/Controllers/AccountController.cs

@ -0,0 +1,94 @@
using Build_God_Api.DB;
using Build_God_Api.Services;
using Microsoft.AspNetCore.Mvc;
using System.ComponentModel.DataAnnotations;
namespace Build_God_Api.Controllers
{
/// <summary>
/// 本地测试账号 Tom 123456 email:976802198@qq.com
/// </summary>
/// <param name="service"></param>
[ApiController]
[Route("api/god/[controller]")]
public class AccountController(IAccountService service) : ControllerBase
{
private readonly IAccountService _service = service;
[HttpPost("register")]
public async Task<ActionResult<int>> Register(AccountPostCmd cmd)
{
var exist = await _service.ExistsAccount(cmd.Name, cmd.Email);
if (exist) return BadRequest("账号已经存在");
cmd.Password = _service.CalculateHash(cmd.Name, cmd.Password);
var account = new Account
{
Name = cmd.Name,
Email = cmd.Email,
Password = cmd.Password,
Active = true,
IsAdmin = false
};
var bo = await _service.Register(account);
if (!bo)
{
return BadRequest("注册账号失败");
}
return Ok(account.Id);
}
[HttpPost("login")]
public async Task<ActionResult<LoginResult>> Login(LoginCmd cmd)
{
var account = await _service.GetAccountByName(cmd.Name);
if (account == null) return BadRequest("账号或者密码错误");
if (account.Password != _service.CalculateHash(cmd.Name, cmd.Password))
return BadRequest("账号或者密码错误");
var token = _service.GenerateToken(account.Id, account.Name, false);
return new LoginResult { Token = token };
}
[HttpPost("login/admin")]
public ActionResult<LoginResult> AdminLogin(LoginCmd cmd)
{
if (cmd.Name != "admin" && cmd.Password != "love_god.123")
{
return BadRequest("只有管理员账号才能使用此接口登录");
}
var token = _service.GenerateToken(0, "admin", true);
return new LoginResult { Token = token };
}
}
public class AccountPostCmd
{
[StringLength(30, MinimumLength = 3)]
[RegularExpression(@"^[a-zA-Z0-9_]+$")]
public string Name { get; set; } = string.Empty;
[EmailAddress]
[StringLength(50)]
public string Email { get; set; } = string.Empty;
[StringLength(30, MinimumLength = 4)]
public string Password { get; set; } = string.Empty;
}
public class LoginCmd
{
[StringLength(30, MinimumLength = 3)]
[RegularExpression(@"^[a-zA-Z0-9_]+$")]
public string Name { get; set; } = string.Empty;
[StringLength(30, MinimumLength = 4)]
public string Password { get; set; } = string.Empty;
}
public class LoginResult
{
public string Token { get; set; } = string.Empty;
}
}

121
Build_God_Api/Build_God_Api/Controllers/BagController.cs

@ -0,0 +1,121 @@
using Build_God_Api.Common;
using Build_God_Api.DB;
using Build_God_Api.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Build_God_Api.Controllers
{
[ApiController]
[Route("api/god/[controller]")]
public class BagController(IBagService service) : ControllerBase
{
private readonly IBagService service = service;
// ============ Bag配置管理 ============
[HttpGet("all")]
[Authorize]
public async Task<ActionResult<List<Bag>>> GetAllBags()
{
return await service.GetAllBags();
}
[HttpGet("{id}")]
[Authorize]
public async Task<ActionResult<Bag>> GetBagById(int id)
{
return await service.GetBagById(id);
}
[HttpPost]
[Authorize(Roles = "admin")]
public async Task<ActionResult<bool>> CreateBag([FromBody] Bag bag)
{
return await service.CreateBag(bag);
}
[HttpPut("{id}")]
[Authorize(Roles = "admin")]
public async Task<ActionResult<bool>> UpdateBag(int id, [FromBody] Bag bag)
{
bag.Id = id;
return await service.UpdateBag(bag);
}
[HttpDelete("{id}")]
[Authorize(Roles = "admin")]
public async Task<ActionResult<bool>> DeleteBag(int id)
{
return await service.DeleteBag(id);
}
// ============ 角色背包管理 ============
[HttpGet("character/{characterId}")]
[Authorize]
public async Task<ActionResult<CharacterBagDto>> GetCharacterBag(int characterId)
{
return await service.GetCharacterBag(characterId);
}
[HttpPost("assign")]
[Authorize(Roles = "admin")]
public async Task<ActionResult<bool>> AssignBagToCharacter([FromBody] AssignBagDto dto)
{
return await service.AssignBagToCharacter(dto.CharacterId, dto.BagId);
}
// ============ 背包物品管理 ============
[HttpGet("{characterBagId}/items")]
[Authorize]
public async Task<ActionResult<List<BagItemDto>>> GetBagItems(int characterBagId)
{
return await service.GetBagItems(characterBagId);
}
[HttpPost("{characterBagId}/items")]
[Authorize(Roles = "admin")]
public async Task<ActionResult<bool>> AddItemToBag(int characterBagId, [FromBody] AddBagItemDto dto)
{
return await service.AddItemToBag(characterBagId, dto.ItemType, dto.ItemId, dto.Quantity);
}
[HttpDelete("{characterBagId}/items/{itemId}")]
[Authorize(Roles = "admin")]
public async Task<ActionResult<bool>> RemoveItemFromBag(int characterBagId, int itemId)
{
return await service.RemoveItemFromBag(characterBagId, itemId);
}
// ============ 枚举 ============
[HttpGet("rarities")]
[Authorize]
public ActionResult<List<EnumInfoDto>> GetBagRarities()
{
return EnumHelper.GetEnumList<BagRarity>();
}
[HttpGet("item-types")]
[Authorize]
public ActionResult<List<EnumInfoDto>> GetBagItemTypes()
{
return EnumHelper.GetEnumList<BagItemType>();
}
}
public class AssignBagDto
{
public int CharacterId { get; set; }
public int BagId { get; set; }
}
public class AddBagItemDto
{
public int ItemType { get; set; }
public int ItemId { get; set; }
public int Quantity { get; set; }
}
}

100
Build_God_Api/Build_God_Api/Controllers/CharacterController.cs

@ -0,0 +1,100 @@
using Build_God_Api.DB;
using Build_God_Api.Dto;
using Build_God_Api.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Build_God_Api.Controllers
{
[ApiController]
[Route("api/god/[controller]")]
public class CharacterController(ICharacterService characterService, ICurrentUserService currentUserService) : ControllerBase
{
private readonly ICharacterService characterService = characterService;
private readonly ICurrentUserService currentUserService = currentUserService;
/// <summary>
/// 获取当前账号的角色列表(包含境界和灵根信息)
/// </summary>
[HttpGet("list")]
[Authorize]
public async Task<ActionResult<List<CharacterDto>>> GetCharacterList()
{
var result = await characterService.GetCharacterListWithDetails(currentUserService.UserId);
return result;
}
[HttpGet("accountId/{accountId}")]
[Authorize]
public async Task<ActionResult<Character?>> GetCharacterByAccountId(int accountId)
{
var result = await characterService.GetCharacterByAccountId(accountId);
return result;
}
[HttpGet("all")]
[Authorize(Roles = "admin")]
public async Task<ActionResult<List<Character>>> GetAllCharacters()
{
return await characterService.GetAllCharacters();
}
[HttpGet("{characterId}")]
[Authorize(Roles = "admin")]
public async Task<ActionResult<Character?>> GetCharacterById(int characterId)
{
return await characterService.GetCharacterById(characterId);
}
[HttpPost("register")]
[Authorize]
public async Task<ActionResult<bool>> RegisterCharacter([FromBody] CharacterRegisterDto dto)
{
return await characterService.RegisterCharacter(dto);
}
/// <summary>
/// 选择角色(更新最后登录时间)
/// </summary>
[HttpPost("select/{characterId}")]
[Authorize]
public async Task<ActionResult<bool>> SelectCharacter(int characterId)
{
return await characterService.SelectCharacter(characterId);
}
[HttpDelete("{characterId}")]
[Authorize]
public async Task<ActionResult<bool>> DeleteCharacter(int characterId)
{
return await characterService.DeleteCharacter(characterId);
}
[HttpPost("{characterId}/breakthrough")]
[Authorize]
public async Task<ActionResult<bool>> Breakthrough(int characterId)
{
return await characterService.Breakthrough(characterId);
}
/// <summary>
/// 开始打坐
/// </summary>
[HttpPost("{characterId}/training/start")]
[Authorize]
public async Task<ActionResult<bool>> StartTraining(int characterId)
{
return await characterService.StartTraining(characterId);
}
/// <summary>
/// 停止打坐并结算经验
/// </summary>
[HttpPost("{characterId}/training/stop")]
[Authorize]
public async Task<ActionResult<decimal>> StopTraining(int characterId)
{
return await characterService.StopTraining(characterId);
}
}
}

75
Build_God_Api/Build_God_Api/Controllers/EquipmentController.cs

@ -0,0 +1,75 @@
using Build_God_Api.Common;
using Build_God_Api.DB;
using Build_God_Api.Dto;
using Build_God_Api.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Build_God_Api.Controllers
{
[ApiController]
[Route("api/god/[controller]")]
public class EquipmentController(IEquipmentService service) : ControllerBase
{
private readonly IEquipmentService service = service;
[HttpPost]
[Authorize(Roles = "admin")]
public async Task<ActionResult<bool>> Add([FromBody] EquipmentTemplate item)
{
return await service.Add(item);
}
[HttpPost("list")]
[Authorize(Roles = "admin")]
public async Task<ActionResult<bool>> Add([FromBody] List<EquipmentTemplate> items)
{
return await service.Add(items);
}
[HttpDelete("{id}")]
[Authorize(Roles = "admin")]
public async Task<ActionResult<bool>> Delete(int id)
{
return await service.Delete(id);
}
[HttpPut]
[Authorize(Roles = "admin")]
public async Task<ActionResult<bool>> Update([FromBody] EquipmentTemplate item)
{
return await service.Update(item);
}
[HttpPost("all")]
[Authorize]
public async Task<ActionResult<PagedResult<EquipmentTemplate>>> GetAll([FromBody] SearchEquipmentDto dto)
{
return await service.GetAll(dto);
}
[HttpGet("types")]
[Authorize]
public ActionResult<List<EnumInfoDto>> GetEquipmentTypes()
{
var result = EnumHelper.GetEnumList<EquipmentType>();
return result;
}
[HttpGet("rarities")]
[Authorize]
public ActionResult<List<EnumInfoDto>> GetEquipmentRarity()
{
var result = EnumHelper.GetEnumList<Rarity>();
return result;
}
[HttpGet("attribute-types")]
[Authorize]
public ActionResult<List<EnumInfoDto>> GetEquipmentAttributeTypes()
{
var result = EnumHelper.GetEnumList<EquipmentAttributeType>();
return result;
}
}
}

24
Build_God_Api/Build_God_Api/Controllers/Game/TrainingController.cs

@ -0,0 +1,24 @@
using Build_God_Api.Services.Game;
using Microsoft.AspNetCore.Mvc;
namespace Build_God_Api.Controllers.Game
{
[ApiController]
[Route("api/god/[controller]")]
public class TrainingController(ITrainingService trainingService) : ControllerBase
{
private readonly ITrainingService trainingService = trainingService;
[HttpPost("start/{characterId}")]
public async Task<ActionResult<bool>> StartTraining(int characterId)
{
return await trainingService.StartTraining(characterId);
}
[HttpPost("end/{characterId}")]
public async Task<ActionResult<bool>> EndTraining(int characterId)
{
return await trainingService.EndTraining(characterId);
}
}
}

42
Build_God_Api/Build_God_Api/Controllers/LevelController.cs

@ -0,0 +1,42 @@
using Build_God_Api.DB;
using Build_God_Api.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Build_God_Api.Controllers
{
[ApiController]
[Route("api/god/[controller]")]
public class LevelController(ILevelService service) : ControllerBase
{
private readonly ILevelService service = service;
[HttpPost]
[Authorize(Roles = "admin")]
public async Task<ActionResult<bool>> Add([FromBody] Level item)
{
return await service.Add(item);
}
[HttpDelete("{id}")]
[Authorize(Roles = "admin")]
public async Task<ActionResult<bool>> Delete(int id)
{
return await service.Delete(id);
}
[HttpPut]
[Authorize(Roles = "admin")]
public async Task<ActionResult<bool>> Update([FromBody] Level item)
{
return await service.Update(item);
}
[HttpGet("all")]
[Authorize]
public async Task<ActionResult<List<Level>>> GetAll()
{
return await service.GetAll();
}
}
}

130
Build_God_Api/Build_God_Api/Controllers/MissionController.cs

@ -0,0 +1,130 @@
using Build_God_Api.Common;
using Build_God_Api.DB;
using Build_God_Api.Dto;
using Build_God_Api.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Build_God_Api.Controllers
{
[ApiController]
[Route("api/god/[controller]")]
public class MissionController(
IMissionService service,
IMissionProgressService progressService
) : ControllerBase
{
private readonly IMissionService service = service;
private readonly IMissionProgressService progressService = progressService;
// ============ Mission ============
[HttpPost]
[Authorize(Roles = "admin")]
public async Task<ActionResult<bool>> Add([FromBody] Mission item)
{
return await service.Add(item);
}
[HttpDelete("{id}")]
[Authorize(Roles = "admin")]
public async Task<ActionResult<bool>> Delete(int id)
{
return await service.Delete(id);
}
[HttpPut]
[Authorize(Roles = "admin")]
public async Task<ActionResult<bool>> Update([FromBody] Mission item)
{
return await service.Update(item);
}
[HttpPost("all")]
[Authorize]
public async Task<ActionResult<PagedResult<Mission>>> GetAll([FromBody] SearchMissionDto dto)
{
return await service.GetAll(dto);
}
[HttpPost("reward/add")]
[Authorize]
public async Task<ActionResult<bool>> AddReward([FromBody] MissionReward reward)
{
return await service.AddReward(reward);
}
[HttpPost("reward/update")]
[Authorize]
public async Task<ActionResult<bool>> UpdateReward([FromBody] MissionReward reward)
{
return await service.UpdateReward(reward);
}
// ============ Mission Progress ============
[HttpGet("{missionId}/progresses")]
[Authorize]
public async Task<ActionResult<List<MissionProgress>>> GetProgresses(int missionId)
{
return await progressService.GetByMissionId(missionId);
}
[HttpPost("{missionId}/progresses")]
[Authorize(Roles = "admin")]
public async Task<ActionResult<bool>> AddProgress(int missionId, [FromBody] MissionProgress progress)
{
progress.MissionId = missionId;
return await progressService.Create(progress);
}
[HttpPut("progresses/{id}")]
[Authorize(Roles = "admin")]
public async Task<ActionResult<bool>> UpdateProgress(int id, [FromBody] MissionProgress progress)
{
progress.Id = id;
return await progressService.Update(progress);
}
[HttpDelete("progresses/{id}")]
[Authorize(Roles = "admin")]
public async Task<ActionResult<bool>> DeleteProgress(int id)
{
return await progressService.Delete(id);
}
// ============ Enums ============
[HttpGet("types")]
[Authorize]
public ActionResult<List<EnumInfoDto>> GetMissionTypes()
{
var result = EnumHelper.GetEnumList<MissionType>();
return result;
}
[HttpGet("difficulties")]
[Authorize]
public ActionResult<List<EnumInfoDto>> GetMissionDifficultys()
{
var result = EnumHelper.GetEnumList<MissionDifficulty>();
return result;
}
[HttpGet("reward/types")]
[Authorize]
public ActionResult<List<EnumInfoDto>> GetMissionRewardTypes()
{
var result = EnumHelper.GetEnumList<RewardType>();
return result;
}
[HttpGet("progress/target-types")]
[Authorize]
public ActionResult<List<EnumInfoDto>> GetProgressTargetTypes()
{
var result = EnumHelper.GetEnumList<ProgressTargetType>();
return result;
}
}
}

59
Build_God_Api/Build_God_Api/Controllers/PillController.cs

@ -0,0 +1,59 @@
using Build_God_Api.Common;
using Build_God_Api.DB;
using Build_God_Api.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Build_God_Api.Controllers
{
[ApiController]
[Route("api/god/[controller]")]
public class PillController(IPillService service) : ControllerBase
{
private readonly IPillService service = service;
[HttpPost]
[Authorize(Roles = "admin")]
public async Task<ActionResult<bool>> Add([FromBody] Pill item)
{
return await service.Add(item);
}
[HttpDelete("{id}")]
[Authorize(Roles = "admin")]
public async Task<ActionResult<bool>> Delete(int id)
{
return await service.Delete(id);
}
[HttpPut]
[Authorize(Roles = "admin")]
public async Task<ActionResult<bool>> Update([FromBody] Pill item)
{
return await service.Update(item);
}
[HttpGet("all")]
[Authorize]
public async Task<ActionResult<List<Pill>>> GetAll()
{
return await service.GetAll();
}
[HttpGet("types")]
[Authorize]
public ActionResult<List<EnumInfoDto>> GetTypes()
{
var result = EnumHelper.GetEnumList<PillType>();
return result;
}
[HttpGet("rarities")]
[Authorize]
public ActionResult<List<EnumInfoDto>> GetRarities()
{
var result = EnumHelper.GetEnumList<PillRarity>();
return result;
}
}
}

43
Build_God_Api/Build_God_Api/Controllers/ProfessionController.cs

@ -0,0 +1,43 @@
using Build_God_Api.Common;
using Build_God_Api.DB;
using Build_God_Api.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Build_God_Api.Controllers
{
[ApiController]
[Route("api/god/[controller]")]
public class ProfessionController(IProfessionService professionService) : ControllerBase
{
private readonly IProfessionService service = professionService;
[HttpPost]
[Authorize(Roles = "admin")]
public async Task<ActionResult<bool>> Add([FromBody] Profession item)
{
return await service.Add(item);
}
[HttpDelete("{id}")]
[Authorize(Roles = "admin")]
public async Task<ActionResult<bool>> Delete(int id)
{
return await service.Delete(id);
}
[HttpPut]
[Authorize(Roles = "admin")]
public async Task<ActionResult<bool>> Update([FromBody] Profession item)
{
return await service.Update(item);
}
[HttpGet("all")]
[Authorize]
public async Task<ActionResult<List<Profession>>> GetAll()
{
return await service.GetAll();
}
}
}

18
Build_God_Api/Build_God_Api/Controllers/StatisticsController.cs

@ -0,0 +1,18 @@
using Build_God_Api.Services;
using Microsoft.AspNetCore.Mvc;
namespace Build_God_Api.Controllers
{
[ApiController]
[Route("api/god/[controller]")]
public class StatisticsController(IStatisticsService service) : ControllerBase
{
private readonly IStatisticsService service = service;
[HttpGet("summary")]
public async Task<ActionResult<StatisticsSummary>> GetSummary()
{
return await service.GetSummary();
}
}
}

19
Build_God_Api/Build_God_Api/DB/Account.cs

@ -0,0 +1,19 @@
namespace Build_God_Api.DB
{
public class Account : BaseEntity
{
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string Password { get; set; } = string.Empty;
public bool Active { get; set; }
public bool IsAdmin { get; set; }
}
public enum ThirdPartyProvider
{
QQ,
WeChat,
System
}
}

50
Build_God_Api/Build_God_Api/DB/Bag.cs

@ -0,0 +1,50 @@
using SqlSugar;
using System.ComponentModel;
namespace Build_God_Api.DB
{
/// <summary>
/// 背包配置表 (预设5种背包类型)
/// </summary>
public class Bag : BaseEntity
{
/// <summary>
/// 背包名称
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// 背包稀有度
/// </summary>
public BagRarity Rarity { get; set; }
/// <summary>
/// 背包容量
/// </summary>
public int Capacity { get; set; }
/// <summary>
/// 描述
/// </summary>
[SugarColumn(IsNullable = true)]
public string? Description { get; set; }
}
/// <summary>
/// 背包稀有度枚举
/// </summary>
public enum BagRarity
{
[Description("普通")]
Common = 1,
[Description("稀有")]
Rare = 2,
[Description("史诗")]
Epic = 3,
[Description("传说")]
Legendary = 4
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save