17 KiB
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)
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)
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)
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)
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)
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)
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)
<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
通用响应格式
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 错误处理
// 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
表单验证错误
// 使用 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 进行单元测试,测试以下内容:
- API 服务层测试 - 验证 API 函数正确调用 axios
- 工具函数测试 - 验证 Token 管理、请求拦截器等
- 组件测试 - 验证页面组件渲染和交互
属性测试
使用 fast-check 进行属性测试,验证以下属性:
- Property 1 - 分页数据一致性
- Property 2 - 菜单权限过滤
- Property 3 - Token 注入
- Property 4 - 配置项类型渲染
- Property 5 - 错误响应处理
测试配置
// 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']
}
}
})
测试示例
// 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 }
)
})
})