434 lines
12 KiB
Markdown
434 lines
12 KiB
Markdown
# 商品管理模块前端设计文档
|
||
|
||
## 架构设计
|
||
|
||
### 目录结构
|
||
|
||
```
|
||
src/views/business/goods/
|
||
├── index.vue # 盒子列表页面
|
||
├── type.vue # 盒子类型管理页面
|
||
├── components/
|
||
│ ├── GoodsSearchForm.vue # 盒子搜索表单
|
||
│ ├── GoodsTable.vue # 盒子列表表格
|
||
│ ├── GoodsAddDialog.vue # 盒子新增弹窗
|
||
│ ├── GoodsEditDialog.vue # 盒子编辑弹窗
|
||
│ ├── GoodsExtendDialog.vue # 盒子扩展设置弹窗
|
||
│ ├── PrizeListDialog.vue # 奖品列表弹窗
|
||
│ ├── PrizeAddDialog.vue # 奖品新增弹窗
|
||
│ ├── PrizeEditDialog.vue # 奖品编辑弹窗
|
||
│ ├── TypeAddDialog.vue # 类型新增弹窗
|
||
│ └── TypeEditDialog.vue # 类型编辑弹窗
|
||
|
||
src/api/business/
|
||
├── goods.ts # 商品API接口
|
||
```
|
||
|
||
### 组件关系图
|
||
|
||
```
|
||
index.vue (盒子列表)
|
||
├── GoodsSearchForm.vue (搜索表单)
|
||
├── GoodsTable.vue (列表表格)
|
||
│ ├── GoodsAddDialog.vue (新增弹窗)
|
||
│ ├── GoodsEditDialog.vue (编辑弹窗)
|
||
│ ├── GoodsExtendDialog.vue (扩展设置弹窗)
|
||
│ └── PrizeListDialog.vue (奖品列表弹窗)
|
||
│ ├── PrizeAddDialog.vue (奖品新增弹窗)
|
||
│ └── PrizeEditDialog.vue (奖品编辑弹窗)
|
||
|
||
type.vue (类型管理)
|
||
├── TypeAddDialog.vue (类型新增弹窗)
|
||
└── TypeEditDialog.vue (类型编辑弹窗)
|
||
```
|
||
|
||
## 数据模型
|
||
|
||
### 盒子类型枚举
|
||
|
||
```typescript
|
||
export enum GoodsType {
|
||
YiFanShang = 1, // 一番赏
|
||
WuXianShang = 2, // 无限赏
|
||
LeiTaiShang = 3, // 擂台赏
|
||
FuDai = 5, // 福袋
|
||
XingYunShang = 6, // 幸运赏
|
||
LingZhuShang = 8, // 领主赏
|
||
LianJiShang = 9, // 连击赏
|
||
MangHe = 10, // 盲盒
|
||
XingYunShangNew = 11,// 幸运赏(新)
|
||
FuLiWu = 15, // 福利屋
|
||
FanBeiShang = 16, // 翻倍赏
|
||
TeShuHeZi = 17, // 特殊盒子
|
||
}
|
||
|
||
export const GoodsTypeLabels: Record<GoodsType, string> = {
|
||
[GoodsType.YiFanShang]: '一番赏',
|
||
[GoodsType.WuXianShang]: '无限赏',
|
||
[GoodsType.LeiTaiShang]: '擂台赏',
|
||
[GoodsType.FuDai]: '福袋',
|
||
[GoodsType.XingYunShang]: '幸运赏',
|
||
[GoodsType.LingZhuShang]: '领主赏',
|
||
[GoodsType.LianJiShang]: '连击赏',
|
||
[GoodsType.MangHe]: '盲盒',
|
||
[GoodsType.XingYunShangNew]: '幸运赏(新)',
|
||
[GoodsType.FuLiWu]: '福利屋',
|
||
[GoodsType.FanBeiShang]: '翻倍赏',
|
||
[GoodsType.TeShuHeZi]: '特殊盒子',
|
||
};
|
||
```
|
||
|
||
### 盒子类型字段配置
|
||
|
||
```typescript
|
||
export interface GoodsTypeFieldConfig {
|
||
showStock: boolean; // 显示套数
|
||
showLock: boolean; // 显示锁箱配置
|
||
showDailyLimit: boolean; // 显示每日限购
|
||
showRage: boolean; // 显示怒气值
|
||
showItemCard: boolean; // 显示道具卡
|
||
showLingzhu: boolean; // 显示领主配置
|
||
showLianji: boolean; // 显示连击配置
|
||
showTimeConfig: boolean; // 显示时间配置(福利屋)
|
||
showAutoXiajia: boolean; // 显示自动下架
|
||
showCoupon: boolean; // 显示发券开关
|
||
showIntegral: boolean; // 显示发积分开关
|
||
showDescription: boolean; // 显示盒子描述
|
||
showQuanjuXiangou: boolean; // 显示限购次数
|
||
}
|
||
|
||
export const GoodsTypeFieldConfigs: Record<GoodsType, GoodsTypeFieldConfig> = {
|
||
[GoodsType.YiFanShang]: {
|
||
showStock: true, showLock: true, showDailyLimit: true,
|
||
showRage: false, showItemCard: false, showLingzhu: false,
|
||
showLianji: false, showTimeConfig: false, showAutoXiajia: true,
|
||
showCoupon: true, showIntegral: true, showDescription: false,
|
||
showQuanjuXiangou: false,
|
||
},
|
||
[GoodsType.WuXianShang]: {
|
||
showStock: false, showLock: false, showDailyLimit: false,
|
||
showRage: true, showItemCard: true, showLingzhu: false,
|
||
showLianji: false, showTimeConfig: false, showAutoXiajia: true,
|
||
showCoupon: true, showIntegral: true, showDescription: false,
|
||
showQuanjuXiangou: false,
|
||
},
|
||
// ... 其他类型配置
|
||
};
|
||
```
|
||
|
||
### 盒子数据模型
|
||
|
||
```typescript
|
||
export interface GoodsItem {
|
||
id: number;
|
||
type: GoodsType;
|
||
title: string;
|
||
price: number;
|
||
stock: number;
|
||
imgUrl: string;
|
||
imgUrlDetail: string;
|
||
status: number; // 0-下架 1-上架 2-售罄
|
||
sort: number;
|
||
isNew: boolean;
|
||
isShouZhe: boolean; // 首抽五折
|
||
lockIs: boolean; // 锁箱开关
|
||
lockTime: number; // 锁箱时间(秒)
|
||
dailyXiangou: number; // 每日限购
|
||
quanjuXiangou: number; // 限购次数
|
||
rageIs: boolean; // 怒气值开关
|
||
rage: number; // 怒气值
|
||
itemCardId: number; // 道具卡ID
|
||
couponIs: boolean; // 发券开关
|
||
integralIs: boolean; // 发积分开关
|
||
isAutoXiajia: boolean; // 自动下架开关
|
||
xiajiaLirun: number; // 下架利润值(%)
|
||
xiajiaAutoCoushu: number; // 下架抽数阈值
|
||
xiajiaJine: number; // 下架金额
|
||
unlockAmount: number; // 解锁金额
|
||
choujiangXianzhi: number; // 抽奖门槛
|
||
goodsDescribe: string; // 盒子描述
|
||
// 福利屋专用
|
||
flwStartTime: string; // 开始时间
|
||
flwEndTime: string; // 结束时间
|
||
openTime: string; // 开奖时间
|
||
// 领主赏专用
|
||
lingzhuIs: boolean;
|
||
lingzhuFan: number;
|
||
lingzhuShangId: number;
|
||
// 连击赏专用
|
||
lianJiNum: number;
|
||
lianJiShangId: number;
|
||
// 时间戳
|
||
createTime: string;
|
||
updateTime: string;
|
||
}
|
||
```
|
||
|
||
### 奖品数据模型
|
||
|
||
```typescript
|
||
export enum PrizeCategory {
|
||
XianHuo = 1, // 现货
|
||
YuShou = 2, // 预售
|
||
HuoBi = 3, // 货币
|
||
BaoXiang = 4, // 宝箱
|
||
}
|
||
|
||
export interface PrizeItem {
|
||
id: number;
|
||
goodsId: number;
|
||
title: string;
|
||
category: PrizeCategory;
|
||
level: string; // A/B/C/D/E等
|
||
price: number; // 售价
|
||
costPrice: number; // 采购价
|
||
referencePrice: number; // 参考价
|
||
quantity: number; // 数量(一番赏等)
|
||
probability: number; // 概率%(无限赏等)
|
||
giftMultiple: number; // 赠送倍率(翻倍赏)
|
||
isLingzhu: boolean; // 是否领主(领主赏)
|
||
preSaleTime: string; // 预售时间
|
||
sort: number;
|
||
imgUrl: string;
|
||
detailImgUrl: string;
|
||
giftCurrency: GiftCurrency[]; // 赠送货币配置
|
||
children?: PrizeItem[]; // 宝箱子奖品
|
||
createTime: string;
|
||
updateTime: string;
|
||
}
|
||
|
||
export interface GiftCurrency {
|
||
type: string; // 货币类型
|
||
amount: number; // 数量
|
||
}
|
||
```
|
||
|
||
## API接口设计
|
||
|
||
```typescript
|
||
// src/api/business/goods.ts
|
||
|
||
import request from '@/utils/request';
|
||
|
||
const BASE_URL = '/api/admin/business/goods';
|
||
|
||
// 盒子管理
|
||
export const goodsApi = {
|
||
// 获取盒子列表
|
||
getList: (params: GoodsListParams) =>
|
||
request.get<PageResult<GoodsItem>>(`${BASE_URL}`, { params }),
|
||
|
||
// 获取盒子详情
|
||
getDetail: (id: number) =>
|
||
request.get<GoodsItem>(`${BASE_URL}/${id}`),
|
||
|
||
// 创建盒子
|
||
create: (data: GoodsCreateRequest) =>
|
||
request.post<{ id: number }>(`${BASE_URL}`, data),
|
||
|
||
// 更新盒子
|
||
update: (id: number, data: GoodsUpdateRequest) =>
|
||
request.put(`${BASE_URL}/${id}`, data),
|
||
|
||
// 删除盒子
|
||
delete: (id: number) =>
|
||
request.delete(`${BASE_URL}/${id}`),
|
||
|
||
// 设置盒子状态
|
||
setStatus: (id: number, status: number) =>
|
||
request.put(`${BASE_URL}/${id}/status`, { status }),
|
||
|
||
// 获取盒子奖品列表
|
||
getPrizes: (goodsId: number) =>
|
||
request.get<PrizeItem[]>(`${BASE_URL}/${goodsId}/prizes`),
|
||
|
||
// 添加奖品
|
||
addPrize: (goodsId: number, data: PrizeCreateRequest) =>
|
||
request.post<{ id: number }>(`${BASE_URL}/${goodsId}/prizes`, data),
|
||
};
|
||
|
||
// 奖品管理
|
||
export const prizesApi = {
|
||
// 获取奖品详情
|
||
getDetail: (id: number) =>
|
||
request.get<PrizeItem>(`/api/admin/business/prizes/${id}`),
|
||
|
||
// 更新奖品
|
||
update: (id: number, data: PrizeUpdateRequest) =>
|
||
request.put(`/api/admin/business/prizes/${id}`, data),
|
||
|
||
// 删除奖品
|
||
delete: (id: number) =>
|
||
request.delete(`/api/admin/business/prizes/${id}`),
|
||
};
|
||
|
||
// 盒子类型管理
|
||
export const goodsTypeApi = {
|
||
// 获取类型列表
|
||
getList: () =>
|
||
request.get<GoodsTypeItem[]>(`${BASE_URL}/types`),
|
||
|
||
// 创建类型
|
||
create: (data: GoodsTypeCreateRequest) =>
|
||
request.post(`${BASE_URL}/types`, data),
|
||
|
||
// 更新类型
|
||
update: (id: number, data: GoodsTypeUpdateRequest) =>
|
||
request.put(`${BASE_URL}/types/${id}`, data),
|
||
|
||
// 删除类型
|
||
delete: (id: number) =>
|
||
request.delete(`${BASE_URL}/types/${id}`),
|
||
};
|
||
```
|
||
|
||
## 组件设计
|
||
|
||
### GoodsAddDialog 盒子新增弹窗
|
||
|
||
核心逻辑:根据盒子类型动态显示/隐藏字段
|
||
|
||
```typescript
|
||
// 计算属性:根据类型获取字段配置
|
||
const fieldConfig = computed(() => {
|
||
return GoodsTypeFieldConfigs[formData.type] || defaultFieldConfig;
|
||
});
|
||
|
||
// 监听类型变化,重置相关字段
|
||
watch(() => formData.type, (newType) => {
|
||
resetTypeSpecificFields();
|
||
});
|
||
```
|
||
|
||
### PrizeListDialog 奖品列表弹窗
|
||
|
||
核心逻辑:根据盒子类型动态显示不同列
|
||
|
||
```typescript
|
||
// 计算属性:动态列配置
|
||
const dynamicColumns = computed(() => {
|
||
const type = props.goodsType;
|
||
const columns = [...baseColumns];
|
||
|
||
// 一番赏/擂台赏/福袋等显示数量列
|
||
if ([1, 3, 5, 6, 10, 11, 15, 17].includes(type)) {
|
||
columns.push({ prop: 'quantity', label: '奖品数量' });
|
||
}
|
||
|
||
// 无限赏/翻倍赏等显示概率列
|
||
if ([2, 8, 9, 16, 17].includes(type)) {
|
||
columns.push({ prop: 'probability', label: '真实概率(%)' });
|
||
}
|
||
|
||
// 翻倍赏显示赠送倍率
|
||
if ([16, 17].includes(type)) {
|
||
columns.push({ prop: 'giftMultiple', label: '赠送倍率' });
|
||
}
|
||
|
||
return columns;
|
||
});
|
||
```
|
||
|
||
## 路由配置
|
||
|
||
```typescript
|
||
// src/router/modules/business.ts
|
||
|
||
{
|
||
path: 'goods',
|
||
name: 'BusinessGoods',
|
||
component: () => import('@/views/business/goods/index.vue'),
|
||
meta: { title: '盒子管理', icon: 'goods', permission: 'goods:list' }
|
||
},
|
||
{
|
||
path: 'goods/type',
|
||
name: 'BusinessGoodsType',
|
||
component: () => import('@/views/business/goods/type.vue'),
|
||
meta: { title: '盒子类型', icon: 'type', permission: 'goods:type' }
|
||
},
|
||
```
|
||
|
||
## 属性测试设计
|
||
|
||
### 测试用例
|
||
|
||
```csharp
|
||
// GoodsManagementFrontendPropertyTests.cs
|
||
|
||
[Property]
|
||
public Property GoodsTypeFieldConfig_ShouldBeConsistent()
|
||
{
|
||
// 验证每种盒子类型都有对应的字段配置
|
||
}
|
||
|
||
[Property]
|
||
public Property GoodsCreate_WithValidData_ShouldSucceed()
|
||
{
|
||
// 验证有效数据创建盒子成功
|
||
}
|
||
|
||
[Property]
|
||
public Property GoodsCreate_WithInvalidType_ShouldFail()
|
||
{
|
||
// 验证无效类型创建盒子失败
|
||
}
|
||
|
||
[Property]
|
||
public Property PrizeCreate_WithValidData_ShouldSucceed()
|
||
{
|
||
// 验证有效数据创建奖品成功
|
||
}
|
||
|
||
[Property]
|
||
public Property PrizeProbability_ShouldNotExceed100()
|
||
{
|
||
// 验证奖品概率总和不超过100%
|
||
}
|
||
```
|
||
|
||
## 状态管理
|
||
|
||
使用组合式API管理状态,不需要Pinia store:
|
||
|
||
```typescript
|
||
// 盒子列表状态
|
||
const goodsListState = reactive({
|
||
loading: false,
|
||
list: [] as GoodsItem[],
|
||
total: 0,
|
||
searchParams: {
|
||
title: '',
|
||
status: null,
|
||
type: null,
|
||
page: 1,
|
||
pageSize: 20,
|
||
},
|
||
});
|
||
|
||
// 奖品列表状态
|
||
const prizeListState = reactive({
|
||
loading: false,
|
||
list: [] as PrizeItem[],
|
||
goodsId: 0,
|
||
goodsType: 0,
|
||
});
|
||
```
|
||
|
||
## 错误处理
|
||
|
||
```typescript
|
||
// 统一错误处理
|
||
const handleApiError = (error: any, defaultMessage: string) => {
|
||
const message = error.response?.data?.message || defaultMessage;
|
||
ElMessage.error(message);
|
||
};
|
||
|
||
// 使用示例
|
||
try {
|
||
await goodsApi.create(formData);
|
||
ElMessage.success('创建成功');
|
||
} catch (error) {
|
||
handleApiError(error, '创建失败');
|
||
}
|
||
```
|