HaniBlindBox/.kiro/specs/goods-management-frontend/design.md
2026-01-17 18:04:08 +08:00

12 KiB
Raw Blame History

商品管理模块前端设计文档

架构设计

目录结构

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 (类型编辑弹窗)

数据模型

盒子类型枚举

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]: '特殊盒子',
};

盒子类型字段配置

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,
  },
  // ... 其他类型配置
};

盒子数据模型

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;
}

奖品数据模型

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接口设计

// 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 盒子新增弹窗

核心逻辑:根据盒子类型动态显示/隐藏字段

// 计算属性:根据类型获取字段配置
const fieldConfig = computed(() => {
  return GoodsTypeFieldConfigs[formData.type] || defaultFieldConfig;
});

// 监听类型变化,重置相关字段
watch(() => formData.type, (newType) => {
  resetTypeSpecificFields();
});

PrizeListDialog 奖品列表弹窗

核心逻辑:根据盒子类型动态显示不同列

// 计算属性:动态列配置
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;
});

路由配置

// 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' }
},

属性测试设计

测试用例

// 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

// 盒子列表状态
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,
});

错误处理

// 统一错误处理
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, '创建失败');
}