578 lines
17 KiB
Markdown
578 lines
17 KiB
Markdown
# Design Document: Admin Frontend Migration
|
||
|
||
## Overview
|
||
|
||
本设计文档描述了 HoneyBox 后台管理系统前端业务模块的迁移方案。基于现有的 Vue 3 + Element Plus + TypeScript 技术栈,在 `admin-web` 项目中添加业务模块页面,对接已完成的后端 API(HoneyBox.Admin.Business)。
|
||
|
||
## Architecture
|
||
|
||
### 整体架构
|
||
|
||
```
|
||
admin-web/
|
||
├── src/
|
||
│ ├── api/ # API 服务层
|
||
│ │ ├── auth.ts # 认证 API (已有)
|
||
│ │ ├── menu.ts # 菜单 API (已有)
|
||
│ │ ├── role.ts # 角色 API (已有)
|
||
│ │ ├── dashboard.ts # 仪表盘 API (新增)
|
||
│ │ ├── user.ts # 用户管理 API (新增)
|
||
│ │ ├── vip.ts # VIP管理 API (新增)
|
||
│ │ ├── goods.ts # 商品管理 API (新增)
|
||
│ │ ├── goodsType.ts # 商品类型 API (新增)
|
||
│ │ ├── order.ts # 订单管理 API (新增)
|
||
│ │ ├── finance.ts # 财务管理 API (新增)
|
||
│ │ └── config.ts # 系统配置 API (新增)
|
||
│ │
|
||
│ ├── views/
|
||
│ │ ├── dashboard/ # 仪表盘 (升级)
|
||
│ │ ├── system/ # 系统管理 (已有)
|
||
│ │ └── business/ # 业务模块 (新增)
|
||
│ │ ├── user/ # 用户管理
|
||
│ │ ├── vip/ # VIP管理
|
||
│ │ ├── goods/ # 商品管理
|
||
│ │ ├── goodsType/ # 商品类型
|
||
│ │ ├── order/ # 订单管理
|
||
│ │ ├── finance/ # 财务管理
|
||
│ │ └── config/ # 系统配置
|
||
│ │
|
||
│ ├── router/ # 路由配置
|
||
│ ├── store/ # 状态管理
|
||
│ └── utils/ # 工具函数
|
||
```
|
||
|
||
### 技术栈
|
||
|
||
| 技术 | 版本 | 用途 |
|
||
|------|------|------|
|
||
| Vue.js | 3.5.x | 前端框架 |
|
||
| Element Plus | 2.9.x | UI 组件库 |
|
||
| TypeScript | 5.6.x | 类型系统 |
|
||
| Pinia | 3.0.x | 状态管理 |
|
||
| Vue Router | 4.5.x | 路由管理 |
|
||
| Axios | 1.7.x | HTTP 请求 |
|
||
| Vite | 6.0.x | 构建工具 |
|
||
|
||
## Components and Interfaces
|
||
|
||
### API 服务层接口
|
||
|
||
#### Dashboard API (`src/api/dashboard.ts`)
|
||
|
||
```typescript
|
||
interface DashboardOverview {
|
||
todayUsers: number;
|
||
todayOrders: number;
|
||
todayAmount: number;
|
||
totalUsers: number;
|
||
totalOrders: number;
|
||
totalAmount: number;
|
||
}
|
||
|
||
interface AdAccount {
|
||
id: number;
|
||
imgUrl: string;
|
||
linkUrl?: string;
|
||
sort: number;
|
||
status: number;
|
||
createTime: string;
|
||
}
|
||
|
||
// API Functions
|
||
export function getDashboardOverview(): Promise<ApiResponse<DashboardOverview>>;
|
||
export function getAdAccounts(): Promise<ApiResponse<AdAccount[]>>;
|
||
export function createAdAccount(data: { imgUrl: string; linkUrl?: string }): Promise<ApiResponse<{ id: number }>>;
|
||
export function deleteAdAccount(id: number): Promise<ApiResponse<void>>;
|
||
```
|
||
|
||
#### User API (`src/api/user.ts`)
|
||
|
||
```typescript
|
||
interface UserListRequest {
|
||
page: number;
|
||
pageSize: number;
|
||
userId?: number;
|
||
nickname?: string;
|
||
mobile?: string;
|
||
status?: number;
|
||
}
|
||
|
||
interface UserInfo {
|
||
id: number;
|
||
nickname: string;
|
||
avatar: string;
|
||
mobile: string;
|
||
money: number;
|
||
integral: number;
|
||
diamond: number;
|
||
vipLevel: number;
|
||
status: number;
|
||
isTest: number;
|
||
createTime: string;
|
||
}
|
||
|
||
interface UserDetail extends UserInfo {
|
||
openid: string;
|
||
inviteCode: string;
|
||
parentId: number;
|
||
totalConsume: number;
|
||
totalWin: number;
|
||
totalRecovery: number;
|
||
}
|
||
|
||
// API Functions
|
||
export function getUserList(params: UserListRequest): Promise<ApiResponse<PagedResult<UserInfo>>>;
|
||
export function getUserDetail(id: number): Promise<ApiResponse<UserDetail>>;
|
||
export function changeUserMoney(id: number, data: { type: string; amount: number; remark: string }): Promise<ApiResponse<void>>;
|
||
export function setUserStatus(id: number, status: number): Promise<ApiResponse<void>>;
|
||
export function setTestAccount(id: number, isTest: number): Promise<ApiResponse<void>>;
|
||
export function clearMobile(id: number): Promise<ApiResponse<void>>;
|
||
export function clearWeChat(id: number): Promise<ApiResponse<void>>;
|
||
export function giftCoupon(id: number, data: { couponId: number; count: number }): Promise<ApiResponse<void>>;
|
||
export function giftCard(id: number, data: { goodsId: number; prizeId: number; count: number }): Promise<ApiResponse<void>>;
|
||
export function getUserProfitLoss(id: number, startDate?: string, endDate?: string): Promise<ApiResponse<ProfitLoss>>;
|
||
export function getSubordinateUsers(id: number, page: number, pageSize: number): Promise<ApiResponse<PagedResult<UserInfo>>>;
|
||
```
|
||
|
||
#### Goods API (`src/api/goods.ts`)
|
||
|
||
```typescript
|
||
interface GoodsListRequest {
|
||
page: number;
|
||
pageSize: number;
|
||
name?: string;
|
||
typeId?: number;
|
||
status?: number;
|
||
}
|
||
|
||
interface GoodsInfo {
|
||
id: number;
|
||
name: string;
|
||
typeId: number;
|
||
typeName: string;
|
||
price: number;
|
||
image: string;
|
||
status: number;
|
||
sort: number;
|
||
createTime: string;
|
||
}
|
||
|
||
interface GoodsDetail extends GoodsInfo {
|
||
description: string;
|
||
images: string[];
|
||
rules: string;
|
||
prizes: PrizeInfo[];
|
||
}
|
||
|
||
interface PrizeInfo {
|
||
id: number;
|
||
name: string;
|
||
level: number;
|
||
levelName: string;
|
||
stock: number;
|
||
probability: number;
|
||
image: string;
|
||
price: number;
|
||
}
|
||
|
||
// API Functions
|
||
export function getGoodsList(params: GoodsListRequest): Promise<ApiResponse<PagedResult<GoodsInfo>>>;
|
||
export function getGoodsDetail(id: number): Promise<ApiResponse<GoodsDetail>>;
|
||
export function createGoods(data: GoodsCreateRequest): Promise<ApiResponse<{ id: number }>>;
|
||
export function updateGoods(id: number, data: GoodsUpdateRequest): Promise<ApiResponse<void>>;
|
||
export function deleteGoods(id: number): Promise<ApiResponse<void>>;
|
||
export function setGoodsStatus(id: number, status: number): Promise<ApiResponse<void>>;
|
||
export function getPrizes(goodsId: number): Promise<ApiResponse<PrizeInfo[]>>;
|
||
export function addPrize(goodsId: number, data: PrizeCreateRequest): Promise<ApiResponse<{ id: number }>>;
|
||
export function updatePrize(id: number, data: PrizeUpdateRequest): Promise<ApiResponse<void>>;
|
||
export function deletePrize(id: number): Promise<ApiResponse<void>>;
|
||
```
|
||
|
||
#### Order API (`src/api/order.ts`)
|
||
|
||
```typescript
|
||
interface OrderListRequest {
|
||
page: number;
|
||
pageSize: number;
|
||
orderNo?: string;
|
||
userId?: number;
|
||
status?: number;
|
||
startDate?: string;
|
||
endDate?: string;
|
||
}
|
||
|
||
interface OrderInfo {
|
||
id: number;
|
||
orderNo: string;
|
||
userId: number;
|
||
nickname: string;
|
||
goodsName: string;
|
||
amount: number;
|
||
status: number;
|
||
statusText: string;
|
||
createTime: string;
|
||
}
|
||
|
||
interface ShippingOrder {
|
||
id: number;
|
||
orderNo: string;
|
||
userId: number;
|
||
nickname: string;
|
||
address: string;
|
||
mobile: string;
|
||
status: number;
|
||
courierName?: string;
|
||
courierNumber?: string;
|
||
createTime: string;
|
||
}
|
||
|
||
// API Functions
|
||
export function getOrderList(params: OrderListRequest): Promise<ApiResponse<PagedResult<OrderInfo>>>;
|
||
export function getOrderDetail(id: number): Promise<ApiResponse<OrderDetail>>;
|
||
export function getStuckOrders(params: OrderListRequest): Promise<ApiResponse<PagedResult<OrderInfo>>>;
|
||
export function getRecoveryOrders(params: OrderListRequest): Promise<ApiResponse<PagedResult<OrderInfo>>>;
|
||
export function getShippingOrders(params: ShippingOrderListRequest): Promise<ApiResponse<PagedResult<ShippingOrder>>>;
|
||
export function shipOrder(id: number, data: { courierName: string; courierNumber: string }): Promise<ApiResponse<void>>;
|
||
export function cancelShippingOrder(id: number): Promise<ApiResponse<void>>;
|
||
export function exportOrders(params: OrderExportRequest): Promise<Blob>;
|
||
```
|
||
|
||
#### Finance API (`src/api/finance.ts`)
|
||
|
||
```typescript
|
||
interface FinanceRecord {
|
||
id: number;
|
||
userId: number;
|
||
nickname: string;
|
||
type: string;
|
||
amount: number;
|
||
balance: number;
|
||
remark: string;
|
||
createTime: string;
|
||
}
|
||
|
||
interface RankingItem {
|
||
rank: number;
|
||
userId: number;
|
||
nickname: string;
|
||
avatar: string;
|
||
totalAmount: number;
|
||
}
|
||
|
||
// API Functions
|
||
export function getConsumptionRanking(params: { page: number; pageSize: number; startDate?: string; endDate?: string }): Promise<ApiResponse<PagedResult<RankingItem>>>;
|
||
export function getMoneyRecords(params: FinanceListRequest): Promise<ApiResponse<PagedResult<FinanceRecord>>>;
|
||
export function getIntegralRecords(params: FinanceListRequest): Promise<ApiResponse<PagedResult<FinanceRecord>>>;
|
||
export function getDiamondRecords(params: FinanceListRequest): Promise<ApiResponse<PagedResult<FinanceRecord>>>;
|
||
export function getRechargeRecords(params: FinanceListRequest): Promise<ApiResponse<PagedResult<RechargeRecord>>>;
|
||
```
|
||
|
||
#### Config API (`src/api/config.ts`)
|
||
|
||
```typescript
|
||
interface ConfigGroup {
|
||
group: string;
|
||
name: string;
|
||
items: ConfigItem[];
|
||
}
|
||
|
||
interface ConfigItem {
|
||
key: string;
|
||
name: string;
|
||
value: string;
|
||
type: 'text' | 'number' | 'switch' | 'image' | 'textarea';
|
||
description?: string;
|
||
}
|
||
|
||
// API Functions
|
||
export function getConfigGroups(): Promise<ApiResponse<ConfigGroup[]>>;
|
||
export function getConfigByGroup(group: string): Promise<ApiResponse<ConfigItem[]>>;
|
||
export function updateConfig(group: string, items: { key: string; value: string }[]): Promise<ApiResponse<void>>;
|
||
```
|
||
|
||
### 页面组件结构
|
||
|
||
#### 用户管理页面 (`src/views/business/user/index.vue`)
|
||
|
||
```vue
|
||
<template>
|
||
<div class="user-container">
|
||
<!-- 搜索表单 -->
|
||
<el-card class="search-card">
|
||
<el-form :model="searchForm" inline>
|
||
<el-form-item label="用户ID">
|
||
<el-input v-model="searchForm.userId" />
|
||
</el-form-item>
|
||
<el-form-item label="昵称">
|
||
<el-input v-model="searchForm.nickname" />
|
||
</el-form-item>
|
||
<el-form-item label="手机号">
|
||
<el-input v-model="searchForm.mobile" />
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||
<el-button @click="handleReset">重置</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</el-card>
|
||
|
||
<!-- 用户列表 -->
|
||
<el-card>
|
||
<el-table :data="userList" v-loading="loading">
|
||
<el-table-column prop="id" label="ID" width="80" />
|
||
<el-table-column prop="nickname" label="昵称" />
|
||
<el-table-column prop="mobile" label="手机号" />
|
||
<el-table-column prop="money" label="余额" />
|
||
<el-table-column prop="vipLevel" label="VIP等级" />
|
||
<el-table-column prop="status" label="状态">
|
||
<template #default="{ row }">
|
||
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
|
||
{{ row.status === 1 ? '正常' : '封禁' }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="操作" width="200">
|
||
<template #default="{ row }">
|
||
<el-button link @click="handleDetail(row)">详情</el-button>
|
||
<el-button link @click="handleMoney(row)">资金</el-button>
|
||
<el-dropdown>
|
||
<el-button link>更多</el-button>
|
||
<template #dropdown>
|
||
<el-dropdown-menu>
|
||
<el-dropdown-item @click="handleStatus(row)">
|
||
{{ row.status === 1 ? '封号' : '解封' }}
|
||
</el-dropdown-item>
|
||
<el-dropdown-item @click="handleTest(row)">
|
||
{{ row.isTest === 1 ? '取消测试' : '设为测试' }}
|
||
</el-dropdown-item>
|
||
</el-dropdown-menu>
|
||
</template>
|
||
</el-dropdown>
|
||
</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<el-pagination
|
||
v-model:current-page="pagination.page"
|
||
v-model:page-size="pagination.pageSize"
|
||
:total="pagination.total"
|
||
@change="fetchUserList"
|
||
/>
|
||
</el-card>
|
||
|
||
<!-- 资金变动对话框 -->
|
||
<el-dialog v-model="moneyDialogVisible" title="资金变动">
|
||
<el-form :model="moneyForm" label-width="80px">
|
||
<el-form-item label="类型">
|
||
<el-select v-model="moneyForm.type">
|
||
<el-option label="余额" value="money" />
|
||
<el-option label="积分" value="integral" />
|
||
<el-option label="钻石" value="diamond" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="金额">
|
||
<el-input-number v-model="moneyForm.amount" />
|
||
</el-form-item>
|
||
<el-form-item label="备注">
|
||
<el-input v-model="moneyForm.remark" type="textarea" />
|
||
</el-form-item>
|
||
</el-form>
|
||
<template #footer>
|
||
<el-button @click="moneyDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="submitMoneyChange">确定</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
```
|
||
|
||
## Data Models
|
||
|
||
### 通用响应格式
|
||
|
||
```typescript
|
||
interface ApiResponse<T> {
|
||
code: number;
|
||
message: string;
|
||
data: T;
|
||
}
|
||
|
||
interface PagedResult<T> {
|
||
list: T[];
|
||
total: number;
|
||
page: number;
|
||
pageSize: number;
|
||
}
|
||
```
|
||
|
||
### 业务数据模型
|
||
|
||
详见上述 Components and Interfaces 部分的接口定义。
|
||
|
||
## Correctness Properties
|
||
|
||
*A property is a characteristic or behavior that should hold true across all valid executions of a system-essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
|
||
|
||
### Property 1: 列表分页数据一致性
|
||
|
||
*For any* 列表页面(用户、商品、订单等),当请求分页数据时,返回的列表长度应小于等于请求的 pageSize,且 total 应大于等于列表长度。
|
||
|
||
**Validates: Requirements 2.1, 4.1, 6.1**
|
||
|
||
### Property 2: 动态菜单权限过滤
|
||
|
||
*For any* 菜单数据和用户权限配置,生成的导航菜单应只包含用户有权限访问的菜单项,无权限的菜单项不应出现在导航中。
|
||
|
||
**Validates: Requirements 9.2, 9.4**
|
||
|
||
### Property 3: 请求拦截器Token注入
|
||
|
||
*For any* 需要认证的 API 请求,请求头中应包含有效的 Authorization Token,且 Token 格式应为 "Bearer {token}"。
|
||
|
||
**Validates: Requirements 10.3, 10.4**
|
||
|
||
### Property 4: 配置项类型渲染
|
||
|
||
*For any* 配置项数据,根据配置项的 type 字段,应渲染对应类型的输入组件(text 对应输入框,number 对应数字输入框,switch 对应开关,image 对应图片上传)。
|
||
|
||
**Validates: Requirements 8.4**
|
||
|
||
### Property 5: 错误响应处理
|
||
|
||
*For any* API 响应,当 code 不为 0 时,应显示错误提示信息,且错误信息应来自响应的 message 字段。
|
||
|
||
**Validates: Requirements 1.4, 10.3**
|
||
|
||
## Error Handling
|
||
|
||
### API 错误处理
|
||
|
||
```typescript
|
||
// src/utils/request.ts
|
||
import axios from 'axios'
|
||
import { ElMessage } from 'element-plus'
|
||
import { getToken, removeToken } from './auth'
|
||
import router from '@/router'
|
||
|
||
const request = axios.create({
|
||
baseURL: '/api',
|
||
timeout: 30000
|
||
})
|
||
|
||
// 请求拦截器
|
||
request.interceptors.request.use(
|
||
config => {
|
||
const token = getToken()
|
||
if (token) {
|
||
config.headers.Authorization = `Bearer ${token}`
|
||
}
|
||
return config
|
||
},
|
||
error => Promise.reject(error)
|
||
)
|
||
|
||
// 响应拦截器
|
||
request.interceptors.response.use(
|
||
response => {
|
||
const res = response.data
|
||
if (res.code !== 0) {
|
||
ElMessage.error(res.message || '请求失败')
|
||
// Token 过期
|
||
if (res.code === 40001) {
|
||
removeToken()
|
||
router.push('/login')
|
||
}
|
||
return Promise.reject(new Error(res.message))
|
||
}
|
||
return res
|
||
},
|
||
error => {
|
||
ElMessage.error(error.message || '网络错误')
|
||
return Promise.reject(error)
|
||
}
|
||
)
|
||
|
||
export default request
|
||
```
|
||
|
||
### 表单验证错误
|
||
|
||
```typescript
|
||
// 使用 Element Plus 表单验证
|
||
const rules = {
|
||
amount: [
|
||
{ required: true, message: '请输入金额', trigger: 'blur' },
|
||
{ type: 'number', min: 0.01, message: '金额必须大于0', trigger: 'blur' }
|
||
],
|
||
remark: [
|
||
{ required: true, message: '请输入备注', trigger: 'blur' }
|
||
]
|
||
}
|
||
```
|
||
|
||
## Testing Strategy
|
||
|
||
### 单元测试
|
||
|
||
使用 Vitest 进行单元测试,测试以下内容:
|
||
|
||
1. **API 服务层测试** - 验证 API 函数正确调用 axios
|
||
2. **工具函数测试** - 验证 Token 管理、请求拦截器等
|
||
3. **组件测试** - 验证页面组件渲染和交互
|
||
|
||
### 属性测试
|
||
|
||
使用 fast-check 进行属性测试,验证以下属性:
|
||
|
||
1. **Property 1** - 分页数据一致性
|
||
2. **Property 2** - 菜单权限过滤
|
||
3. **Property 3** - Token 注入
|
||
4. **Property 4** - 配置项类型渲染
|
||
5. **Property 5** - 错误响应处理
|
||
|
||
### 测试配置
|
||
|
||
```typescript
|
||
// vitest.config.ts
|
||
import { defineConfig } from 'vitest/config'
|
||
import vue from '@vitejs/plugin-vue'
|
||
|
||
export default defineConfig({
|
||
plugins: [vue()],
|
||
test: {
|
||
environment: 'jsdom',
|
||
globals: true,
|
||
coverage: {
|
||
reporter: ['text', 'json', 'html']
|
||
}
|
||
}
|
||
})
|
||
```
|
||
|
||
### 测试示例
|
||
|
||
```typescript
|
||
// src/api/__tests__/user.test.ts
|
||
import { describe, it, expect, vi } from 'vitest'
|
||
import fc from 'fast-check'
|
||
import { getUserList } from '../user'
|
||
|
||
describe('User API', () => {
|
||
// Property 1: 分页数据一致性
|
||
it('should return list length <= pageSize', async () => {
|
||
await fc.assert(
|
||
fc.asyncProperty(
|
||
fc.integer({ min: 1, max: 100 }),
|
||
fc.integer({ min: 1, max: 50 }),
|
||
async (page, pageSize) => {
|
||
const result = await getUserList({ page, pageSize })
|
||
expect(result.data.list.length).toBeLessThanOrEqual(pageSize)
|
||
expect(result.data.total).toBeGreaterThanOrEqual(result.data.list.length)
|
||
}
|
||
),
|
||
{ numRuns: 100 }
|
||
)
|
||
})
|
||
})
|
||
```
|
||
|