commit
9c0bcc23ae
178 changed files with 23148 additions and 0 deletions
@ -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 |
||||
@ -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 |
||||
@ -0,0 +1,2 @@ |
|||||
|
# 开发环境配置 |
||||
|
VITE_API_URL=http://localhost:5091/api/god/ |
||||
@ -0,0 +1,2 @@ |
|||||
|
# 生产环境配置 |
||||
|
VITE_API_URL=https://api.example.com/api |
||||
@ -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 |
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Binary file not shown.
@ -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}" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -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}" |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
|
] |
||||
|
} |
||||
@ -0,0 +1,3 @@ |
|||||
|
{ |
||||
|
"CurrentProjectSetting": null |
||||
|
} |
||||
@ -0,0 +1,6 @@ |
|||||
|
{ |
||||
|
"ExpandedNodes": [ |
||||
|
"" |
||||
|
], |
||||
|
"PreviewInSolutionExplorer": false |
||||
|
} |
||||
Binary file not shown.
@ -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 |
||||
@ -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) |
||||
@ -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. 测试登录功能 |
||||
|
|
||||
|
有任何问题,请参考文档或查看相关代码注释!🚀 |
||||
@ -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 刷新机制 |
||||
|
- [ ] 添加错误日志上报 |
||||
@ -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. 检查后端日志 |
||||
@ -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` 进行测试 |
||||
@ -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 |
||||
|
**主题**: 暗黑风格(黑白双色) |
||||
|
**状态**: ✅ 已完成且可直接使用 |
||||
|
|
||||
|
祝你开发愉快!🚀 |
||||
@ -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/ # 自定义指令 |
||||
|
└── ... |
||||
|
``` |
||||
|
|
||||
|
祝你开发愉快!如有任何问题,随时联系我。 |
||||
@ -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 |
||||
|
``` |
||||
@ -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) |
||||
@ -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. 在登录页测试 |
||||
|
|
||||
|
准备好了吗?让我们开始吧!🚀 |
||||
@ -0,0 +1 @@ |
|||||
|
/// <reference types="vite/client" />
|
||||
@ -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> |
||||
File diff suppressed because it is too large
@ -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" |
||||
|
} |
||||
|
} |
||||
|
After Width: | Height: | Size: 4.2 KiB |
@ -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> |
||||
@ -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', {}) |
||||
|
} |
||||
@ -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') |
||||
|
} |
||||
@ -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}`) |
||||
|
} |
||||
@ -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 } }); |
||||
|
}; |
||||
@ -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 |
||||
@ -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}`); |
||||
|
} |
||||
@ -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"); |
||||
|
}; |
||||
@ -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') |
||||
|
} |
||||
@ -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'); |
||||
|
} |
||||
@ -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}`); |
||||
|
} |
||||
@ -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') |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
After Width: | Height: | Size: 276 B |
@ -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; |
||||
|
} |
||||
@ -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> |
||||
@ -0,0 +1,41 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
defineProps<{ |
||||
|
msg: string |
||||
|
}>() |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<div class="greetings"> |
||||
|
<h1 class="green">{{ msg }}</h1> |
||||
|
<h3> |
||||
|
You’ve 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> |
||||
@ -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> |
||||
@ -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> |
||||
|
|
||||
|
Vue’s |
||||
|
<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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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' |
||||
|
} |
||||
|
} |
||||
@ -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') |
||||
@ -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 |
||||
@ -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 |
||||
|
}; |
||||
|
}); |
||||
@ -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 } |
||||
|
}) |
||||
@ -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> |
||||
@ -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> |
||||
@ -0,0 +1,9 @@ |
|||||
|
<script setup lang="ts"> |
||||
|
import TheWelcome from '../components/TheWelcome.vue' |
||||
|
</script> |
||||
|
|
||||
|
<template> |
||||
|
<main> |
||||
|
<TheWelcome /> |
||||
|
</main> |
||||
|
</template> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
File diff suppressed because it is too large
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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/*"] |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
{ |
||||
|
"files": [], |
||||
|
"references": [ |
||||
|
{ |
||||
|
"path": "./tsconfig.node.json" |
||||
|
}, |
||||
|
{ |
||||
|
"path": "./tsconfig.app.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"] |
||||
|
} |
||||
|
} |
||||
@ -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)) |
||||
|
}, |
||||
|
}, |
||||
|
}) |
||||
@ -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/** |
||||
@ -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/ |
||||
@ -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 |
||||
@ -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> |
||||
@ -0,0 +1,6 @@ |
|||||
|
@Build_God_Api_HostAddress = http://localhost:5091 |
||||
|
|
||||
|
GET {{Build_God_Api_HostAddress}}/weatherforecast/ |
||||
|
Accept: application/json |
||||
|
|
||||
|
### |
||||
@ -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; } |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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; } |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
} |
||||
|
} |
||||
@ -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…
Reference in new issue