浏览代码

!270 合并商城最新代码
Merge pull request !270 from 芋道源码/dev

芋道源码 1 年之前
父节点
当前提交
6ed7b3a69e
共有 91 个文件被更改,包括 4035 次插入1166 次删除
  1. 2 2
      src/api/mall/product/spu.ts
  2. 8 4
      src/api/mall/promotion/bargain/bargainActivity.ts
  3. 14 0
      src/api/mall/promotion/bargain/bargainHelp.ts
  4. 19 0
      src/api/mall/promotion/bargain/bargainRecord.ts
  5. 5 0
      src/api/mall/promotion/combination/combinationActivity.ts
  6. 33 0
      src/api/mall/promotion/combination/combinationRecord.ts
  7. 5 0
      src/api/mall/promotion/seckill/seckillActivity.ts
  8. 91 0
      src/api/mall/statistics/member.ts
  9. 70 0
      src/api/mall/statistics/trade.ts
  10. 2 2
      src/api/mall/trade/config/index.ts
  11. 17 5
      src/api/mall/trade/order/index.ts
  12. 19 0
      src/api/member/config/index.ts
  13. 0 19
      src/api/member/point/config/index.ts
  14. 5 4
      src/api/member/signin/config/index.ts
  15. 10 0
      src/api/member/user/index.ts
  16. 22 0
      src/api/pay/wallet/index.ts
  17. 0 8
      src/api/system/area/index.ts
  18. 0 0
      src/assets/map/json/china.json
  19. 11 11
      src/components/CountTo/src/CountTo.vue
  20. 1 0
      src/components/Icon/src/Icon.vue
  21. 4 3
      src/layout/components/Breadcrumb/src/Breadcrumb.vue
  22. 7 7
      src/layout/components/useRenderLayout.tsx
  23. 3 1
      src/plugins/echarts/index.ts
  24. 13 15
      src/router/modules/remaining.ts
  25. 80 24
      src/utils/constants.ts
  26. 4 2
      src/utils/dict.ts
  27. 117 1
      src/utils/formatTime.ts
  28. 4 9
      src/utils/formatter.ts
  29. 3 3
      src/utils/index.ts
  30. 4 3
      src/utils/tree.ts
  31. 5 5
      src/views/mall/product/category/index.vue
  32. 8 9
      src/views/mall/product/comment/index.vue
  33. 3 3
      src/views/mall/product/property/index.vue
  34. 2 2
      src/views/mall/product/property/value/index.vue
  35. 10 10
      src/views/mall/product/spu/components/SkuList.vue
  36. 2 1
      src/views/mall/product/spu/components/SkuTableSelect.vue
  37. 18 33
      src/views/mall/product/spu/form/BasicInfoForm.vue
  38. 1 1
      src/views/mall/product/spu/form/OtherSettingsForm.vue
  39. 10 10
      src/views/mall/product/spu/form/index.vue
  40. 53 81
      src/views/mall/product/spu/index.vue
  41. 8 7
      src/views/mall/promotion/bargain/activity/BargainActivityForm.vue
  42. 2 21
      src/views/mall/promotion/bargain/activity/bargainActivity.data.ts
  43. 194 70
      src/views/mall/promotion/bargain/activity/index.vue
  44. 90 0
      src/views/mall/promotion/bargain/record/BargainRecordListDialog.vue
  45. 195 0
      src/views/mall/promotion/bargain/record/index.vue
  46. 1 1
      src/views/mall/promotion/combination/activity/CombinationActivityForm.vue
  47. 10 29
      src/views/mall/promotion/combination/activity/combinationActivity.data.ts
  48. 198 72
      src/views/mall/promotion/combination/activity/index.vue
  49. 98 0
      src/views/mall/promotion/combination/record/CombinationRecordListDialog.vue
  50. 269 2
      src/views/mall/promotion/combination/record/index.vue
  51. 1 1
      src/views/mall/promotion/coupon/formatter.ts
  52. 11 5
      src/views/mall/promotion/coupon/index.vue
  53. 18 21
      src/views/mall/promotion/coupon/template/index.vue
  54. 212 90
      src/views/mall/promotion/seckill/activity/index.vue
  55. 0 96
      src/views/mall/promotion/seckill/activity/seckillActivity.data.ts
  56. 484 0
      src/views/mall/statistics/member/index.vue
  57. 36 0
      src/views/mall/statistics/trade/components/TradeStatisticValue.vue
  58. 52 0
      src/views/mall/statistics/trade/components/TradeTrendValue.vue
  59. 428 0
      src/views/mall/statistics/trade/index.vue
  60. 42 38
      src/views/mall/trade/afterSale/detail/index.vue
  61. 4 5
      src/views/mall/trade/afterSale/index.vue
  62. 2 2
      src/views/mall/trade/brokerage/record/index.vue
  63. 1 1
      src/views/mall/trade/brokerage/user/BrokerageOrderListDialog.vue
  64. 1 1
      src/views/mall/trade/brokerage/user/BrokerageUserListDialog.vue
  65. 2 2
      src/views/mall/trade/brokerage/user/UpdateBindUserForm.vue
  66. 4 4
      src/views/mall/trade/brokerage/user/index.vue
  67. 6 4
      src/views/mall/trade/brokerage/withdraw/index.vue
  68. 84 21
      src/views/mall/trade/config/index.vue
  69. 4 4
      src/views/mall/trade/delivery/express/ExpressForm.vue
  70. 4 4
      src/views/mall/trade/delivery/express/index.vue
  71. 46 159
      src/views/mall/trade/delivery/expressTemplate/ExpressTemplateForm.vue
  72. 4 4
      src/views/mall/trade/delivery/expressTemplate/index.vue
  73. 12 26
      src/views/mall/trade/delivery/pickUpStore/PickUpStoreForm.vue
  74. 12 7
      src/views/mall/trade/delivery/pickUpStore/index.vue
  75. 134 72
      src/views/mall/trade/order/detail/index.vue
  76. 2 2
      src/views/mall/trade/order/form/OrderDeliveryForm.vue
  77. 1 1
      src/views/mall/trade/order/form/OrderUpdateAddressForm.vue
  78. 1 1
      src/views/mall/trade/order/form/OrderUpdatePriceForm.vue
  79. 1 1
      src/views/mall/trade/order/form/OrderUpdateRemarkForm.vue
  80. 48 18
      src/views/mall/trade/order/index.vue
  81. 19 19
      src/views/member/config/index.vue
  82. 32 13
      src/views/member/signin/config/SignInConfigForm.vue
  83. 2 1
      src/views/member/signin/config/index.vue
  84. 0 0
      src/views/member/user/UserLevelUpdateForm.vue
  85. 128 0
      src/views/member/user/UserPointUpdateForm.vue
  86. 0 14
      src/views/member/user/components/growth-list.vue
  87. 31 5
      src/views/member/user/detail/UserAccountInfo.vue
  88. 125 0
      src/views/member/user/detail/UserBrokerageList.vue
  89. 190 0
      src/views/member/user/detail/UserCouponList.vue
  90. 25 21
      src/views/member/user/detail/index.vue
  91. 76 23
      src/views/member/user/index.vue

+ 2 - 2
src/api/mall/product/spu.ts

@@ -20,8 +20,8 @@ export interface Sku {
   stock?: number // 库存
   stock?: number // 库存
   weight?: number // 商品重量,单位:kg 千克
   weight?: number // 商品重量,单位:kg 千克
   volume?: number // 商品体积,单位:m^3 平米
   volume?: number // 商品体积,单位:m^3 平米
-  firstBrokerageRecord?: number | string // 一级分销的佣金
-  secondBrokerageRecord?: number | string // 二级分销的佣金
+  firstBrokeragePrice?: number | string // 一级分销的佣金
+  secondBrokeragePrice?: number | string // 二级分销的佣金
   salesCount?: number // 商品销量
   salesCount?: number // 商品销量
 }
 }
 
 

+ 8 - 4
src/api/mall/promotion/bargain/bargainActivity.ts

@@ -7,17 +7,16 @@ export interface BargainActivityVO {
   startTime?: Date
   startTime?: Date
   endTime?: Date
   endTime?: Date
   status?: number
   status?: number
-  userSize?: number // 达到该人数,才能砍到低价
+  helpMaxCount?: number // 达到该人数,才能砍到低价
   bargainCount?: number // 最大帮砍次数
   bargainCount?: number // 最大帮砍次数
   totalLimitCount?: number // 最大购买次数
   totalLimitCount?: number // 最大购买次数
   spuId: number
   spuId: number
   skuId: number
   skuId: number
   bargainFirstPrice: number // 砍价起始价格,单位分
   bargainFirstPrice: number // 砍价起始价格,单位分
-  bargainPrice: number // 砍价底价
+  bargainMinPrice: number // 砍价底价
   stock: number // 活动库存
   stock: number // 活动库存
   randomMinPrice?: number // 用户每次砍价的最小金额,单位:分
   randomMinPrice?: number // 用户每次砍价的最小金额,单位:分
   randomMaxPrice?: number // 用户每次砍价的最大金额,单位:分
   randomMaxPrice?: number // 用户每次砍价的最大金额,单位:分
-  successCount?: number // 砍价成功数量
 }
 }
 
 
 // 砍价活动所需属性。选择的商品和属性的时候使用方便使用活动的通用封装
 // 砍价活动所需属性。选择的商品和属性的时候使用方便使用活动的通用封装
@@ -25,7 +24,7 @@ export interface BargainProductVO {
   spuId: number
   spuId: number
   skuId: number
   skuId: number
   bargainFirstPrice: number // 砍价起始价格,单位分
   bargainFirstPrice: number // 砍价起始价格,单位分
-  bargainPrice: number // 砍价底价
+  bargainMinPrice: number // 砍价底价
   stock: number // 活动库存
   stock: number // 活动库存
 }
 }
 
 
@@ -58,6 +57,11 @@ export const updateBargainActivity = async (data: BargainActivityVO) => {
   return await request.put({ url: '/promotion/bargain-activity/update', data })
   return await request.put({ url: '/promotion/bargain-activity/update', data })
 }
 }
 
 
+// 关闭砍价活动
+export const closeBargainActivity = async (id: number) => {
+  return await request.put({ url: '/promotion/bargain-activity/close?id=' + id })
+}
+
 // 删除砍价活动
 // 删除砍价活动
 export const deleteBargainActivity = async (id: number) => {
 export const deleteBargainActivity = async (id: number) => {
   return await request.delete({ url: '/promotion/bargain-activity/delete?id=' + id })
   return await request.delete({ url: '/promotion/bargain-activity/delete?id=' + id })

+ 14 - 0
src/api/mall/promotion/bargain/bargainHelp.ts

@@ -0,0 +1,14 @@
+import request from '@/config/axios'
+
+export interface BargainHelpVO {
+  id: number
+  record: number
+  userId: number
+  reducePrice: number
+  endTime: Date
+}
+
+// 查询砍价记录列表
+export const getBargainHelpPage = async (params) => {
+  return await request.get({ url: `/promotion/bargain-help/page`, params })
+}

+ 19 - 0
src/api/mall/promotion/bargain/bargainRecord.ts

@@ -0,0 +1,19 @@
+import request from '@/config/axios'
+
+export interface BargainRecordVO {
+  id: number
+  activityId: number
+  userId: number
+  spuId: number
+  skuId: number
+  bargainFirstPrice: number
+  bargainPrice: number
+  status: number
+  orderId: number
+  endTime: Date
+}
+
+// 查询砍价记录列表
+export const getBargainRecordPage = async (params) => {
+  return await request.get({ url: `/promotion/bargain-record/page`, params })
+}

+ 5 - 0
src/api/mall/promotion/combination/combinationActivity.ts

@@ -55,6 +55,11 @@ export const updateCombinationActivity = async (data: CombinationActivityVO) =>
   return await request.put({ url: '/promotion/combination-activity/update', data })
   return await request.put({ url: '/promotion/combination-activity/update', data })
 }
 }
 
 
+// 关闭拼团活动
+export const closeCombinationActivity = async (id: number) => {
+  return await request.put({ url: '/promotion/bargain-combination/close?id=' + id })
+}
+
 // 删除拼团活动
 // 删除拼团活动
 export const deleteCombinationActivity = async (id: number) => {
 export const deleteCombinationActivity = async (id: number) => {
   return await request.delete({ url: '/promotion/combination-activity/delete?id=' + id })
   return await request.delete({ url: '/promotion/combination-activity/delete?id=' + id })

+ 33 - 0
src/api/mall/promotion/combination/combinationRecord.ts

@@ -0,0 +1,33 @@
+import request from '@/config/axios'
+
+export interface CombinationRecordVO {
+  id: number // 拼团记录编号
+  activityId: number // 拼团活动编号
+  nickname: string // 用户昵称
+  avatar: string // 用户头像
+  headId: number // 团长编号
+  expireTime: string // 过期时间
+  userSize: number // 可参团人数
+  userCount: number // 已参团人数
+  status: number // 拼团状态
+  spuName: string // 商品名字
+  picUrl: string // 商品图片
+  virtualGroup: boolean // 是否虚拟成团
+  startTime: string // 开始时间 (订单付款后开始的时间)
+  endTime: string // 结束时间(成团时间/失败时间)
+}
+
+// 查询拼团记录列表
+export const getCombinationRecordPage = async (params) => {
+  return await request.get({ url: '/promotion/combination-record/page', params })
+}
+
+// 查询一个拼团的完整拼团记录
+export const getCombinationRecordPageByHeadId = async (params) => {
+  return await request.get({ url: '/promotion/combination-record/page-by-headId', params })
+}
+
+// 获得拼团记录的概要信息
+export const getCombinationRecordSummary = async () => {
+  return await request.get({ url: '/promotion/combination-record/get-summary' })
+}

+ 5 - 0
src/api/mall/promotion/seckill/seckillActivity.ts

@@ -57,6 +57,11 @@ export const updateSeckillActivity = async (data: SeckillActivityVO) => {
   return await request.put({ url: '/promotion/seckill-activity/update', data })
   return await request.put({ url: '/promotion/seckill-activity/update', data })
 }
 }
 
 
+// 关闭秒杀活动
+export const closeSeckillActivity = async (id: number) => {
+  return await request.put({ url: '/promotion/seckill-activity/close?id=' + id })
+}
+
 // 删除秒杀活动
 // 删除秒杀活动
 export const deleteSeckillActivity = async (id: number) => {
 export const deleteSeckillActivity = async (id: number) => {
   return await request.delete({ url: '/promotion/seckill-activity/delete?id=' + id })
   return await request.delete({ url: '/promotion/seckill-activity/delete?id=' + id })

+ 91 - 0
src/api/mall/statistics/member.ts

@@ -0,0 +1,91 @@
+import request from '@/config/axios'
+import dayjs from 'dayjs'
+import { TradeStatisticsComparisonRespVO } from '@/api/mall/statistics/trade'
+import { formatDate } from '@/utils/formatTime'
+
+/** 会员分析 Request VO */
+export interface MemberAnalyseReqVO {
+  times: [dayjs.ConfigType, dayjs.ConfigType]
+}
+
+/** 会员分析 Response VO */
+export interface MemberAnalyseRespVO {
+  visitorCount: number
+  orderUserCount: number
+  payUserCount: number
+  atv: number
+  comparison: TradeStatisticsComparisonRespVO<MemberAnalyseComparisonRespVO>
+}
+
+/** 会员分析对照数据 Response VO */
+export interface MemberAnalyseComparisonRespVO {
+  userCount: number
+  activeUserCount: number
+  rechargeUserCount: number
+}
+
+/** 会员地区统计 Response VO */
+export interface MemberAreaStatisticsRespVO {
+  areaId: number
+  areaName: string
+  userCount: number
+  orderCreateCount: number
+  orderPayCount: number
+  orderPayPrice: number
+}
+
+/** 会员性别统计 Response VO */
+export interface MemberSexStatisticsRespVO {
+  sex: number
+  userCount: number
+}
+
+/** 会员统计 Response VO */
+export interface MemberSummaryRespVO {
+  userCount: number
+  rechargeUserCount: number
+  rechargePrice: number
+  expensePrice: number
+}
+
+/** 会员终端统计 Response VO */
+export interface MemberTerminalStatisticsRespVO {
+  terminal: number
+  userCount: number
+}
+
+// 查询会员统计
+export const getMemberSummary = () => {
+  return request.get<MemberSummaryRespVO>({
+    url: '/statistics/member/summary'
+  })
+}
+
+// 查询会员分析数据
+export const getMemberAnalyse = (params: MemberAnalyseReqVO) => {
+  return request.get<MemberAnalyseRespVO>({
+    url: '/statistics/member/analyse',
+    params: { times: [formatDate(params.times[0]), formatDate(params.times[1])] }
+  })
+}
+
+// 按照省份,查询会员统计列表
+export const getMemberAreaStatisticsList = () => {
+  return request.get<MemberAreaStatisticsRespVO[]>({
+    url: '/statistics/member/get-area-statistics-list'
+  })
+}
+
+// 按照性别,查询会员统计列表
+export const getMemberSexStatisticsList = () => {
+  return request.get<MemberSexStatisticsRespVO[]>({
+    url: '/statistics/member/get-sex-statistics-list'
+  })
+}
+
+// 按照终端,查询会员统计列表
+export const getMemberTerminalStatisticsList = () => {
+  return request.get<MemberTerminalStatisticsRespVO[]>({
+    url: '/statistics/member/get-terminal-statistics-list'
+  })
+}

+ 70 - 0
src/api/mall/statistics/trade.ts

@@ -0,0 +1,70 @@
+import request from '@/config/axios'
+import dayjs from 'dayjs'
+import { formatDate } from '@/utils/formatTime'
+
+/** 交易统计对照 Response VO */
+export interface TradeStatisticsComparisonRespVO<T> {
+  value: T
+  reference: T
+}
+
+/** 交易统计 Response VO */
+export interface TradeSummaryRespVO {
+  yesterdayOrderCount: number
+  monthOrderCount: number
+  yesterdayPayPrice: number
+  monthPayPrice: number
+}
+
+/** 交易状况 Request VO */
+export interface TradeTrendReqVO {
+  times: [dayjs.ConfigType, dayjs.ConfigType]
+}
+
+/** 交易状况统计 Response VO */
+export interface TradeTrendSummaryRespVO {
+  time: string
+  turnover: number
+  orderPayPrice: number
+  rechargePrice: number
+  expensePrice: number
+  balancePrice: number
+  brokerageSettlementPrice: number
+  orderRefundPrice: number
+}
+
+// 查询交易统计
+export const getTradeStatisticsSummary = () => {
+  return request.get<TradeStatisticsComparisonRespVO<TradeSummaryRespVO>>({
+    url: '/statistics/trade/summary'
+  })
+}
+
+// 获得交易状况统计
+export const getTradeTrendSummary = (params: TradeTrendReqVO) => {
+  return request.get<TradeStatisticsComparisonRespVO<TradeTrendSummaryRespVO>>({
+    url: '/statistics/trade/trend/summary',
+    params: formatDateParam(params)
+  })
+}
+
+// 获得交易状况明细
+export const getTradeTrendList = (params: TradeTrendReqVO) => {
+  return request.get<TradeTrendSummaryRespVO[]>({
+    url: '/statistics/trade/trend/list',
+    params: formatDateParam(params)
+  })
+}
+
+// 导出交易状况明细
+export const exportTradeTrend = (params: TradeTrendReqVO) => {
+  return request.download({
+    url: '/statistics/trade/trend/export-excel',
+    params: formatDateParam(params)
+  })
+}
+
+/** 时间参数需要格式化, 确保接口能识别 */
+const formatDateParam = (params: TradeTrendReqVO) => {
+  return { times: [formatDate(params.times[0]), formatDate(params.times[1])] } as TradeTrendReqVO
+}

+ 2 - 2
src/api/mall/trade/config/index.ts

@@ -4,13 +4,13 @@ export interface ConfigVO {
   brokerageEnabled: boolean
   brokerageEnabled: boolean
   brokerageEnabledCondition: number
   brokerageEnabledCondition: number
   brokerageBindMode: number
   brokerageBindMode: number
-  brokeragePostUrls: string
+  brokeragePosterUrls: string
   brokerageFirstPercent: number
   brokerageFirstPercent: number
   brokerageSecondPercent: number
   brokerageSecondPercent: number
   brokerageWithdrawMinPrice: number
   brokerageWithdrawMinPrice: number
   brokerageBankNames: string
   brokerageBankNames: string
   brokerageFrozenDays: number
   brokerageFrozenDays: number
-  brokerageWithdrawType: string
+  brokerageWithdrawTypes: string
 }
 }
 
 
 // 查询交易中心配置详情
 // 查询交易中心配置详情

+ 17 - 5
src/api/mall/trade/order/index.ts

@@ -41,15 +41,22 @@ export interface OrderVO {
   refundPrice?: number | null // 退款金额
   refundPrice?: number | null // 退款金额
   couponId?: number | null // 优惠劵编号
   couponId?: number | null // 优惠劵编号
   couponPrice?: number | null // 优惠劵减免金额
   couponPrice?: number | null // 优惠劵减免金额
+  vipPrice?: number | null // VIP 减免金额
   pointPrice?: number | null // 积分抵扣的金额
   pointPrice?: number | null // 积分抵扣的金额
   receiverAreaName?: string //收件人地区名字
   receiverAreaName?: string //收件人地区名字
   items?: OrderItemRespVO[] // 订单项列表
   items?: OrderItemRespVO[] // 订单项列表
-  // 用户信息
+  // 下单用户信息
   user?: {
   user?: {
     id?: number | null
     id?: number | null
     nickname?: string
     nickname?: string
     avatar?: string
     avatar?: string
   }
   }
+  // 推广用户信息
+  brokerageUser?: {
+    id?: number | null
+    nickname?: string
+    avatar?: string
+  }
   // 订单操作日志
   // 订单操作日志
   logs?: OrderLogRespVO[]
   logs?: OrderLogRespVO[]
 }
 }
@@ -114,21 +121,26 @@ export interface DeliveryVO {
 }
 }
 
 
 // 订单发货
 // 订单发货
-export const delivery = async (data: DeliveryVO) => {
+export const deliveryOrder = async (data: DeliveryVO) => {
   return await request.put({ url: `/trade/order/delivery`, data })
   return await request.put({ url: `/trade/order/delivery`, data })
 }
 }
 
 
 // 订单备注
 // 订单备注
-export const updateRemark = async (data: any) => {
+export const updateOrderRemark = async (data: any) => {
   return await request.put({ url: `/trade/order/update-remark`, data })
   return await request.put({ url: `/trade/order/update-remark`, data })
 }
 }
 
 
 // 订单调价
 // 订单调价
-export const updatePrice = async (data: any) => {
+export const updateOrderPrice = async (data: any) => {
   return await request.put({ url: `/trade/order/update-price`, data })
   return await request.put({ url: `/trade/order/update-price`, data })
 }
 }
 
 
 // 修改订单地址
 // 修改订单地址
-export const updateAddress = async (data: any) => {
+export const updateOrderAddress = async (data: any) => {
   return await request.put({ url: `/trade/order/update-address`, data })
   return await request.put({ url: `/trade/order/update-address`, data })
 }
 }
+
+// 订单核销
+export const pickUpOrder = async (id: number) => {
+  return await request.put({ url: `/trade/order/pick-up?id=${id}` })
+}

+ 19 - 0
src/api/member/config/index.ts

@@ -0,0 +1,19 @@
+import request from '@/config/axios'
+
+export interface ConfigVO {
+  id: number
+  pointTradeDeductEnable: number
+  pointTradeDeductUnitPrice: number
+  pointTradeDeductMaxPrice: number
+  pointTradeGivePoint: number
+}
+
+// 查询积分设置详情
+export const getConfig = async () => {
+  return await request.get({ url: `/member/config/get` })
+}
+
+// 新增修改积分设置
+export const saveConfig = async (data: ConfigVO) => {
+  return await request.put({ url: `/member/config/save`, data })
+}

+ 0 - 19
src/api/member/point/config/index.ts

@@ -1,19 +0,0 @@
-import request from '@/config/axios'
-
-export interface ConfigVO {
-  id: number
-  tradeDeductEnable: number
-  tradeDeductUnitPrice: number
-  tradeDeductMaxPrice: number
-  tradeGivePoint: number
-}
-
-// 查询积分设置详情
-export const getConfig = async () => {
-  return await request.get({ url: `/member/point/config/get` })
-}
-
-// 新增修改积分设置
-export const saveConfig = async (data: ConfigVO) => {
-  return await request.put({ url: `/member/point/config/save`, data })
-}

+ 5 - 4
src/api/member/signin/config/index.ts

@@ -1,10 +1,11 @@
 import request from '@/config/axios'
 import request from '@/config/axios'
 
 
 export interface SignInConfigVO {
 export interface SignInConfigVO {
-  id: number
-  day: number | null
-  point: number | null
-  enable: boolean | null
+  id?: number
+  day?: number
+  point?: number
+  experience?: number
+  status?: number
 }
 }
 
 
 // 查询积分签到规则列表
 // 查询积分签到规则列表

+ 10 - 0
src/api/member/user/index.ts

@@ -41,3 +41,13 @@ export const updateUser = async (data: UserVO) => {
 export const updateUserLevel = async (data: any) => {
 export const updateUserLevel = async (data: any) => {
   return await request.put({ url: `/member/user/update-level`, data })
   return await request.put({ url: `/member/user/update-level`, data })
 }
 }
+
+// 修改会员用户积分
+export const updateUserPoint = async (data: any) => {
+  return await request.put({ url: `/member/user/update-point`, data })
+}
+
+// 修改会员用户余额
+export const updateUserBalance = async (data: any) => {
+  return await request.put({ url: `/member/user/update-balance`, data })
+}

+ 22 - 0
src/api/pay/wallet/index.ts

@@ -0,0 +1,22 @@
+import request from '@/config/axios'
+
+/** 用户钱包查询参数 */
+export interface PayWalletUserReqVO {
+  userId: number
+  userType: number
+}
+/** 钱包 VO */
+export interface WalletVO {
+  id: number
+  userId: number
+  userType: number
+  balance: number
+  totalExpense: number
+  totalRecharge: number
+  freezePrice: number
+}
+
+/** 查询用户钱包详情 */
+export const getWallet = async (params: PayWalletUserReqVO) => {
+  return await request.get<WalletVO>({ url: `/pay/wallet/get`, params })
+}

+ 0 - 8
src/api/system/area/index.ts

@@ -5,14 +5,6 @@ export const getAreaTree = async () => {
   return await request.get({ url: '/system/area/tree' })
   return await request.get({ url: '/system/area/tree' })
 }
 }
 
 
-export const getChildrenArea = async (id: number) => {
-  return await request.get({ url: '/system/area/get-children?id=' + id })
-}
-
-export const getAreaListByIds = async (ids) => {
-  return await request.get({ url: '/system/area/get-by-ids?ids=' + ids })
-}
-
 // 获得 IP 对应的地区名
 // 获得 IP 对应的地区名
 export const getAreaByIp = async (ip: string) => {
 export const getAreaByIp = async (ip: string) => {
   return await request.get({ url: '/system/area/get-by-ip?ip=' + ip })
   return await request.get({ url: '/system/area/get-by-ip?ip=' + ip })

文件差异内容过多而无法显示
+ 0 - 0
src/assets/map/json/china.json


+ 11 - 11
src/components/CountTo/src/CountTo.vue

@@ -11,21 +11,21 @@ const { getPrefixCls } = useDesign()
 const prefixCls = getPrefixCls('count-to')
 const prefixCls = getPrefixCls('count-to')
 
 
 const props = defineProps({
 const props = defineProps({
-  startVal: propTypes.number.def(0),
-  endVal: propTypes.number.def(2021),
-  duration: propTypes.number.def(3000),
-  autoplay: propTypes.bool.def(true),
-  decimals: propTypes.number.validate((value: number) => value >= 0).def(0),
-  decimal: propTypes.string.def('.'),
-  separator: propTypes.string.def(','),
-  prefix: propTypes.string.def(''),
-  suffix: propTypes.string.def(''),
-  useEasing: propTypes.bool.def(true),
+  startVal: propTypes.number.def(0), // 开始播放值
+  endVal: propTypes.number.def(2021), // 最终值
+  duration: propTypes.number.def(3000), // 动画时长
+  autoplay: propTypes.bool.def(true), // 是否自动播放动画, 默认播放
+  decimals: propTypes.number.validate((value: number) => value >= 0).def(0), // 显示的小数位数, 默认不显示小数
+  decimal: propTypes.string.def('.'), // 小数分隔符号, 默认为点
+  separator: propTypes.string.def(','), // 数字每三位的分隔符, 默认为逗号
+  prefix: propTypes.string.def(''), // 前缀, 数值前面显示的内容
+  suffix: propTypes.string.def(''), // 后缀, 数值后面显示的内容
+  useEasing: propTypes.bool.def(true), // 是否使用缓动效果, 默认启用
   easingFn: {
   easingFn: {
     type: Function as PropType<(t: number, b: number, c: number, d: number) => number>,
     type: Function as PropType<(t: number, b: number, c: number, d: number) => number>,
     default(t: number, b: number, c: number, d: number) {
     default(t: number, b: number, c: number, d: number) {
       return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b
       return (c * (-Math.pow(2, (-10 * t) / d) + 1) * 1024) / 1023 + b
-    }
+    } // 缓动函数
   }
   }
 })
 })
 
 

+ 1 - 0
src/components/Icon/src/Icon.vue

@@ -32,6 +32,7 @@ const getIconifyStyle = computed(() => {
   const { color, size } = props
   const { color, size } = props
   return {
   return {
     fontSize: `${size}px`,
     fontSize: `${size}px`,
+    height: '1em',
     color
     color
   }
   }
 })
 })

+ 4 - 3
src/layout/components/Breadcrumb/src/Breadcrumb.vue

@@ -52,10 +52,10 @@ export default defineComponent({
         return (
         return (
           <ElBreadcrumbItem to={{ path: disabled ? '' : v.path }} key={v.name}>
           <ElBreadcrumbItem to={{ path: disabled ? '' : v.path }} key={v.name}>
             {meta?.icon && breadcrumbIcon.value ? (
             {meta?.icon && breadcrumbIcon.value ? (
-              <>
+              <div class="flex items-center">
                 <Icon icon={meta.icon} class="mr-[2px]" svgClass="inline-block"></Icon>
                 <Icon icon={meta.icon} class="mr-[2px]" svgClass="inline-block"></Icon>
                 {t(v?.meta?.title)}
                 {t(v?.meta?.title)}
-              </>
+              </div>
             ) : (
             ) : (
               t(v?.meta?.title)
               t(v?.meta?.title)
             )}
             )}
@@ -114,9 +114,10 @@ $prefix-cls: #{$elNamespace}-breadcrumb;
       }
       }
     }
     }
   }
   }
-
   :deep(&__item):last-child {
   :deep(&__item):last-child {
     .#{$prefix-cls}__inner {
     .#{$prefix-cls}__inner {
+      display: flex;
+      align-items: center;
       color: var(--el-text-color-placeholder);
       color: var(--el-text-color-placeholder);
 
 
       &:hover {
       &:hover {

+ 7 - 7
src/layout/components/useRenderLayout.tsx

@@ -107,7 +107,7 @@ export const useRenderLayout = () => {
               ></ToolHeader>
               ></ToolHeader>
 
 
               {tagsView.value ? (
               {tagsView.value ? (
-                <TagsView class="layout-border__bottom layout-border__top"></TagsView>
+                <TagsView class="layout-border__top layout-border__bottom"></TagsView>
               ) : undefined}
               ) : undefined}
             </div>
             </div>
 
 
@@ -121,13 +121,13 @@ export const useRenderLayout = () => {
   const renderTopLeft = () => {
   const renderTopLeft = () => {
     return (
     return (
       <>
       <>
-        <div class="flex items-center bg-[var(--top-header-bg-color)] relative layout-border__bottom dark:bg-[var(--el-bg-color)]">
+        <div class="relative flex items-center bg-[var(--top-header-bg-color)] layout-border__bottom dark:bg-[var(--el-bg-color)]">
           {logo.value ? <Logo class="custom-hover"></Logo> : undefined}
           {logo.value ? <Logo class="custom-hover"></Logo> : undefined}
 
 
           <ToolHeader class="flex-1"></ToolHeader>
           <ToolHeader class="flex-1"></ToolHeader>
         </div>
         </div>
-        <div class="absolute top-[var(--logo-height)+1px] left-0 w-full h-[calc(100%-1px-var(--logo-height))] flex">
-          <Menu class="!h-full relative layout-border__right"></Menu>
+        <div class="absolute left-0 top-[var(--logo-height)+1px] h-[calc(100%-1px-var(--logo-height))] w-full flex">
+          <Menu class="relative layout-border__right !h-full"></Menu>
           <div
           <div
             class={[
             class={[
               `${prefixCls}-content`,
               `${prefixCls}-content`,
@@ -187,7 +187,7 @@ export const useRenderLayout = () => {
           ]}
           ]}
         >
         >
           {logo.value ? <Logo class="custom-hover"></Logo> : undefined}
           {logo.value ? <Logo class="custom-hover"></Logo> : undefined}
-          <Menu class="flex-1 px-10px h-[var(--top-tool-height)]"></Menu>
+          <Menu class="h-[var(--top-tool-height)] flex-1 px-10px"></Menu>
           <ToolHeader></ToolHeader>
           <ToolHeader></ToolHeader>
         </div>
         </div>
         <div
         <div
@@ -233,12 +233,12 @@ export const useRenderLayout = () => {
   const renderCutMenu = () => {
   const renderCutMenu = () => {
     return (
     return (
       <>
       <>
-        <div class="flex items-center bg-[var(--top-header-bg-color)] relative layout-border__bottom">
+        <div class="relative flex items-center bg-[var(--top-header-bg-color)] layout-border__bottom">
           {logo.value ? <Logo class="custom-hover !pr-15px"></Logo> : undefined}
           {logo.value ? <Logo class="custom-hover !pr-15px"></Logo> : undefined}
 
 
           <ToolHeader class="flex-1"></ToolHeader>
           <ToolHeader class="flex-1"></ToolHeader>
         </div>
         </div>
-        <div class="absolute top-[var(--logo-height)] left-0 w-[calc(100%-2px)] h-[calc(100%-var(--logo-height))] flex">
+        <div class="absolute left-0 top-[var(--logo-height)] h-[calc(100%-var(--logo-height))] w-[calc(100%-2px)] flex">
           <TabMenu></TabMenu>
           <TabMenu></TabMenu>
           <div
           <div
             class={[
             class={[

+ 3 - 1
src/plugins/echarts/index.ts

@@ -18,7 +18,8 @@ import {
   AriaComponent,
   AriaComponent,
   ParallelComponent,
   ParallelComponent,
   LegendComponent,
   LegendComponent,
-  ToolboxComponent
+  ToolboxComponent,
+  VisualMapComponent
 } from 'echarts/components'
 } from 'echarts/components'
 
 
 import { CanvasRenderer } from 'echarts/renderers'
 import { CanvasRenderer } from 'echarts/renderers'
@@ -32,6 +33,7 @@ echarts.use([
   PolarComponent,
   PolarComponent,
   AriaComponent,
   AriaComponent,
   ParallelComponent,
   ParallelComponent,
+  VisualMapComponent,
   BarChart,
   BarChart,
   LineChart,
   LineChart,
   PieChart,
   PieChart,

+ 13 - 15
src/router/modules/remaining.ts

@@ -331,9 +331,8 @@ const remainingRouter: AppRouteRecordRaw[] = [
     ]
     ]
   },
   },
   {
   {
-    path: '/product',
+    path: '/mall/product', // 商品中心
     component: Layout,
     component: Layout,
-    name: 'Product',
     meta: {
     meta: {
       hidden: true
       hidden: true
     },
     },
@@ -347,12 +346,12 @@ const remainingRouter: AppRouteRecordRaw[] = [
           hidden: true,
           hidden: true,
           canTo: true,
           canTo: true,
           icon: 'ep:edit',
           icon: 'ep:edit',
-          title: '添加商品',
-          activeMenu: '/product/product-spu'
+          title: '商品添加',
+          activeMenu: '/mall/product/spu'
         }
         }
       },
       },
       {
       {
-        path: 'spu/edit/:spuId(\\d+)',
+        path: 'spu/edit/:id(\\d+)',
         component: () => import('@/views/mall/product/spu/form/index.vue'),
         component: () => import('@/views/mall/product/spu/form/index.vue'),
         name: 'ProductSpuEdit',
         name: 'ProductSpuEdit',
         meta: {
         meta: {
@@ -360,12 +359,12 @@ const remainingRouter: AppRouteRecordRaw[] = [
           hidden: true,
           hidden: true,
           canTo: true,
           canTo: true,
           icon: 'ep:edit',
           icon: 'ep:edit',
-          title: '编辑商品',
-          activeMenu: '/product/product-spu'
+          title: '商品编辑',
+          activeMenu: '/mall/product/spu'
         }
         }
       },
       },
       {
       {
-        path: 'spu/detail/:spuId(\\d+)',
+        path: 'spu/detail/:id(\\d+)',
         component: () => import('@/views/mall/product/spu/form/index.vue'),
         component: () => import('@/views/mall/product/spu/form/index.vue'),
         name: 'ProductSpuDetail',
         name: 'ProductSpuDetail',
         meta: {
         meta: {
@@ -374,7 +373,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
           canTo: true,
           canTo: true,
           icon: 'ep:view',
           icon: 'ep:view',
           title: '商品详情',
           title: '商品详情',
-          activeMenu: '/product/product-spu'
+          activeMenu: '/mall/product/spu'
         }
         }
       },
       },
       {
       {
@@ -393,24 +392,23 @@ const remainingRouter: AppRouteRecordRaw[] = [
     ]
     ]
   },
   },
   {
   {
-    path: '/trade',
+    path: '/mall/trade', // 交易中心
     component: Layout,
     component: Layout,
-    name: 'Order',
     meta: {
     meta: {
       hidden: true
       hidden: true
     },
     },
     children: [
     children: [
       {
       {
-        path: 'order/detail/:orderId(\\d+)',
+        path: 'order/detail/:id(\\d+)',
         component: () => import('@/views/mall/trade/order/detail/index.vue'),
         component: () => import('@/views/mall/trade/order/detail/index.vue'),
         name: 'TradeOrderDetail',
         name: 'TradeOrderDetail',
-        meta: { title: '订单详情', icon: '', activeMenu: '/trade/trade/order' }
+        meta: { title: '订单详情', icon: 'ep:view', activeMenu: '/mall/trade/order' }
       },
       },
       {
       {
-        path: 'after-sale/detail/:orderId(\\d+)',
+        path: 'after-sale/detail/:id(\\d+)',
         component: () => import('@/views/mall/trade/afterSale/detail/index.vue'),
         component: () => import('@/views/mall/trade/afterSale/detail/index.vue'),
         name: 'TradeAfterSaleDetail',
         name: 'TradeAfterSaleDetail',
-        meta: { title: '退款详情', icon: '', activeMenu: '/trade/trade/after-sale' }
+        meta: { title: '退款详情', icon: 'ep:view', activeMenu: '/mall/trade/after-sale' }
       }
       }
     ]
     ]
   },
   },

+ 80 - 24
src/utils/constants.ts

@@ -4,12 +4,20 @@
  * 枚举类
  * 枚举类
  */
  */
 
 
+// ========== COMMON 模块 ==========
 // 全局通用状态枚举
 // 全局通用状态枚举
 export const CommonStatusEnum = {
 export const CommonStatusEnum = {
   ENABLE: 0, // 开启
   ENABLE: 0, // 开启
   DISABLE: 1 // 禁用
   DISABLE: 1 // 禁用
 }
 }
 
 
+// 全局用户类型枚举
+export const UserTypeEnum = {
+  MEMBER: 1, // 会员
+  ADMIN: 2 // 管理员
+}
+
+// ========== SYSTEM 模块 ==========
 /**
 /**
  * 菜单的类型枚举
  * 菜单的类型枚举
  */
  */
@@ -38,6 +46,25 @@ export const SystemDataScopeEnum = {
   DEPT_SELF: 5 // 仅本人数据权限
   DEPT_SELF: 5 // 仅本人数据权限
 }
 }
 
 
+/**
+ * 用户的社交平台的类型枚举
+ */
+export const SystemUserSocialTypeEnum = {
+  DINGTALK: {
+    title: '钉钉',
+    type: 20,
+    source: 'dingtalk',
+    img: 'https://s1.ax1x.com/2022/05/22/OzMDRs.png'
+  },
+  WECHAT_ENTERPRISE: {
+    title: '企业微信',
+    type: 30,
+    source: 'wechat_enterprise',
+    img: 'https://s1.ax1x.com/2022/05/22/OzMrzn.png'
+  }
+}
+
+// ========== INFRA 模块 ==========
 /**
 /**
  * 代码生成模板类型
  * 代码生成模板类型
  */
  */
@@ -65,24 +92,7 @@ export const InfraApiErrorLogProcessStatusEnum = {
   IGNORE: 2 // 已忽略
   IGNORE: 2 // 已忽略
 }
 }
 
 
-/**
- * 用户的社交平台的类型枚举
- */
-export const SystemUserSocialTypeEnum = {
-  DINGTALK: {
-    title: '钉钉',
-    type: 20,
-    source: 'dingtalk',
-    img: 'https://s1.ax1x.com/2022/05/22/OzMDRs.png'
-  },
-  WECHAT_ENTERPRISE: {
-    title: '企业微信',
-    type: 30,
-    source: 'wechat_enterprise',
-    img: 'https://s1.ax1x.com/2022/05/22/OzMrzn.png'
-  }
-}
-
+// ========== PAY 模块 ==========
 /**
 /**
  * 支付渠道枚举
  * 支付渠道枚举
  */
  */
@@ -177,6 +187,7 @@ export const PayOrderStatusEnum = {
   }
   }
 }
 }
 
 
+// ========== MALL - 商品模块 ==========
 /**
 /**
  * 商品 SPU 状态
  * 商品 SPU 状态
  */
  */
@@ -195,6 +206,7 @@ export const ProductSpuStatusEnum = {
   }
   }
 }
 }
 
 
+// ========== MALL - 营销模块 ==========
 /**
 /**
  * 优惠劵模板的有限期类型的枚举
  * 优惠劵模板的有限期类型的枚举
  */
  */
@@ -273,17 +285,22 @@ export const PromotionDiscountTypeEnum = {
   }
   }
 }
 }
 
 
+// ========== MALL - 交易模块 ==========
 /**
 /**
  * 分销关系绑定模式枚举
  * 分销关系绑定模式枚举
  */
  */
 export const BrokerageBindModeEnum = {
 export const BrokerageBindModeEnum = {
   ANYTIME: {
   ANYTIME: {
-    mode: 0,
-    name: '没有推广人'
+    mode: 1,
+    name: '首次绑定'
   },
   },
   REGISTER: {
   REGISTER: {
-    mode: 1,
-    name: '新用户'
+    mode: 2,
+    name: '注册绑定'
+  },
+  OVERRIDE: {
+    mode: 3,
+    name: '覆盖绑定'
   }
   }
 }
 }
 /**
 /**
@@ -291,11 +308,11 @@ export const BrokerageBindModeEnum = {
  */
  */
 export const BrokerageEnabledConditionEnum = {
 export const BrokerageEnabledConditionEnum = {
   ALL: {
   ALL: {
-    condition: 0,
+    condition: 1,
     name: '人人分销'
     name: '人人分销'
   },
   },
   ADMIN: {
   ADMIN: {
-    condition: 1,
+    condition: 2,
     name: '指定分销'
     name: '指定分销'
   }
   }
 }
 }
@@ -358,3 +375,42 @@ export const BrokerageWithdrawTypeEnum = {
     name: '支付宝'
     name: '支付宝'
   }
   }
 }
 }
+
+/**
+ * 配送方式枚举
+ */
+export const DeliveryTypeEnum = {
+  EXPRESS: {
+    type: 1,
+    name: '快递发货'
+  },
+  PICK_UP: {
+    type: 2,
+    name: '到店自提'
+  }
+}
+/**
+ * 交易订单 - 状态
+ */
+export const TradeOrderStatusEnum = {
+  UNPAID: {
+    status: 0,
+    name: '待支付'
+  },
+  UNDELIVERED: {
+    status: 10,
+    name: '待发货'
+  },
+  DELIVERED: {
+    status: 20,
+    name: '已发货'
+  },
+  COMPLETED: {
+    status: 30,
+    name: '已完成'
+  },
+  CANCELED: {
+    status: 40,
+    name: '已取消'
+  }
+}

+ 4 - 2
src/utils/dict.ts

@@ -24,7 +24,7 @@ export const getDictOptions = (dictType: string) => {
   return dictStore.getDictByType(dictType) || []
   return dictStore.getDictByType(dictType) || []
 }
 }
 
 
-export const getIntDictOptions = (dictType: string) => {
+export const getIntDictOptions = (dictType: string): DictDataType[] => {
   const dictOption: DictDataType[] = []
   const dictOption: DictDataType[] = []
   const dictOptions: DictDataType[] = getDictOptions(dictType)
   const dictOptions: DictDataType[] = getDictOptions(dictType)
   dictOptions.forEach((dict: DictDataType) => {
   dictOptions.forEach((dict: DictDataType) => {
@@ -180,5 +180,7 @@ export enum DICT_TYPE {
   PROMOTION_COUPON_STATUS = 'promotion_coupon_status', // 优惠劵的状态
   PROMOTION_COUPON_STATUS = 'promotion_coupon_status', // 优惠劵的状态
   PROMOTION_COUPON_TAKE_TYPE = 'promotion_coupon_take_type', // 优惠劵的领取方式
   PROMOTION_COUPON_TAKE_TYPE = 'promotion_coupon_take_type', // 优惠劵的领取方式
   PROMOTION_ACTIVITY_STATUS = 'promotion_activity_status', // 优惠活动的状态
   PROMOTION_ACTIVITY_STATUS = 'promotion_activity_status', // 优惠活动的状态
-  PROMOTION_CONDITION_TYPE = 'promotion_condition_type' // 营销的条件类型枚举
+  PROMOTION_CONDITION_TYPE = 'promotion_condition_type', // 营销的条件类型枚举
+  PROMOTION_BARGAIN_RECORD_STATUS = 'promotion_bargain_record_status', // 砍价记录的状态
+  PROMOTION_COMBINATION_RECORD_STATUS = 'promotion_combination_record_status' // 拼团记录的状态
 }
 }

+ 117 - 1
src/utils/formatTime.ts

@@ -1,5 +1,56 @@
 import dayjs from 'dayjs'
 import dayjs from 'dayjs'
 
 
+/**
+ * 日期快捷选项适用于 el-date-picker
+ */
+export const defaultShortcuts = [
+  {
+    text: '今天',
+    value: () => {
+      return new Date()
+    }
+  },
+  {
+    text: '昨天',
+    value: () => {
+      const date = new Date()
+      date.setTime(date.getTime() - 3600 * 1000 * 24)
+      return [date, date]
+    }
+  },
+  {
+    text: '最近七天',
+    value: () => {
+      const date = new Date()
+      date.setTime(date.getTime() - 3600 * 1000 * 24 * 7)
+      return [date, new Date()]
+    }
+  },
+  {
+    text: '最近 30 天',
+    value: () => {
+      const date = new Date()
+      date.setTime(date.getTime() - 3600 * 1000 * 24 * 30)
+      return [date, new Date()]
+    }
+  },
+  {
+    text: '本月',
+    value: () => {
+      const date = new Date()
+      date.setDate(1) // 设置为当前月的第一天
+      return [date, new Date()]
+    }
+  },
+  {
+    text: '今年',
+    value: () => {
+      const date = new Date()
+      return [new Date(`${date.getFullYear()}-01-01`), date]
+    }
+  }
+]
+
 /**
 /**
  * 时间日期转换
  * 时间日期转换
  * @param date 当前时间,new Date() 格式
  * @param date 当前时间,new Date() 格式
@@ -11,7 +62,7 @@ import dayjs from 'dayjs'
  * @description format 季度 + 星期 + 几周:"YYYY-mm-dd HH:MM:SS WWW QQQQ ZZZ"
  * @description format 季度 + 星期 + 几周:"YYYY-mm-dd HH:MM:SS WWW QQQQ ZZZ"
  * @returns 返回拼接后的时间字符串
  * @returns 返回拼接后的时间字符串
  */
  */
-export function formatDate(date: Date | number, format?: string): string {
+export function formatDate(date: dayjs.ConfigType, format?: string): string {
   // 日期不存在,则返回空
   // 日期不存在,则返回空
   if (!date) {
   if (!date) {
     return ''
     return ''
@@ -221,3 +272,68 @@ export function convertDate(param: Date | string) {
   }
   }
   return param
   return param
 }
 }
+
+/**
+ * 指定的两个日期, 是否为同一天
+ * @param a 日期 A
+ * @param b 日期 B
+ */
+export function isSameDay(a: dayjs.ConfigType, b: dayjs.ConfigType): boolean {
+  if (!a || !b) return false
+
+  const aa = dayjs(a)
+  const bb = dayjs(b)
+  return aa.year() == bb.year() && aa.month() == bb.month() && aa.day() == bb.day()
+}
+
+/**
+ * 获取一天的开始时间、截止时间
+ * @param date 日期
+ * @param days 天数
+ */
+export function getDayRange(
+  date: dayjs.ConfigType,
+  days: number
+): [dayjs.ConfigType, dayjs.ConfigType] {
+  const day = dayjs(date).add(days, 'd')
+  return getDateRange(day, day)
+}
+
+/**
+ * 获取最近7天的开始时间、截止时间
+ */
+export function getLast7Days(): [dayjs.ConfigType, dayjs.ConfigType] {
+  const lastWeekDay = dayjs().subtract(7, 'd')
+  const yesterday = dayjs().subtract(1, 'd')
+  return getDateRange(lastWeekDay, yesterday)
+}
+
+/**
+ * 获取最近30天的开始时间、截止时间
+ */
+export function getLast30Days(): [dayjs.ConfigType, dayjs.ConfigType] {
+  const lastMonthDay = dayjs().subtract(30, 'd')
+  const yesterday = dayjs().subtract(1, 'd')
+  return getDateRange(lastMonthDay, yesterday)
+}
+
+/**
+ * 获取最近1年的开始时间、截止时间
+ */
+export function getLast1Year(): [dayjs.ConfigType, dayjs.ConfigType] {
+  const lastYearDay = dayjs().subtract(1, 'y')
+  const yesterday = dayjs().subtract(1, 'd')
+  return getDateRange(lastYearDay, yesterday)
+}
+
+/**
+ * 获取指定日期的开始时间、截止时间
+ * @param beginDate 开始日期
+ * @param endDate 截止日期
+ */
+export function getDateRange(
+  beginDate: dayjs.ConfigType,
+  endDate: dayjs.ConfigType
+): [dayjs.ConfigType, dayjs.ConfigType] {
+  return [dayjs(beginDate).startOf('d'), dayjs(endDate).endOf('d')]
+}

+ 4 - 9
src/utils/formatter.ts

@@ -1,12 +1,7 @@
-import { fenToYuan } from '@/utils'
-import { TableColumnCtx } from 'element-plus'
+import { floatToFixed2 } from '@/utils'
 
 
 // 格式化金额【分转元】
 // 格式化金额【分转元】
-export const fenToYuanFormat = (
-  row: any,
-  column: TableColumnCtx<any>,
-  cellValue: any,
-  index: number
-) => {
-  return `¥${fenToYuan(cellValue)}`
+// @ts-ignore
+export const fenToYuanFormat = (_, __, cellValue: any, ___) => {
+  return `¥${floatToFixed2(cellValue)}`
 }
 }

+ 3 - 3
src/utils/index.ts

@@ -224,12 +224,12 @@ export const convertToInteger = (num: number | string | undefined): number => {
  * 元转分
  * 元转分
  */
  */
 export const yuanToFen = (amount: string | number): number => {
 export const yuanToFen = (amount: string | number): number => {
-  return Math.round(Number(amount) * 100)
+  return convertToInteger(amount)
 }
 }
 
 
 /**
 /**
  * 分转元
  * 分转元
  */
  */
-export const fenToYuan = (amount: string | number): number => {
-  return Number((Number(amount) / 100).toFixed(2))
+export const fenToYuan = (price: string | number): number => {
+  return formatToFraction(price)
 }
 }

+ 4 - 3
src/utils/tree.ts

@@ -13,7 +13,8 @@ export const defaultProps = {
   children: 'children',
   children: 'children',
   label: 'name',
   label: 'name',
   value: 'id',
   value: 'id',
-  isLeaf: 'leaf'
+  isLeaf: 'leaf',
+  emitPath: false // 用于 cascader 组件:在选中节点改变时,是否返回由该节点所在的各级菜单的值所组成的数组,若设置 false,则只返回该节点的值
 }
 }
 
 
 const getConfig = (config: Partial<TreeHelperConfig>) => Object.assign({}, DEFAULT_CONFIG, config)
 const getConfig = (config: Partial<TreeHelperConfig>) => Object.assign({}, DEFAULT_CONFIG, config)
@@ -377,10 +378,10 @@ export const treeToString = (tree: any[], nodeId) => {
   function performAThoroughValidation(arr) {
   function performAThoroughValidation(arr) {
     for (const item of arr) {
     for (const item of arr) {
       if (item.id === nodeId) {
       if (item.id === nodeId) {
-        str += `/${item.name}`
+        str += ` / ${item.name}`
         return true
         return true
       } else if (typeof item.children !== 'undefined' && item.children.length !== 0) {
       } else if (typeof item.children !== 'undefined' && item.children.length !== 0) {
-        str += `/${item.name}`
+        str += ` / ${item.name}`
         if (performAThoroughValidation(item.children)) {
         if (performAThoroughValidation(item.children)) {
           return true
           return true
         }
         }

+ 5 - 5
src/views/mall/product/category/index.vue

@@ -35,14 +35,14 @@
   <!-- 列表 -->
   <!-- 列表 -->
   <ContentWrap>
   <ContentWrap>
     <el-table v-loading="loading" :data="list" row-key="id" default-expand-all>
     <el-table v-loading="loading" :data="list" row-key="id" default-expand-all>
-      <el-table-column label="分类名称" prop="name" sortable />
-      <el-table-column label="移动端分类图" align="center" prop="picUrl">
+      <el-table-column label="名称" min-width="240" prop="name" sortable />
+      <el-table-column label="分类图" align="center" min-width="80" prop="picUrl">
         <template #default="scope">
         <template #default="scope">
-          <img v-if="scope.row.picUrl" :src="scope.row.picUrl" alt="移动端分类图" class="h-30px" />
+          <img v-if="scope.row.picUrl" :src="scope.row.picUrl" alt="移动端分类图" class="h-36px" />
         </template>
         </template>
       </el-table-column>
       </el-table-column>
-      <el-table-column label="分类排序" align="center" prop="sort" />
-      <el-table-column label="开启状态" align="center" prop="status">
+      <el-table-column label="排序" align="center" min-width="150" prop="sort" />
+      <el-table-column label="状态" align="center" min-width="150" prop="status">
         <template #default="scope">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
           <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
         </template>
         </template>

+ 8 - 9
src/views/mall/product/comment/index.vue

@@ -59,9 +59,8 @@
   <!-- 列表 -->
   <!-- 列表 -->
   <ContentWrap>
   <ContentWrap>
     <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="false">
     <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="false">
-      <el-table-column label="评论编号" align="center" prop="id" min-width="60" />
-      <el-table-column label="用户名称" align="center" prop="userNickname" width="80" />
-      <el-table-column label="商品信息" align="center" min-width="300">
+      <el-table-column label="评论编号" align="center" prop="id" min-width="50" />
+      <el-table-column label="商品信息" align="center" min-width="400">
         <template #default="scope">
         <template #default="scope">
           <div class="row flex items-center gap-x-4px">
           <div class="row flex items-center gap-x-4px">
             <el-image
             <el-image
@@ -82,10 +81,10 @@
           </div>
           </div>
         </template>
         </template>
       </el-table-column>
       </el-table-column>
-      <el-table-column label="评分星级" align="center" prop="scores" width="80" />
-      <el-table-column label="描述星级" align="center" prop="descriptionScores" width="80" />
-      <el-table-column label="服务星级" align="center" prop="benefitScores" width="80" />
-      <el-table-column label="评论内容" align="center" prop="content" min-width="80">
+      <el-table-column label="用户名称" align="center" prop="userNickname" width="100" />
+      <el-table-column label="商品评分" align="center" prop="descriptionScores" width="90" />
+      <el-table-column label="服务评分" align="center" prop="benefitScores" width="90" />
+      <el-table-column label="评论内容" align="center" prop="content" min-width="210">
         <template #default="scope">
         <template #default="scope">
           <p>{{ scope.row.content }}</p>
           <p>{{ scope.row.content }}</p>
           <div class="flex justify-center gap-x-4px">
           <div class="flex justify-center gap-x-4px">
@@ -105,7 +104,7 @@
         label="回复内容"
         label="回复内容"
         align="center"
         align="center"
         prop="replyContent"
         prop="replyContent"
-        min-width="100"
+        min-width="250"
         show-overflow-tooltip
         show-overflow-tooltip
       />
       />
       <el-table-column
       <el-table-column
@@ -113,7 +112,7 @@
         align="center"
         align="center"
         prop="createTime"
         prop="createTime"
         :formatter="dateFormatter"
         :formatter="dateFormatter"
-        width="170"
+        width="180"
       />
       />
       <el-table-column label="是否展示" align="center" width="80px">
       <el-table-column label="是否展示" align="center" width="80px">
         <template #default="scope">
         <template #default="scope">

+ 3 - 3
src/views/mall/product/property/index.vue

@@ -53,8 +53,8 @@
   <!-- 列表 -->
   <!-- 列表 -->
   <ContentWrap>
   <ContentWrap>
     <el-table v-loading="loading" :data="list">
     <el-table v-loading="loading" :data="list">
-      <el-table-column align="center" label="编号" prop="id" />
-      <el-table-column align="center" label="名称" prop="name" />
+      <el-table-column align="center" label="编号" min-width="60" prop="id" />
+      <el-table-column align="center" label="属性名称" prop="name" min-width="150" />
       <el-table-column :show-overflow-tooltip="true" align="center" label="备注" prop="remark" />
       <el-table-column :show-overflow-tooltip="true" align="center" label="备注" prop="remark" />
       <el-table-column
       <el-table-column
         :formatter="dateFormatter"
         :formatter="dateFormatter"
@@ -165,7 +165,7 @@ const handleDelete = async (id: number) => {
 
 
 /** 跳转商品属性列表 */
 /** 跳转商品属性列表 */
 const goValueList = (id: number) => {
 const goValueList = (id: number) => {
-  push({ path: '/product/property/value/' + id })
+  push({ name: 'ProductPropertyValue', params: { propertyId: id } })
 }
 }
 
 
 /** 初始化 **/
 /** 初始化 **/

+ 2 - 2
src/views/mall/product/property/value/index.vue

@@ -45,8 +45,8 @@
   <!-- 列表 -->
   <!-- 列表 -->
   <ContentWrap>
   <ContentWrap>
     <el-table v-loading="loading" :data="list">
     <el-table v-loading="loading" :data="list">
-      <el-table-column label="编号" align="center" prop="id" />
-      <el-table-column label="名称" align="center" prop="name" :show-overflow-tooltip="true" />
+      <el-table-column label="编号" align="center" min-width="60" prop="id" />
+      <el-table-column label="属性值名称" align="center" min-width="150" prop="name" />
       <el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
       <el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
       <el-table-column
       <el-table-column
         label="创建时间"
         label="创建时间"

+ 10 - 10
src/views/mall/product/spu/components/SkuList.vue

@@ -80,7 +80,7 @@
       <el-table-column align="center" label="一级返佣(元)" min-width="168">
       <el-table-column align="center" label="一级返佣(元)" min-width="168">
         <template #default="{ row }">
         <template #default="{ row }">
           <el-input-number
           <el-input-number
-            v-model="row.firstBrokerageRecord"
+            v-model="row.firstBrokeragePrice"
             :min="0"
             :min="0"
             :precision="2"
             :precision="2"
             :step="0.1"
             :step="0.1"
@@ -91,7 +91,7 @@
       <el-table-column align="center" label="二级返佣(元)" min-width="168">
       <el-table-column align="center" label="二级返佣(元)" min-width="168">
         <template #default="{ row }">
         <template #default="{ row }">
           <el-input-number
           <el-input-number
-            v-model="row.secondBrokerageRecord"
+            v-model="row.secondBrokeragePrice"
             :min="0"
             :min="0"
             :precision="2"
             :precision="2"
             :step="0.1"
             :step="0.1"
@@ -181,12 +181,12 @@
     <template v-if="formData!.subCommissionType">
     <template v-if="formData!.subCommissionType">
       <el-table-column align="center" label="一级返佣(元)" min-width="80">
       <el-table-column align="center" label="一级返佣(元)" min-width="80">
         <template #default="{ row }">
         <template #default="{ row }">
-          {{ row.firstBrokerageRecord }}
+          {{ row.firstBrokeragePrice }}
         </template>
         </template>
       </el-table-column>
       </el-table-column>
       <el-table-column align="center" label="二级返佣(元)" min-width="80">
       <el-table-column align="center" label="二级返佣(元)" min-width="80">
         <template #default="{ row }">
         <template #default="{ row }">
-          {{ row.secondBrokerageRecord }}
+          {{ row.secondBrokeragePrice }}
         </template>
         </template>
       </el-table-column>
       </el-table-column>
     </template>
     </template>
@@ -295,8 +295,8 @@ const skuList = ref<Sku[]>([
     stock: 0, // 库存
     stock: 0, // 库存
     weight: 0, // 商品重量
     weight: 0, // 商品重量
     volume: 0, // 商品体积
     volume: 0, // 商品体积
-    firstBrokerageRecord: 0, // 一级分销的佣金
-    secondBrokerageRecord: 0 // 二级分销的佣金
+    firstBrokeragePrice: 0, // 一级分销的佣金
+    secondBrokeragePrice: 0 // 二级分销的佣金
   }
   }
 ]) // 批量添加时的临时数据
 ]) // 批量添加时的临时数据
 
 
@@ -415,8 +415,8 @@ const generateTableData = (propertyList: any[]) => {
       stock: 0,
       stock: 0,
       weight: 0,
       weight: 0,
       volume: 0,
       volume: 0,
-      firstBrokerageRecord: 0,
-      secondBrokerageRecord: 0
+      firstBrokeragePrice: 0,
+      secondBrokeragePrice: 0
     }
     }
     // 如果存在属性相同的 sku 则不做处理
     // 如果存在属性相同的 sku 则不做处理
     const index = formData.value!.skus!.findIndex(
     const index = formData.value!.skus!.findIndex(
@@ -491,8 +491,8 @@ watch(
           stock: 0,
           stock: 0,
           weight: 0,
           weight: 0,
           volume: 0,
           volume: 0,
-          firstBrokerageRecord: 0,
-          secondBrokerageRecord: 0
+          firstBrokeragePrice: 0,
+          secondBrokeragePrice: 0
         }
         }
       ]
       ]
     }
     }

+ 2 - 1
src/views/mall/product/spu/components/SkuTableSelect.vue

@@ -25,7 +25,7 @@
       </el-table-column>
       </el-table-column>
       <el-table-column align="center" label="销售价(元)" min-width="80">
       <el-table-column align="center" label="销售价(元)" min-width="80">
         <template #default="{ row }">
         <template #default="{ row }">
-          {{ row.price }}
+          {{ fenToYuan(row.price) }}
         </template>
         </template>
       </el-table-column>
       </el-table-column>
     </el-table>
     </el-table>
@@ -36,6 +36,7 @@
 import { ElTable } from 'element-plus'
 import { ElTable } from 'element-plus'
 import * as ProductSpuApi from '@/api/mall/product/spu'
 import * as ProductSpuApi from '@/api/mall/product/spu'
 import { propTypes } from '@/utils/propTypes'
 import { propTypes } from '@/utils/propTypes'
+import { fenToYuan } from '@/utils'
 
 
 defineOptions({ name: 'SkuTableSelect' })
 defineOptions({ name: 'SkuTableSelect' })
 
 

+ 18 - 33
src/views/mall/product/spu/form/BasicInfoForm.vue

@@ -15,15 +15,14 @@
       </el-col>
       </el-col>
       <el-col :span="12">
       <el-col :span="12">
         <el-form-item label="商品分类" prop="categoryId">
         <el-form-item label="商品分类" prop="categoryId">
-          <el-tree-select
+          <el-cascader
             v-model="formData.categoryId"
             v-model="formData.categoryId"
-            :data="categoryList"
+            :options="categoryList"
             :props="defaultProps"
             :props="defaultProps"
-            check-strictly
             class="w-1/1"
             class="w-1/1"
-            node-key="id"
+            clearable
             placeholder="请选择商品分类"
             placeholder="请选择商品分类"
-            @change="categoryNodeClick"
+            filterable
           />
           />
         </el-form-item>
         </el-form-item>
       </el-col>
       </el-col>
@@ -74,8 +73,6 @@
               :value="item.id"
               :value="item.id"
             />
             />
           </el-select>
           </el-select>
-          <!-- TODO 可能情况:善品录入后选择运费发现下拉选择中没有对应的模版 这里需不需要做添加运费模版后选择的功能 -->
-          <!-- <el-button class="ml-20px">运费模板</el-button>-->
         </el-form-item>
         </el-form-item>
       </el-col>
       </el-col>
       <el-col :span="12">
       <el-col :span="12">
@@ -102,7 +99,7 @@
         <el-form-item label="分销类型" props="subCommissionType">
         <el-form-item label="分销类型" props="subCommissionType">
           <el-radio-group v-model="formData.subCommissionType" @change="changeSubCommissionType">
           <el-radio-group v-model="formData.subCommissionType" @change="changeSubCommissionType">
             <el-radio :label="false">默认设置</el-radio>
             <el-radio :label="false">默认设置</el-radio>
-            <el-radio :label="true" class="radio">自行设置</el-radio>
+            <el-radio :label="true" class="radio">单独设置</el-radio>
           </el-radio-group>
           </el-radio-group>
         </el-form-item>
         </el-form-item>
       </el-col>
       </el-col>
@@ -117,7 +114,7 @@
           />
           />
         </el-form-item>
         </el-form-item>
         <el-form-item v-if="formData.specType" label="商品属性">
         <el-form-item v-if="formData.specType" label="商品属性">
-          <el-button class="mb-10px mr-15px" @click="attributesAddFormRef.open">添加规格</el-button>
+          <el-button class="mb-10px mr-15px" @click="attributesAddFormRef.open">添加属性</el-button>
           <ProductAttributes :propertyList="propertyList" @success="generateSkus" />
           <ProductAttributes :propertyList="propertyList" @success="generateSkus" />
         </el-form-item>
         </el-form-item>
         <template v-if="formData.specType && propertyList.length > 0">
         <template v-if="formData.specType && propertyList.length > 0">
@@ -139,7 +136,7 @@
 
 
   <!-- 情况二:详情 -->
   <!-- 情况二:详情 -->
   <Descriptions v-if="isDetail" :data="formData" :schema="allSchemas.detailSchema">
   <Descriptions v-if="isDetail" :data="formData" :schema="allSchemas.detailSchema">
-    <template #categoryId="{ row }"> {{ categoryString(row.categoryId) }}</template>
+    <template #categoryId="{ row }"> {{ formatCategoryName(row.categoryId) }}</template>
     <template #brandId="{ row }">
     <template #brandId="{ row }">
       {{ brandList.find((item) => item.id === row.brandId)?.name }}
       {{ brandList.find((item) => item.id === row.brandId)?.name }}
     </template>
     </template>
@@ -150,7 +147,7 @@
       {{ row.specType ? '多规格' : '单规格' }}
       {{ row.specType ? '多规格' : '单规格' }}
     </template>
     </template>
     <template #subCommissionType="{ row }">
     <template #subCommissionType="{ row }">
-      {{ row.subCommissionType ? '自行设置' : '默认设置' }}
+      {{ row.subCommissionType ? '单独设置' : '默认设置' }}
     </template>
     </template>
     <template #picUrl="{ row }">
     <template #picUrl="{ row }">
       <el-image :src="row.picUrl" class="h-60px w-60px" @click="imagePreview(row.picUrl)" />
       <el-image :src="row.picUrl" class="h-60px w-60px" @click="imagePreview(row.picUrl)" />
@@ -206,17 +203,17 @@ const ruleConfig: RuleConfig[] = [
   {
   {
     name: 'price',
     name: 'price',
     rule: (arg) => arg >= 0.01,
     rule: (arg) => arg >= 0.01,
-    message: '商品销售价格必须大于等于 0.01 !!!'
+    message: '商品销售价格必须大于等于 0.01 !!!'
   },
   },
   {
   {
     name: 'marketPrice',
     name: 'marketPrice',
     rule: (arg) => arg >= 0.01,
     rule: (arg) => arg >= 0.01,
-    message: '商品市场价格必须大于等于 0.01 !!!'
+    message: '商品市场价格必须大于等于 0.01 !!!'
   },
   },
   {
   {
     name: 'costPrice',
     name: 'costPrice',
     rule: (arg) => arg >= 0.01,
     rule: (arg) => arg >= 0.01,
-    message: '商品成本价格必须大于等于 0.01 !!!'
+    message: '商品成本价格必须大于等于 0.01 !!!'
   }
   }
 ]
 ]
 
 
@@ -332,8 +329,8 @@ defineExpose({ validate })
 const changeSubCommissionType = () => {
 const changeSubCommissionType = () => {
   // 默认为零,类型切换后也要重置为零
   // 默认为零,类型切换后也要重置为零
   for (const item of formData.skus) {
   for (const item of formData.skus) {
-    item.firstBrokerageRecord = 0
-    item.secondBrokerageRecord = 0
+    item.firstBrokeragePrice = 0
+    item.secondBrokeragePrice = 0
   }
   }
 }
 }
 
 
@@ -352,30 +349,18 @@ const onChangeSpec = () => {
       stock: 0,
       stock: 0,
       weight: 0,
       weight: 0,
       volume: 0,
       volume: 0,
-      firstBrokerageRecord: 0,
-      secondBrokerageRecord: 0
+      firstBrokeragePrice: 0,
+      secondBrokeragePrice: 0
     }
     }
   ]
   ]
 }
 }
 
 
 const categoryList = ref([]) // 分类树
 const categoryList = ref([]) // 分类树
-/**
- * 选择分类时触发校验
- */
-const categoryNodeClick = () => {
-  if (!checkSelectedNode(categoryList.value, formData.categoryId)) {
-    formData.categoryId = null
-    message.warning('必须选择二级及以下节点!!')
-  }
-}
-/**
- * 获取分类的节点的完整结构
- *
- * @param categoryId 分类id
- */
-const categoryString = (categoryId) => {
+/** 获取分类的节点的完整结构 */
+const formatCategoryName = (categoryId) => {
   return treeToString(categoryList.value, categoryId)
   return treeToString(categoryList.value, categoryId)
 }
 }
+
 const brandList = ref([]) // 精简商品品牌列表
 const brandList = ref([]) // 精简商品品牌列表
 const deliveryTemplateList = ref([]) // 运费模版
 const deliveryTemplateList = ref([]) // 运费模版
 onMounted(async () => {
 onMounted(async () => {

+ 1 - 1
src/views/mall/product/spu/form/OtherSettingsForm.vue

@@ -41,7 +41,7 @@
         </el-form-item>
         </el-form-item>
       </el-col>
       </el-col>
       <el-col :span="24">
       <el-col :span="24">
-        <!--   TODO tag展示暂时不考虑排序 -->
+        <!--   TODO @puhui999:tag展示暂时不考虑排序;支持拖动排序 -->
         <el-form-item label="活动优先级">
         <el-form-item label="活动优先级">
           <el-tag>默认</el-tag>
           <el-tag>默认</el-tag>
           <el-tag class="ml-2" type="success">秒杀</el-tag>
           <el-tag class="ml-2" type="success">秒杀</el-tag>

+ 10 - 10
src/views/mall/product/spu/form/index.vue

@@ -82,8 +82,8 @@ const formData = ref<ProductSpuApi.Spu>({
       stock: 0, // 库存
       stock: 0, // 库存
       weight: 0, // 商品重量
       weight: 0, // 商品重量
       volume: 0, // 商品体积
       volume: 0, // 商品体积
-      firstBrokerageRecord: 0, // 一级分销的佣金
-      secondBrokerageRecord: 0 // 二级分销的佣金
+      firstBrokeragePrice: 0, // 一级分销的佣金
+      secondBrokeragePrice: 0 // 二级分销的佣金
     }
     }
   ],
   ],
   description: '', // 商品详情
   description: '', // 商品详情
@@ -102,7 +102,7 @@ const getDetail = async () => {
   if ('ProductSpuDetail' === name) {
   if ('ProductSpuDetail' === name) {
     isDetail.value = true
     isDetail.value = true
   }
   }
-  const id = params.spuId as unknown as number
+  const id = params.id as unknown as number
   if (id) {
   if (id) {
     formLoading.value = true
     formLoading.value = true
     try {
     try {
@@ -112,15 +112,15 @@ const getDetail = async () => {
           item.price = floatToFixed2(item.price)
           item.price = floatToFixed2(item.price)
           item.marketPrice = floatToFixed2(item.marketPrice)
           item.marketPrice = floatToFixed2(item.marketPrice)
           item.costPrice = floatToFixed2(item.costPrice)
           item.costPrice = floatToFixed2(item.costPrice)
-          item.firstBrokerageRecord = floatToFixed2(item.firstBrokerageRecord)
-          item.secondBrokerageRecord = floatToFixed2(item.secondBrokerageRecord)
+          item.firstBrokeragePrice = floatToFixed2(item.firstBrokeragePrice)
+          item.secondBrokeragePrice = floatToFixed2(item.secondBrokeragePrice)
         } else {
         } else {
           // 回显价格分转元
           // 回显价格分转元
           item.price = formatToFraction(item.price)
           item.price = formatToFraction(item.price)
           item.marketPrice = formatToFraction(item.marketPrice)
           item.marketPrice = formatToFraction(item.marketPrice)
           item.costPrice = formatToFraction(item.costPrice)
           item.costPrice = formatToFraction(item.costPrice)
-          item.firstBrokerageRecord = formatToFraction(item.firstBrokerageRecord)
-          item.secondBrokerageRecord = formatToFraction(item.secondBrokerageRecord)
+          item.firstBrokeragePrice = formatToFraction(item.firstBrokeragePrice)
+          item.secondBrokeragePrice = formatToFraction(item.secondBrokeragePrice)
         }
         }
       })
       })
       formData.value = res
       formData.value = res
@@ -149,8 +149,8 @@ const submitForm = async () => {
       item.price = convertToInteger(item.price)
       item.price = convertToInteger(item.price)
       item.marketPrice = convertToInteger(item.marketPrice)
       item.marketPrice = convertToInteger(item.marketPrice)
       item.costPrice = convertToInteger(item.costPrice)
       item.costPrice = convertToInteger(item.costPrice)
-      item.firstBrokerageRecord = convertToInteger(item.firstBrokerageRecord)
-      item.secondBrokerageRecord = convertToInteger(item.secondBrokerageRecord)
+      item.firstBrokeragePrice = convertToInteger(item.firstBrokeragePrice)
+      item.secondBrokeragePrice = convertToInteger(item.secondBrokeragePrice)
     })
     })
     // 处理轮播图列表
     // 处理轮播图列表
     const newSliderPicUrls: any[] = []
     const newSliderPicUrls: any[] = []
@@ -161,7 +161,7 @@ const submitForm = async () => {
     deepCopyFormData.sliderPicUrls = newSliderPicUrls
     deepCopyFormData.sliderPicUrls = newSliderPicUrls
     // 校验都通过后提交表单
     // 校验都通过后提交表单
     const data = deepCopyFormData as ProductSpuApi.Spu
     const data = deepCopyFormData as ProductSpuApi.Spu
-    const id = params.spuId as unknown as number
+    const id = params.id as unknown as number
     if (!id) {
     if (!id) {
       await ProductSpuApi.createSpu(data)
       await ProductSpuApi.createSpu(data)
       message.success(t('common.createSuccess'))
       message.success(t('common.createSuccess'))

+ 53 - 81
src/views/mall/product/spu/index.vue

@@ -18,15 +18,14 @@
         />
         />
       </el-form-item>
       </el-form-item>
       <el-form-item label="商品分类" prop="categoryId">
       <el-form-item label="商品分类" prop="categoryId">
-        <el-tree-select
+        <el-cascader
           v-model="queryParams.categoryId"
           v-model="queryParams.categoryId"
-          :data="categoryList"
+          :options="categoryList"
           :props="defaultProps"
           :props="defaultProps"
-          check-strictly
           class="w-1/1"
           class="w-1/1"
-          node-key="id"
+          clearable
           placeholder="请选择商品分类"
           placeholder="请选择商品分类"
-          @change="nodeClick"
+          filterable
         />
         />
       </el-form-item>
       </el-form-item>
       <el-form-item label="创建时间" prop="createTime">
       <el-form-item label="创建时间" prop="createTime">
@@ -78,7 +77,7 @@
       />
       />
     </el-tabs>
     </el-tabs>
     <el-table v-loading="loading" :data="list">
     <el-table v-loading="loading" :data="list">
-      <el-table-column type="expand" width="30">
+      <el-table-column type="expand">
         <template #default="{ row }">
         <template #default="{ row }">
           <el-form class="spu-table-expand" label-position="left">
           <el-form class="spu-table-expand" label-position="left">
             <el-row>
             <el-row>
@@ -86,17 +85,17 @@
                 <el-row>
                 <el-row>
                   <el-col :span="8">
                   <el-col :span="8">
                     <el-form-item label="商品分类:">
                     <el-form-item label="商品分类:">
-                      <span>{{ categoryString(row.categoryId) }}</span>
+                      <span>{{ formatCategoryName(row.categoryId) }}</span>
                     </el-form-item>
                     </el-form-item>
                   </el-col>
                   </el-col>
                   <el-col :span="8">
                   <el-col :span="8">
                     <el-form-item label="市场价:">
                     <el-form-item label="市场价:">
-                      <span>{{ floatToFixed2(row.marketPrice) }}元</span>
+                      <span>{{ fenToYuan(row.marketPrice) }}</span>
                     </el-form-item>
                     </el-form-item>
                   </el-col>
                   </el-col>
                   <el-col :span="8">
                   <el-col :span="8">
                     <el-form-item label="成本价:">
                     <el-form-item label="成本价:">
-                      <span>{{ floatToFixed2(row.costPrice) }}元</span>
+                      <span>{{ fenToYuan(row.costPrice) }}</span>
                     </el-form-item>
                     </el-form-item>
                   </el-col>
                   </el-col>
                 </el-row>
                 </el-row>
@@ -106,9 +105,8 @@
               <el-col :span="24">
               <el-col :span="24">
                 <el-row>
                 <el-row>
                   <el-col :span="8">
                   <el-col :span="8">
-                    <el-form-item label="收藏:">
-                      <!-- TODO 没有这个属性,暂时写死 5 个 -->
-                      <span>5</span>
+                    <el-form-item label="浏览量:">
+                      <span>{{ row.browseCount }}</span>
                     </el-form-item>
                     </el-form-item>
                   </el-col>
                   </el-col>
                   <el-col :span="8">
                   <el-col :span="8">
@@ -122,7 +120,7 @@
           </el-form>
           </el-form>
         </template>
         </template>
       </el-table-column>
       </el-table-column>
-      <el-table-column key="id" align="center" label="商品编号" prop="id" />
+      <el-table-column align="center" label="商品编号" min-width="60" prop="id" />
       <el-table-column label="商品图" min-width="80">
       <el-table-column label="商品图" min-width="80">
         <template #default="{ row }">
         <template #default="{ row }">
           <el-image :src="row.picUrl" class="h-30px w-30px" @click="imagePreview(row.picUrl)" />
           <el-image :src="row.picUrl" class="h-30px w-30px" @click="imagePreview(row.picUrl)" />
@@ -130,7 +128,7 @@
       </el-table-column>
       </el-table-column>
       <el-table-column :show-overflow-tooltip="true" label="商品名称" min-width="300" prop="name" />
       <el-table-column :show-overflow-tooltip="true" label="商品名称" min-width="300" prop="name" />
       <el-table-column align="center" label="商品售价" min-width="90" prop="price">
       <el-table-column align="center" label="商品售价" min-width="90" prop="price">
-        <template #default="{ row }"> {{ floatToFixed2(row.price) }}元</template>
+        <template #default="{ row }"> {{ fenToYuan(row.price) }}元</template>
       </el-table-column>
       </el-table-column>
       <el-table-column align="center" label="销量" min-width="90" prop="salesCount" />
       <el-table-column align="center" label="销量" min-width="90" prop="salesCount" />
       <el-table-column align="center" label="库存" min-width="90" prop="stock" />
       <el-table-column align="center" label="库存" min-width="90" prop="stock" />
@@ -152,7 +150,7 @@
               active-text="上架"
               active-text="上架"
               inactive-text="下架"
               inactive-text="下架"
               inline-prompt
               inline-prompt
-              @change="changeStatus(row)"
+              @change="handleStatusChange(row)"
             />
             />
           </template>
           </template>
           <template v-else>
           <template v-else>
@@ -191,7 +189,7 @@
               v-hasPermi="['product:spu:update']"
               v-hasPermi="['product:spu:update']"
               link
               link
               type="primary"
               type="primary"
-              @click="changeStatus(row, ProductSpuStatusEnum.DISABLE.status)"
+              @click="handleStatus02Change(row, ProductSpuStatusEnum.DISABLE.status)"
             >
             >
               恢复到仓库
               恢复到仓库
             </el-button>
             </el-button>
@@ -201,7 +199,7 @@
               v-hasPermi="['product:spu:update']"
               v-hasPermi="['product:spu:update']"
               link
               link
               type="primary"
               type="primary"
-              @click="changeStatus(row, ProductSpuStatusEnum.RECYCLE.status)"
+              @click="handleStatus02Change(row, ProductSpuStatusEnum.RECYCLE.status)"
             >
             >
               加入回收站
               加入回收站
             </el-button>
             </el-button>
@@ -220,12 +218,11 @@
 </template>
 </template>
 <script lang="ts" setup>
 <script lang="ts" setup>
 import { TabsPaneContext } from 'element-plus'
 import { TabsPaneContext } from 'element-plus'
-import { cloneDeep } from 'lodash-es'
 import { createImageViewer } from '@/components/ImageViewer'
 import { createImageViewer } from '@/components/ImageViewer'
 import { dateFormatter } from '@/utils/formatTime'
 import { dateFormatter } from '@/utils/formatTime'
-import { checkSelectedNode, defaultProps, handleTree, treeToString } from '@/utils/tree'
+import { defaultProps, handleTree, treeToString } from '@/utils/tree'
 import { ProductSpuStatusEnum } from '@/utils/constants'
 import { ProductSpuStatusEnum } from '@/utils/constants'
-import { floatToFixed2 } from '@/utils'
+import { fenToYuan } from '@/utils'
 import download from '@/utils/download'
 import download from '@/utils/download'
 import * as ProductSpuApi from '@/api/mall/product/spu'
 import * as ProductSpuApi from '@/api/mall/product/spu'
 import * as ProductCategoryApi from '@/api/mall/product/category'
 import * as ProductCategoryApi from '@/api/mall/product/category'
@@ -254,7 +251,7 @@ const tabsData = ref([
   },
   },
   {
   {
     count: 0,
     count: 0,
-    name: '已经售空商品',
+    name: '已售罄商品',
     type: 2
     type: 2
   },
   },
   {
   {
@@ -303,43 +300,37 @@ const getList = async () => {
   }
   }
 }
 }
 
 
-/**
- * 更改 SPU 状态
- *
- * @param row
- * @param status 更改前的值
- */
-const changeStatus = async (row, status?: number) => {
-  const deepCopyValue = cloneDeep(unref(row))
-  if (typeof status !== 'undefined') deepCopyValue.status = status
+/** 添加到仓库 / 回收站的状态 */
+const handleStatus02Change = async (row, newStatus: number) => {
+  try {
+    // 二次确认
+    const text = newStatus === ProductSpuStatusEnum.RECYCLE.status ? '加入到回收站' : '恢复到仓库'
+    await message.confirm(`确认要"${row.name}"${text}吗?`)
+    // 发起修改
+    await ProductSpuApi.updateStatus({ id: row.id, status: newStatus })
+    message.success(text + '成功')
+    // 刷新 tabs 数据
+    await getTabsCount()
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 更新上架/下架状态 */
+const handleStatusChange = async (row) => {
   try {
   try {
-    let text = ''
-    switch (deepCopyValue.status) {
-      case ProductSpuStatusEnum.DISABLE.status:
-        text = ProductSpuStatusEnum.DISABLE.name
-        break
-      case ProductSpuStatusEnum.ENABLE.status:
-        text = ProductSpuStatusEnum.ENABLE.name
-        break
-      case ProductSpuStatusEnum.RECYCLE.status:
-        text = `加入${ProductSpuStatusEnum.RECYCLE.name}`
-        break
-    }
-    await message.confirm(
-      deepCopyValue.status === -1
-        ? `确认要将[${row.name}]${text}吗?`
-        : row.status === -1 // 再判断一次原对象是否等于-1,例: 把回收站中的商品恢复到仓库中,事件触发时原对象status为-1 深拷贝对象status被赋值为0
-        ? `确认要将[${row.name}]恢复到仓库吗?`
-        : `确认要${text}[${row.name}]吗?`
-    )
-    await ProductSpuApi.updateStatus({ id: deepCopyValue.id, status: deepCopyValue.status })
-    message.success('更新状态成功')
+    // 二次确认
+    const text = row.status ? '上架' : '下架'
+    await message.confirm(`确认要${text}"${row.name}"吗?`)
+    // 发起修改
+    await ProductSpuApi.updateStatus({ id: row.id, status: row.status })
+    message.success(text + '成功')
     // 刷新 tabs 数据
     // 刷新 tabs 数据
     await getTabsCount()
     await getTabsCount()
     // 刷新列表
     // 刷新列表
     await getList()
     await getList()
   } catch {
   } catch {
-    // 取消更改状态时回显数据
+    // 异常时,需要重置回之前的值
     row.status =
     row.status =
       row.status === ProductSpuStatusEnum.DISABLE.status
       row.status === ProductSpuStatusEnum.DISABLE.status
         ? ProductSpuStatusEnum.ENABLE.status
         ? ProductSpuStatusEnum.ENABLE.status
@@ -380,26 +371,20 @@ const resetQuery = () => {
   handleQuery()
   handleQuery()
 }
 }
 
 
-/**
- * 新增或修改
- *
- * @param id 商品 SPU 编号
- */
+/** 新增或修改 */
 const openForm = (id?: number) => {
 const openForm = (id?: number) => {
   // 修改
   // 修改
   if (typeof id === 'number') {
   if (typeof id === 'number') {
-    push({ name: 'ProductSpuEdit', params: { spuId: id } })
+    push({ name: 'ProductSpuEdit', params: { id } })
     return
     return
   }
   }
   // 新增
   // 新增
   push({ name: 'ProductSpuAdd' })
   push({ name: 'ProductSpuAdd' })
 }
 }
 
 
-/**
- * 查看商品详情
- */
+/** 查看商品详情 */
 const openDetail = (id: number) => {
 const openDetail = (id: number) => {
-  push({ name: 'ProductSpuDetail', params: { spuId: id } })
+  push({ name: 'ProductSpuDetail', params: { id } })
 }
 }
 
 
 /** 导出按钮操作 */
 /** 导出按钮操作 */
@@ -417,6 +402,12 @@ const handleExport = async () => {
   }
   }
 }
 }
 
 
+const categoryList = ref() // 分类树
+/** 获取分类的节点的完整结构 */
+const formatCategoryName = (categoryId) => {
+  return treeToString(categoryList.value, categoryId)
+}
+
 // 监听路由变化更新列表,解决商品保存后,列表不刷新的问题。
 // 监听路由变化更新列表,解决商品保存后,列表不刷新的问题。
 watch(
 watch(
   () => currentRoute.value,
   () => currentRoute.value,
@@ -425,25 +416,6 @@ watch(
   }
   }
 )
 )
 
 
-const categoryList = ref() // 分类树
-/**
- * 获取分类的节点的完整结构
- * @param categoryId 分类id
- */
-const categoryString = (categoryId) => {
-  return treeToString(categoryList.value, categoryId)
-}
-
-/**
- * 校验所选是否为二级及以下节点
- */
-const nodeClick = () => {
-  if (!checkSelectedNode(categoryList.value, queryParams.value.categoryId)) {
-    queryParams.value.categoryId = null
-    message.warning('必须选择二级及以下节点!!')
-  }
-}
-
 /** 初始化 **/
 /** 初始化 **/
 onMounted(async () => {
 onMounted(async () => {
   await getTabsCount()
   await getTabsCount()

+ 8 - 7
src/views/mall/promotion/bargain/activity/BargainActivityForm.vue

@@ -30,7 +30,7 @@
           <el-table-column align="center" label="砍价底价(元)" min-width="168">
           <el-table-column align="center" label="砍价底价(元)" min-width="168">
             <template #default="{ row: sku }">
             <template #default="{ row: sku }">
               <el-input-number
               <el-input-number
-                v-model="sku.productConfig.bargainPrice"
+                v-model="sku.productConfig.bargainMinPrice"
                 :min="0"
                 :min="0"
                 :precision="2"
                 :precision="2"
                 :step="0.1"
                 :step="0.1"
@@ -61,6 +61,7 @@ import { SpuAndSkuList, SpuProperty, SpuSelect } from '@/views/mall/promotion/co
 import { getPropertyList, RuleConfig } from '@/views/mall/product/spu/components'
 import { getPropertyList, RuleConfig } from '@/views/mall/product/spu/components'
 import * as ProductSpuApi from '@/api/mall/product/spu'
 import * as ProductSpuApi from '@/api/mall/product/spu'
 import { convertToInteger, formatToFraction } from '@/utils'
 import { convertToInteger, formatToFraction } from '@/utils'
+import { cloneDeep } from 'lodash-es'
 
 
 defineOptions({ name: 'PromotionBargainActivityForm' })
 defineOptions({ name: 'PromotionBargainActivityForm' })
 
 
@@ -86,7 +87,7 @@ const ruleConfig: RuleConfig[] = [
     message: '商品砍价起始价格不能小于 0 !!!'
     message: '商品砍价起始价格不能小于 0 !!!'
   },
   },
   {
   {
-    name: 'productConfig.bargainPrice',
+    name: 'productConfig.bargainMinPrice',
     rule: (arg) => arg >= 0,
     rule: (arg) => arg >= 0,
     message: '商品砍价底价不能小于 0 !!!'
     message: '商品砍价底价不能小于 0 !!!'
   },
   },
@@ -123,14 +124,14 @@ const getSpuDetails = async (
       spuId: spu.id!,
       spuId: spu.id!,
       skuId: sku.id!,
       skuId: sku.id!,
       bargainFirstPrice: 1,
       bargainFirstPrice: 1,
-      bargainPrice: 1,
+      bargainMinPrice: 1,
       stock: 1
       stock: 1
     }
     }
     if (typeof products !== 'undefined') {
     if (typeof products !== 'undefined') {
       const product = products.find((item) => item.skuId === sku.id)
       const product = products.find((item) => item.skuId === sku.id)
       if (product) {
       if (product) {
         product.bargainFirstPrice = formatToFraction(product.bargainFirstPrice)
         product.bargainFirstPrice = formatToFraction(product.bargainFirstPrice)
-        product.bargainPrice = formatToFraction(product.bargainPrice)
+        product.bargainMinPrice = formatToFraction(product.bargainMinPrice)
       }
       }
       config = product || config
       config = product || config
     }
     }
@@ -173,7 +174,7 @@ const open = async (type: string, id?: number) => {
             spuId: data.spuId!,
             spuId: data.spuId!,
             skuId: data.skuId,
             skuId: data.skuId,
             bargainFirstPrice: data.bargainFirstPrice, // 砍价起始价格,单位分
             bargainFirstPrice: data.bargainFirstPrice, // 砍价起始价格,单位分
-            bargainPrice: data.bargainPrice, // 砍价底价
+            bargainMinPrice: data.bargainMinPrice, // 砍价底价
             stock: data.stock // 活动库存
             stock: data.stock // 活动库存
           }
           }
         ]
         ]
@@ -204,12 +205,12 @@ const submitForm = async () => {
   // 提交请求
   // 提交请求
   formLoading.value = true
   formLoading.value = true
   try {
   try {
-    const data = formRef.value.formModel as BargainActivityApi.BargainActivityVO
+    const data = cloneDeep(formRef.value.formModel) as BargainActivityApi.BargainActivityVO
     const products = spuAndSkuListRef.value.getSkuConfigs('productConfig')
     const products = spuAndSkuListRef.value.getSkuConfigs('productConfig')
     products.forEach((item: BargainProductVO) => {
     products.forEach((item: BargainProductVO) => {
       // 砍价价格元转分
       // 砍价价格元转分
       item.bargainFirstPrice = convertToInteger(item.bargainFirstPrice)
       item.bargainFirstPrice = convertToInteger(item.bargainFirstPrice)
-      item.bargainPrice = convertToInteger(item.bargainPrice)
+      item.bargainMinPrice = convertToInteger(item.bargainMinPrice)
     })
     })
     // 用户每次砍价金额分转元, 元转分
     // 用户每次砍价金额分转元, 元转分
     data.randomMinPrice = convertToInteger(data.randomMinPrice)
     data.randomMinPrice = convertToInteger(data.randomMinPrice)

+ 2 - 21
src/views/mall/promotion/bargain/activity/bargainActivity.data.ts

@@ -6,7 +6,7 @@ export const rules = reactive({
   name: [required],
   name: [required],
   startTime: [required],
   startTime: [required],
   endTime: [required],
   endTime: [required],
-  userSize: [required],
+  helpMaxCount: [required],
   bargainCount: [required],
   bargainCount: [required],
   singleLimitCount: [required]
   singleLimitCount: [required]
 })
 })
@@ -72,7 +72,7 @@ const crudSchemas = reactive<CrudSchema[]>([
   },
   },
   {
   {
     label: '砍价人数',
     label: '砍价人数',
-    field: 'userSize',
+    field: 'helpMaxCount',
     isSearch: false,
     isSearch: false,
     form: {
     form: {
       component: 'InputNumber',
       component: 'InputNumber',
@@ -132,20 +132,6 @@ const crudSchemas = reactive<CrudSchema[]>([
       value: 0
       value: 0
     }
     }
   },
   },
-  {
-    label: '砍价成功数量',
-    field: 'successCount',
-    isSearch: false,
-    isForm: false
-  },
-  {
-    label: '活动状态',
-    field: 'status',
-    dictType: DICT_TYPE.COMMON_STATUS,
-    dictClass: 'number',
-    isSearch: true,
-    isForm: false
-  },
   {
   {
     label: '拼团商品',
     label: '拼团商品',
     field: 'spuId',
     field: 'spuId',
@@ -155,11 +141,6 @@ const crudSchemas = reactive<CrudSchema[]>([
         span: 24
         span: 24
       }
       }
     }
     }
-  },
-  {
-    label: '操作',
-    field: 'action',
-    isForm: false
   }
   }
 ])
 ])
 export const { allSchemas } = useCrudSchemas(crudSchemas)
 export const { allSchemas } = useCrudSchemas(crudSchemas)

+ 194 - 70
src/views/mall/promotion/bargain/activity/index.vue

@@ -1,90 +1,194 @@
 <template>
 <template>
-  <!-- 搜索工作栏 -->
   <ContentWrap>
   <ContentWrap>
-    <Search :schema="allSchemas.searchSchema" @reset="setSearchParams" @search="setSearchParams">
-      <!-- 新增等操作按钮 -->
-      <template #actionMore>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="活动名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入活动名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="活动状态" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择活动状态"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
         <el-button
         <el-button
-          v-hasPermi="['promotion:bargain-activity:create']"
-          plain
           type="primary"
           type="primary"
+          plain
           @click="openForm('create')"
           @click="openForm('create')"
+          v-hasPermi="['promotion:bargain-activity:create']"
         >
         >
-          <Icon class="mr-5px" icon="ep:plus" />
-          新增
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
         </el-button>
         </el-button>
-      </template>
-    </Search>
+      </el-form-item>
+    </el-form>
   </ContentWrap>
   </ContentWrap>
 
 
   <!-- 列表 -->
   <!-- 列表 -->
   <ContentWrap>
   <ContentWrap>
-    <Table
-      v-model:currentPage="tableObject.currentPage"
-      v-model:pageSize="tableObject.pageSize"
-      :columns="allSchemas.tableColumns"
-      :data="tableObject.tableList"
-      :loading="tableObject.loading"
-      :pagination="{
-        total: tableObject.total
-      }"
-    >
-      <template #spuId="{ row }">
-        <el-image
-          :src="row.picUrl"
-          class="mr-5px h-30px w-30px align-middle"
-          @click="imagePreview(row.picUrl)"
-        />
-        <span class="align-middle">{{ row.spuName }}</span>
-      </template>
-      <template #action="{ row }">
-        <el-button
-          v-hasPermi="['promotion:bargain-activity:update']"
-          link
-          type="primary"
-          @click="openForm('update', row.id)"
-        >
-          编辑
-        </el-button>
-        <el-button
-          v-hasPermi="['promotion:bargain-activity:delete']"
-          link
-          type="danger"
-          @click="handleDelete(row.id)"
-        >
-          删除
-        </el-button>
-      </template>
-    </Table>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="活动编号" prop="id" min-width="80" />
+      <el-table-column label="活动名称" prop="name" min-width="140" />
+      <el-table-column label="活动时间" min-width="210">
+        <template #default="scope">
+          {{ formatDate(scope.row.startTime, 'YYYY-MM-DD') }}
+          ~ {{ formatDate(scope.row.endTime, 'YYYY-MM-DD') }}
+        </template>
+      </el-table-column>
+      <el-table-column label="商品图片" prop="spuName" min-width="80">
+        <template #default="scope">
+          <el-image
+            :src="scope.row.picUrl"
+            class="h-40px w-40px"
+            :preview-src-list="[scope.row.picUrl]"
+            preview-teleported
+          />
+        </template>
+      </el-table-column>
+      <el-table-column label="商品标题" prop="spuName" min-width="300" />
+      <el-table-column
+        label="起始价格"
+        prop="bargainFirstPrice"
+        min-width="100"
+        :formatter="fenToYuanFormat"
+      />
+      <el-table-column
+        label="砍价底价"
+        prop="bargainMinPrice"
+        min-width="100"
+        :formatter="fenToYuanFormat"
+      />
+      <el-table-column label="总砍价人数" prop="recordUserCount" min-width="100" />
+      <el-table-column label="成功砍价人数" prop="recordSuccessUserCount" min-width="110" />
+      <el-table-column label="助力人数" prop="helpUserCount" min-width="100" />
+      <el-table-column label="活动状态" align="center" prop="status" min-width="100">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column label="库存" align="center" prop="stock" min-width="80" />
+      <el-table-column label="总库存" align="center" prop="totalStock" min-width="80" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="操作" align="center" width="150px" fixed="right">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['promotion:bargain-activity:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleClose(scope.row.id)"
+            v-if="scope.row.status === 0"
+            v-hasPermi="['promotion:bargain-activity:close']"
+          >
+            关闭
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-else
+            v-hasPermi="['promotion:bargain-activity:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
   </ContentWrap>
   </ContentWrap>
 
 
   <!-- 表单弹窗:添加/修改 -->
   <!-- 表单弹窗:添加/修改 -->
   <BargainActivityForm ref="formRef" @success="getList" />
   <BargainActivityForm ref="formRef" @success="getList" />
 </template>
 </template>
-<script lang="ts" setup>
-import { allSchemas } from './bargainActivity.data'
+
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
 import * as BargainActivityApi from '@/api/mall/promotion/bargain/bargainActivity'
 import * as BargainActivityApi from '@/api/mall/promotion/bargain/bargainActivity'
 import BargainActivityForm from './BargainActivityForm.vue'
 import BargainActivityForm from './BargainActivityForm.vue'
-import { createImageViewer } from '@/components/ImageViewer'
-import { sortTableColumns } from '@/hooks/web/useCrudSchemas'
+import { formatDate } from '@/utils/formatTime'
+import { fenToYuanFormat } from '@/utils/formatter'
 
 
 defineOptions({ name: 'PromotionBargainActivity' })
 defineOptions({ name: 'PromotionBargainActivity' })
 
 
-// tableObject:表格的属性对象,可获得分页大小、条数等属性
-// tableMethods:表格的操作对象,可进行获得分页、删除记录等操作
-// 详细可见:https://doc.iocoder.cn/vue3/crud-schema/
-const { tableObject, tableMethods } = useTable({
-  getListApi: BargainActivityApi.getBargainActivityPage, // 分页接口
-  delListApi: BargainActivityApi.deleteBargainActivity // 删除接口
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: null,
+  status: null
 })
 })
-// 获得表格的各种操作
-const { getList, setSearchParams } = tableMethods
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
 
 
-/** 商品图预览 */
-const imagePreview = (imgUrl: string) => {
-  createImageViewer({
-    urlList: [imgUrl]
-  })
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await BargainActivityApi.getBargainActivityPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
 }
 }
 
 
 /** 添加/修改操作 */
 /** 添加/修改操作 */
@@ -93,15 +197,35 @@ const openForm = (type: string, id?: number) => {
   formRef.value.open(type, id)
   formRef.value.open(type, id)
 }
 }
 
 
+// TODO 芋艿:这里要改下
+/** 关闭按钮操作 */
+const handleClose = async (id: number) => {
+  try {
+    // 关闭的二次确认
+    await message.confirm('确认关闭该砍价活动吗?')
+    // 发起关闭
+    await BargainActivityApi.closeSeckillActivity(id)
+    message.success('关闭成功')
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
 /** 删除按钮操作 */
 /** 删除按钮操作 */
-const handleDelete = (id: number) => {
-  tableMethods.delList(id, false)
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await BargainActivityApi.closeBargainActivity(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
 }
 }
 
 
 /** 初始化 **/
 /** 初始化 **/
-onMounted(() => {
-  // 获得活动列表
-  sortTableColumns(allSchemas.tableColumns, 'spuId')
-  getList()
+onMounted(async () => {
+  await getList()
 })
 })
 </script>
 </script>

+ 90 - 0
src/views/mall/promotion/bargain/record/BargainRecordListDialog.vue

@@ -0,0 +1,90 @@
+<template>
+  <Dialog v-model="dialogVisible" title="助力列表">
+    <!-- 列表 -->
+    <ContentWrap>
+      <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+        <el-table-column label="用户编号" prop="userId" min-width="80px" />
+        <el-table-column label="用户头像" prop="avatar" min-width="80px">
+          <template #default="scope">
+            <el-avatar :src="scope.row.avatar" />
+          </template>
+        </el-table-column>
+        <el-table-column label="用户昵称" prop="nickname" min-width="100px" />
+        <el-table-column
+          label="砍价金额"
+          prop="reducePrice"
+          min-width="100px"
+          :formatter="fenToYuanFormat"
+        />
+        <el-table-column
+          label="助力时间"
+          align="center"
+          prop="createTime"
+          :formatter="dateFormatter"
+          width="180px"
+        />
+      </el-table>
+      <!-- 分页 -->
+      <Pagination
+        :total="total"
+        v-model:page="queryParams.pageNo"
+        v-model:limit="queryParams.pageSize"
+        @pagination="getList"
+      />
+    </ContentWrap>
+  </Dialog>
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import * as BargainHelpApi from '@/api/mall/promotion/bargain/bargainHelp'
+import { fenToYuanFormat } from '@/utils/formatter'
+
+/** 助力列表 */
+defineOptions({ name: 'BargainRecordListDialog' })
+
+const message = useMessage() // 消息弹窗
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  recordId: undefined
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 打开弹窗 */
+const dialogVisible = ref(false) // 弹窗的是否展示
+const open = async (recordId: any) => {
+  dialogVisible.value = true
+  queryParams.recordId = recordId
+  resetQuery()
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await BargainHelpApi.getBargainHelpPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+</script>

+ 195 - 0
src/views/mall/promotion/bargain/record/index.vue

@@ -0,0 +1,195 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="砍价状态" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择砍价状态"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_BARGAIN_RECORD_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['promotion:bargain-record:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          @click="handleExport"
+          :loading="exportLoading"
+          v-hasPermi="['promotion:bargain-record:export']"
+        >
+          <Icon icon="ep:download" class="mr-5px" /> 导出
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="编号" min-width="50" prop="id" />
+      <el-table-column label="发起用户" min-width="120">
+        <template #default="scope">
+          <el-image
+            :src="scope.row.avatar"
+            class="h-20px w-20px"
+            :preview-src-list="[scope.row.avatar]"
+            preview-teleported
+          />
+          {{ scope.row.nickname }}
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="发起时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="砍价活动" min-width="150" prop="activity.name" />
+      <el-table-column
+        label="最低价"
+        min-width="100"
+        prop="activity.bargainMinPrice"
+        :formatter="fenToYuanFormat"
+      />
+      <el-table-column
+        label="当前价"
+        min-width="100"
+        prop="bargainPrice"
+        :formatter="fenToYuanFormat"
+      />
+      <el-table-column label="总砍价次数" min-width="100" prop="activity.helpMaxCount" />
+      <el-table-column label="剩余砍价次数" min-width="100" prop="helpCount" />
+      <el-table-column label="砍价状态" align="center" prop="status">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.PROMOTION_BARGAIN_RECORD_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="结束时间"
+        align="center"
+        prop="endTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="订单编号" align="center" prop="orderId" />
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openRecordListDialog(scope.row.id)"
+            v-hasPermi="['promotion:bargain-help:query']"
+          >
+            助力
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗 -->
+  <BargainRecordListDialog ref="recordListDialogRef" />
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as BargainRecordApi from '@/api/mall/promotion/bargain/bargainRecord'
+import { fenToYuanFormat } from '@/utils/formatter'
+import BargainRecordListDialog from './BargainRecordListDialog.vue'
+
+defineOptions({ name: 'PromotionBargainRecord' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  status: null,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await BargainRecordApi.getBargainRecordPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 打开[助力]弹窗 */
+const recordListDialogRef = ref()
+const openRecordListDialog = (id?: number) => {
+  recordListDialogRef.value.open(id)
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 1 - 1
src/views/mall/promotion/combination/activity/CombinationActivityForm.vue

@@ -167,7 +167,7 @@ const submitForm = async () => {
     products.forEach((item: CombinationActivityApi.CombinationProductVO) => {
     products.forEach((item: CombinationActivityApi.CombinationProductVO) => {
       item.combinationPrice = convertToInteger(item.combinationPrice)
       item.combinationPrice = convertToInteger(item.combinationPrice)
     })
     })
-    const data = formRef.value.formModel as CombinationActivityApi.CombinationActivityVO
+    const data = cloneDeep(formRef.value.formModel) as CombinationActivityApi.CombinationActivityVO
     data.products = products
     data.products = products
     // 真正提交
     // 真正提交
     if (formType.value === 'create') {
     if (formType.value === 'create') {

+ 10 - 29
src/views/mall/promotion/combination/activity/combinationActivity.data.ts

@@ -9,7 +9,8 @@ export const rules = reactive({
   startTime: [required],
   startTime: [required],
   endTime: [required],
   endTime: [required],
   userSize: [required],
   userSize: [required],
-  limitDuration: [required]
+  limitDuration: [required],
+  virtualGroup: [required]
 })
 })
 
 
 // CrudSchema https://doc.iocoder.cn/vue3/crud-schema/
 // CrudSchema https://doc.iocoder.cn/vue3/crud-schema/
@@ -115,30 +116,15 @@ const crudSchemas = reactive<CrudSchema[]>([
     }
     }
   },
   },
   {
   {
-    label: '购买人数',
-    field: 'userSize',
-    isSearch: false,
-    isForm: false
-  },
-  {
-    label: '开团组数',
-    field: 'totalCount',
-    isSearch: false,
-    isForm: false
-  },
-  {
-    label: '成团组数',
-    field: 'successCount',
-    isSearch: false,
-    isForm: false
-  },
-  {
-    label: '活动状态',
-    field: 'status',
-    dictType: DICT_TYPE.COMMON_STATUS,
-    dictClass: 'number',
+    label: '虚拟成团',
+    field: 'virtualGroup',
+    dictType: DICT_TYPE.INFRA_BOOLEAN_STRING,
+    dictClass: 'boolean',
     isSearch: true,
     isSearch: true,
-    isForm: false
+    form: {
+      component: 'Radio',
+      value: false
+    }
   },
   },
   {
   {
     label: '拼团商品',
     label: '拼团商品',
@@ -149,11 +135,6 @@ const crudSchemas = reactive<CrudSchema[]>([
         span: 24
         span: 24
       }
       }
     }
     }
-  },
-  {
-    label: '操作',
-    field: 'action',
-    isForm: false
   }
   }
 ])
 ])
 export const { allSchemas } = useCrudSchemas(crudSchemas)
 export const { allSchemas } = useCrudSchemas(crudSchemas)

+ 198 - 72
src/views/mall/promotion/combination/activity/index.vue

@@ -1,91 +1,192 @@
 <template>
 <template>
-  <doc-alert title="功能开启" url="https://doc.iocoder.cn/mall/build/" />
-
-  <!-- 搜索工作栏 -->
   <ContentWrap>
   <ContentWrap>
-    <Search :schema="allSchemas.searchSchema" @reset="setSearchParams" @search="setSearchParams">
-      <!-- 新增等操作按钮 -->
-      <template #actionMore>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="活动名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入活动名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="活动状态" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择活动状态"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
         <el-button
         <el-button
-          v-hasPermi="['promotion:combination-activity:create']"
-          plain
           type="primary"
           type="primary"
+          plain
           @click="openForm('create')"
           @click="openForm('create')"
+          v-hasPermi="['promotion:combination-activity:create']"
         >
         >
-          <Icon class="mr-5px" icon="ep:plus" /> 新增
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
         </el-button>
         </el-button>
-      </template>
-    </Search>
+      </el-form-item>
+    </el-form>
   </ContentWrap>
   </ContentWrap>
 
 
   <!-- 列表 -->
   <!-- 列表 -->
   <ContentWrap>
   <ContentWrap>
-    <Table
-      v-model:currentPage="tableObject.currentPage"
-      v-model:pageSize="tableObject.pageSize"
-      :columns="allSchemas.tableColumns"
-      :data="tableObject.tableList"
-      :loading="tableObject.loading"
-      :pagination="{
-        total: tableObject.total
-      }"
-    >
-      <template #spuId="{ row }">
-        <el-image
-          :src="row.picUrl"
-          class="mr-5px h-30px w-30px align-middle"
-          @click="imagePreview(row.picUrl)"
-        />
-        <span class="align-middle">{{ row.spuName }}</span>
-      </template>
-      <template #action="{ row }">
-        <el-button
-          v-hasPermi="['promotion:combination-activity:update']"
-          link
-          type="primary"
-          @click="openForm('update', row.id)"
-        >
-          编辑
-        </el-button>
-        <el-button
-          v-hasPermi="['promotion:combination-activity:delete']"
-          link
-          type="danger"
-          @click="handleDelete(row.id)"
-        >
-          删除
-        </el-button>
-      </template>
-    </Table>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="活动编号" prop="id" min-width="80" />
+      <el-table-column label="活动名称" prop="name" min-width="140" />
+      <el-table-column label="活动时间" min-width="210">
+        <template #default="scope">
+          {{ formatDate(scope.row.startTime, 'YYYY-MM-DD') }}
+          ~ {{ formatDate(scope.row.endTime, 'YYYY-MM-DD') }}
+        </template>
+      </el-table-column>
+      <el-table-column label="商品图片" prop="spuName" min-width="80">
+        <template #default="scope">
+          <el-image
+            :src="scope.row.picUrl"
+            class="h-40px w-40px"
+            :preview-src-list="[scope.row.picUrl]"
+            preview-teleported
+          />
+        </template>
+      </el-table-column>
+      <el-table-column label="商品标题" prop="spuName" min-width="300" />
+      <el-table-column
+        label="原价"
+        prop="marketPrice"
+        min-width="100"
+        :formatter="fenToYuanFormat"
+      />
+      <el-table-column label="拼团价" prop="seckillPrice" min-width="100">
+        <template #default="scope">
+          {{ formatCombinationPrice(scope.row.products) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="开团组数" prop="groupCount" min-width="100" />
+      <el-table-column label="成团组数" prop="groupSuccessCount" min-width="100" />
+      <el-table-column label="购买次数" prop="recordCount" min-width="100" />
+      <el-table-column label="活动状态" align="center" prop="status" min-width="100">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="操作" align="center" width="150px" fixed="right">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['promotion:combination-activity:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleClose(scope.row.id)"
+            v-if="scope.row.status === 0"
+            v-hasPermi="['promotion:combination-activity:close']"
+          >
+            关闭
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-else
+            v-hasPermi="['promotion:combination-activity:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
   </ContentWrap>
   </ContentWrap>
 
 
   <!-- 表单弹窗:添加/修改 -->
   <!-- 表单弹窗:添加/修改 -->
   <CombinationActivityForm ref="formRef" @success="getList" />
   <CombinationActivityForm ref="formRef" @success="getList" />
 </template>
 </template>
-<script lang="ts" setup>
-import { allSchemas } from './combinationActivity.data'
+
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
 import * as CombinationActivityApi from '@/api/mall/promotion/combination/combinationActivity'
 import * as CombinationActivityApi from '@/api/mall/promotion/combination/combinationActivity'
 import CombinationActivityForm from './CombinationActivityForm.vue'
 import CombinationActivityForm from './CombinationActivityForm.vue'
-import { sortTableColumns } from '@/hooks/web/useCrudSchemas'
-import { createImageViewer } from '@/components/ImageViewer'
+import { formatDate } from '@/utils/formatTime'
+import { fenToYuanFormat } from '@/utils/formatter'
+import { fenToYuan } from '@/utils'
+
+defineOptions({ name: 'PromotionBargainActivity' })
 
 
-defineOptions({ name: 'PromotionCombinationActivity' })
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
 
 
-// tableObject:表格的属性对象,可获得分页大小、条数等属性
-// tableMethods:表格的操作对象,可进行获得分页、删除记录等操作
-// 详细可见:https://doc.iocoder.cn/vue3/crud-schema/
-const { tableObject, tableMethods } = useTable({
-  getListApi: CombinationActivityApi.getCombinationActivityPage, // 分页接口
-  delListApi: CombinationActivityApi.deleteCombinationActivity // 删除接口
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: null,
+  status: null
 })
 })
-// 获得表格的各种操作
-const { getList, setSearchParams } = tableMethods
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await CombinationActivityApi.getCombinationActivityPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
 
 
-/** 商品图预览 */
-const imagePreview = (imgUrl: string) => {
-  createImageViewer({
-    urlList: [imgUrl]
-  })
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
 }
 }
 
 
 /** 添加/修改操作 */
 /** 添加/修改操作 */
@@ -94,15 +195,40 @@ const openForm = (type: string, id?: number) => {
   formRef.value.open(type, id)
   formRef.value.open(type, id)
 }
 }
 
 
+// TODO 芋艿:这里要改下
+/** 关闭按钮操作 */
+const handleClose = async (id: number) => {
+  try {
+    // 关闭的二次确认
+    await message.confirm('确认关闭该秒杀活动吗?')
+    // 发起关闭
+    await CombinationActivityApi.closeCombinationActivity(id)
+    message.success('关闭成功')
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
 /** 删除按钮操作 */
 /** 删除按钮操作 */
-const handleDelete = (id: number) => {
-  tableMethods.delList(id, false)
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await CombinationActivityApi.deleteCombinationActivity(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+const formatCombinationPrice = (products) => {
+  const combinationPrice = Math.min(...products.map((item) => item.combinationPrice))
+  return `¥${fenToYuan(combinationPrice)}`
 }
 }
 
 
 /** 初始化 **/
 /** 初始化 **/
-onMounted(() => {
-  // 获得活动列表
-  sortTableColumns(allSchemas.tableColumns, 'spuId')
-  getList()
+onMounted(async () => {
+  await getList()
 })
 })
 </script>
 </script>

+ 98 - 0
src/views/mall/promotion/combination/record/CombinationRecordListDialog.vue

@@ -0,0 +1,98 @@
+<template>
+  <Dialog v-model="dialogVisible" title="拼团列表" width="950">
+    <!-- 列表 -->
+    <ContentWrap>
+      <el-table v-loading="loading" :data="list">
+        <el-table-column align="center" label="编号" prop="id" min-width="50" />
+        <el-table-column align="center" label="头像" prop="avatar" min-width="80">
+          <template #default="scope">
+            <el-avatar :src="scope.row.avatar" />
+          </template>
+        </el-table-column>
+        <el-table-column align="center" label="昵称" prop="nickname" min-width="100" />
+        <el-table-column align="center" label="开团团长" prop="headId" min-width="100">
+          <template #default="{ row }: { row: CombinationRecordApi.CombinationRecordVO }">
+            <el-tag> {{ row.headId === 0 ? '团长' : '团员' }} </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column
+          :formatter="dateFormatter"
+          align="center"
+          label="参团时间"
+          prop="createTime"
+          width="180"
+        />
+        <el-table-column
+          :formatter="dateFormatter"
+          align="center"
+          label="结束时间"
+          prop="endTime"
+          width="180"
+        />
+        <el-table-column align="center" label="拼团状态" prop="status" min-width="150">
+          <template #default="scope">
+            <dict-tag
+              :type="DICT_TYPE.PROMOTION_COMBINATION_RECORD_STATUS"
+              :value="scope.row.status"
+            />
+          </template>
+        </el-table-column>
+      </el-table>
+      <!-- 分页 -->
+      <Pagination
+        v-model:limit="queryParams.pageSize"
+        v-model:page="queryParams.pageNo"
+        :total="total"
+        @pagination="getList"
+      />
+    </ContentWrap>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { dateFormatter } from '@/utils/formatTime'
+import * as CombinationRecordApi from '@/api/mall/promotion/combination/combinationRecord'
+import { DICT_TYPE } from '@/utils/dict'
+import { createImageViewer } from '@/components/ImageViewer'
+
+/** 助力列表 */
+defineOptions({ name: 'CombinationRecordListDialog' })
+
+const message = useMessage() // 消息弹窗
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  headId: undefined
+})
+
+/** 打开弹窗 */
+const dialogVisible = ref(false) // 弹窗的是否展示
+const open = async (headId: any) => {
+  dialogVisible.value = true
+  queryParams.headId = headId
+  await getList()
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await CombinationRecordApi.getCombinationRecordPageByHeadId(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+/** 商品图预览 */
+const imagePreview = (imgUrl: string) => {
+  createImageViewer({
+    urlList: [imgUrl]
+  })
+}
+</script>

+ 269 - 2
src/views/mall/promotion/combination/record/index.vue

@@ -1,5 +1,272 @@
 <template>
 <template>
-  <div></div>
+  <!-- 统计信息展示 -->
+  <el-row :gutter="12">
+    <el-col :span="6">
+      <ContentWrap class="h-[110px] pb-0!">
+        <div class="flex items-center">
+          <div
+            class="h-[50px] w-[50px] flex items-center justify-center"
+            style="color: rgb(24, 144, 255); background-color: rgba(24, 144, 255, 0.1)"
+          >
+            <Icon :size="23" icon="fa:user-times" />
+          </div>
+          <div class="ml-[20px]">
+            <div class="mb-8px text-14px text-gray-400">参与人数(个)</div>
+            <CountTo
+              :duration="2600"
+              :end-val="recordSummary.userCount"
+              :start-val="0"
+              class="text-20px"
+            />
+          </div>
+        </div>
+      </ContentWrap>
+    </el-col>
+    <el-col :span="6">
+      <ContentWrap class="h-[110px]">
+        <div class="flex items-center">
+          <div
+            class="h-[50px] w-[50px] flex items-center justify-center"
+            style="color: rgb(162, 119, 255); background-color: rgba(162, 119, 255, 0.1)"
+          >
+            <Icon :size="23" icon="fa:user-plus" />
+          </div>
+          <div class="ml-[20px]">
+            <div class="mb-8px text-14px text-gray-400">成团数量(个)</div>
+            <CountTo
+              :duration="2600"
+              :end-val="recordSummary.successCount"
+              :start-val="0"
+              class="text-20px"
+            />
+          </div>
+        </div>
+      </ContentWrap>
+    </el-col>
+    <el-col :span="6">
+      <ContentWrap class="h-[110px]">
+        <div class="flex items-center">
+          <div
+            class="h-[50px] w-[50px] flex items-center justify-center"
+            style="color: rgb(162, 119, 255); background-color: rgba(162, 119, 255, 0.1)"
+          >
+            <Icon :size="23" icon="fa:user-plus" />
+          </div>
+          <div class="ml-[20px]">
+            <div class="mb-8px text-14px text-gray-400">虚拟成团(个)</div>
+            <CountTo
+              :duration="2600"
+              :end-val="recordSummary.virtualGroupCount"
+              :start-val="0"
+              class="text-20px"
+            />
+          </div>
+        </div>
+      </ContentWrap>
+    </el-col>
+  </el-row>
+
+  <!-- 搜索工作栏 -->
+  <ContentWrap>
+    <el-form
+      ref="queryFormRef"
+      :inline="true"
+      :model="queryParams"
+      class="-mb-15px"
+      label-width="68px"
+    >
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          :shortcuts="defaultShortcuts"
+          class="!w-240px"
+          end-placeholder="结束日期"
+          start-placeholder="开始日期"
+          type="daterange"
+          value-format="YYYY-MM-DD HH:mm:ss"
+        />
+      </el-form-item>
+      <el-form-item label="拼团状态" prop="status">
+        <el-select v-model="queryParams.status" class="!w-240px" clearable placeholder="全部">
+          <el-option
+            v-for="(dict, index) in getIntDictOptions(
+              DICT_TYPE.PROMOTION_COMBINATION_RECORD_STATUS
+            )"
+            :key="index"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery">
+          <Icon class="mr-5px" icon="ep:search" />
+          搜索
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon class="mr-5px" icon="ep:refresh" />
+          重置
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 分页列表数据展示 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="pageList">
+      <el-table-column align="center" label="编号" prop="id" min-width="50" />
+      <el-table-column align="center" label="头像" prop="avatar" min-width="80">
+        <template #default="scope">
+          <el-avatar :src="scope.row.avatar" />
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="昵称" prop="nickname" min-width="100" />
+      <el-table-column align="center" label="开团团长" prop="headId" min-width="100">
+        <template #default="{ row }: { row: CombinationRecordApi.CombinationRecordVO }">
+          {{
+            row.headId ? pageList.find((item) => item.id === row.headId)?.nickname : row.nickname
+          }}
+        </template>
+      </el-table-column>
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="开团时间"
+        prop="startTime"
+        width="180"
+      />
+      <el-table-column
+        align="center"
+        label="拼团商品"
+        prop="type"
+        show-overflow-tooltip
+        min-width="300"
+      >
+        <template #defaul="{ row }">
+          <el-image
+            :src="row.picUrl"
+            class="mr-5px h-30px w-30px align-middle"
+            @click="imagePreview(row.picUrl)"
+          />
+          <span class="align-middle">{{ row.spuName }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="几人团" prop="userSize" min-width="100" />
+      <el-table-column align="center" label="参与人数" prop="userCount" min-width="100" />
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="参团时间"
+        prop="createTime"
+        width="180"
+      />
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="结束时间"
+        prop="endTime"
+        width="180"
+      />
+      <el-table-column align="center" label="拼团状态" prop="status" min-width="150">
+        <template #default="scope">
+          <dict-tag
+            :type="DICT_TYPE.PROMOTION_COMBINATION_RECORD_STATUS"
+            :value="scope.row.status"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column align="center" fixed="right" label="操作">
+        <template #default="scope">
+          <el-button
+            v-hasPermi="['promotion:combination-record:query']"
+            link
+            type="primary"
+            @click="openRecordListDialog(scope.row)"
+          >
+            查看拼团
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗 -->
+  <CombinationRecordListDialog ref="combinationRecordListRef" />
 </template>
 </template>
+<script lang="ts" setup>
+import CombinationRecordListDialog from './CombinationRecordListDialog.vue'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter, defaultShortcuts } from '@/utils/formatTime'
+import { createImageViewer } from '@/components/ImageViewer'
+import * as CombinationRecordApi from '@/api/mall/promotion/combination/combinationRecord'
+
+defineOptions({ name: 'PromotionCombinationRecord' })
+
+const queryParams = ref({
+  status: undefined, // 拼团状态
+  createTime: undefined, // 创建时间
+  pageSize: 10,
+  pageNo: 1
+})
+const queryFormRef = ref() // 搜索的表单
+const combinationRecordListRef = ref() // 查询表单 Ref
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 总记录数
+const pageList = ref<CombinationRecordApi.CombinationRecordVO[]>([]) // 分页数据
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await CombinationRecordApi.getCombinationRecordPage(queryParams.value)
+    pageList.value = data.list as CombinationRecordApi.CombinationRecordVO[]
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+// 拼团统计数据
+const recordSummary = ref({
+  successCount: 0,
+  userCount: 0,
+  virtualGroupCount: 0
+})
+/** 获得拼团记录统计信息 */
+const getSummary = async () => {
+  recordSummary.value = await CombinationRecordApi.getCombinationRecordSummary()
+}
+
+const openRecordListDialog = (row: CombinationRecordApi.CombinationRecordVO) => {
+  combinationRecordListRef.value?.open(row.headId)
+}
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.value.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 商品图预览 */
+const imagePreview = (imgUrl: string) => {
+  createImageViewer({
+    urlList: [imgUrl]
+  })
+}
 
 
-<script lang="ts" name="CombinationRecord" setup></script>
+/** 初始化 **/
+onMounted(async () => {
+  await getSummary()
+  await getList()
+})
+</script>

+ 1 - 1
src/views/mall/promotion/coupon/formatter.ts

@@ -9,7 +9,7 @@ export const discountFormat = (row: CouponTemplateVO) => {
     return `¥${floatToFixed2(row.discountPrice)}`
     return `¥${floatToFixed2(row.discountPrice)}`
   }
   }
   if (row.discountType === PromotionDiscountTypeEnum.PERCENT.type) {
   if (row.discountType === PromotionDiscountTypeEnum.PERCENT.type) {
-    return `${row.discountPrice}%`
+    return `${row.discountPercent}%`
   }
   }
   return '未知【' + row.discountType + '】'
   return '未知【' + row.discountType + '】'
 }
 }

+ 11 - 5
src/views/mall/promotion/coupon/index.vue

@@ -19,7 +19,7 @@
           @keyup="handleQuery"
           @keyup="handleQuery"
         />
         />
       </el-form-item>
       </el-form-item>
-      <el-form-item label="创建时间" prop="createTime">
+      <el-form-item label="领取时间" prop="createTime">
         <el-date-picker
         <el-date-picker
           v-model="queryParams.createTime"
           v-model="queryParams.createTime"
           value-format="YYYY-MM-DD HH:mm:ss"
           value-format="YYYY-MM-DD HH:mm:ss"
@@ -50,12 +50,17 @@
 
 
     <!-- 列表 -->
     <!-- 列表 -->
     <el-table v-loading="loading" :data="list">
     <el-table v-loading="loading" :data="list">
-      <el-table-column label="会员信息" align="center" prop="nickname" />
-      <!-- TODO 芋艿:以后支持头像,支持跳转 -->
-      <el-table-column label="优惠劵" align="center" prop="name" />
-      <el-table-column label="优惠券类型" align="center" prop="discountType">
+      <el-table-column label="会员昵称" align="center" min-width="100" prop="nickname" />
+      <el-table-column label="优惠券名称" align="center" min-width="140" prop="name" />
+      <el-table-column label="类型" align="center" prop="discountType">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.PROMOTION_PRODUCT_SCOPE" :value="scope.row.productScope" />
+        </template>
+      </el-table-column>
+      <el-table-column label="优惠" min-width="100" prop="discount">
         <template #default="scope">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.PROMOTION_DISCOUNT_TYPE" :value="scope.row.discountType" />
           <dict-tag :type="DICT_TYPE.PROMOTION_DISCOUNT_TYPE" :value="scope.row.discountType" />
+          {{ discountFormat(scope.row) }}
         </template>
         </template>
       </el-table-column>
       </el-table-column>
       <el-table-column label="领取方式" align="center" prop="takeType">
       <el-table-column label="领取方式" align="center" prop="takeType">
@@ -109,6 +114,7 @@
 import { deleteCoupon, getCouponPage } from '@/api/mall/promotion/coupon/coupon'
 import { deleteCoupon, getCouponPage } from '@/api/mall/promotion/coupon/coupon'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
 import { dateFormatter } from '@/utils/formatTime'
+import { discountFormat } from '@/views/mall/promotion/coupon/formatter'
 
 
 defineOptions({ name: 'PromotionCoupon' })
 defineOptions({ name: 'PromotionCoupon' })
 
 

+ 18 - 21
src/views/mall/promotion/coupon/template/index.vue

@@ -19,7 +19,7 @@
           @keyup="handleQuery"
           @keyup="handleQuery"
         />
         />
       </el-form-item>
       </el-form-item>
-      <el-form-item label="优惠类型" prop="discountType">
+      <el-form-item label="优惠类型" prop="discountType">
         <el-select
         <el-select
           v-model="queryParams.discountType"
           v-model="queryParams.discountType"
           class="!w-240px"
           class="!w-240px"
@@ -71,14 +71,6 @@
         >
         >
           <Icon class="mr-5px" icon="ep:plus" /> 新增
           <Icon class="mr-5px" icon="ep:plus" /> 新增
         </el-button>
         </el-button>
-        <el-button
-          plain
-          type="success"
-          @click="$router.push('/promotion/coupon')"
-          v-hasPermi="['promotion:coupon:query']"
-        >
-          <Icon icon="ep:operation" class="mr-5px" />会员优惠劵
-        </el-button>
       </el-form-item>
       </el-form-item>
     </el-form>
     </el-form>
   </ContentWrap>
   </ContentWrap>
@@ -86,17 +78,29 @@
   <!-- 列表 -->
   <!-- 列表 -->
   <ContentWrap>
   <ContentWrap>
     <el-table v-loading="loading" :data="list">
     <el-table v-loading="loading" :data="list">
-      <el-table-column label="优惠券名称" align="center" prop="name" />
-      <el-table-column label="优惠券类型" align="center" prop="discountType">
+      <el-table-column label="优惠券名称" min-width="140" prop="name" />
+      <el-table-column label="类型" min-width="80" prop="productScope">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.PROMOTION_PRODUCT_SCOPE" :value="scope.row.productScope" />
+        </template>
+      </el-table-column>
+      <el-table-column label="优惠" min-width="100" prop="discount">
         <template #default="scope">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.PROMOTION_DISCOUNT_TYPE" :value="scope.row.discountType" />
           <dict-tag :type="DICT_TYPE.PROMOTION_DISCOUNT_TYPE" :value="scope.row.discountType" />
+          {{ discountFormat(scope.row) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="领取方式" min-width="100" prop="takeType">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.PROMOTION_COUPON_TAKE_TYPE" :value="scope.row.takeType" />
         </template>
         </template>
       </el-table-column>
       </el-table-column>
       <el-table-column
       <el-table-column
-        label="优惠金额 / 折扣"
+        label="使用时间"
         align="center"
         align="center"
-        prop="discount"
-        :formatter="discountFormat"
+        prop="validityType"
+        width="185"
+        :formatter="validityTypeFormat"
       />
       />
       <el-table-column label="发放数量" align="center" prop="totalCount" />
       <el-table-column label="发放数量" align="center" prop="totalCount" />
       <el-table-column
       <el-table-column
@@ -111,13 +115,6 @@
         prop="takeLimitCount"
         prop="takeLimitCount"
         :formatter="takeLimitCountFormat"
         :formatter="takeLimitCountFormat"
       />
       />
-      <el-table-column
-        label="有效期限"
-        align="center"
-        prop="validityType"
-        width="190"
-        :formatter="validityTypeFormat"
-      />
       <el-table-column label="状态" align="center" prop="status">
       <el-table-column label="状态" align="center" prop="status">
         <template #default="scope">
         <template #default="scope">
           <el-switch
           <el-switch

+ 212 - 90
src/views/mall/promotion/seckill/activity/index.vue

@@ -1,94 +1,206 @@
 <template>
 <template>
-  <doc-alert title="功能开启" url="https://doc.iocoder.cn/mall/build/" />
-
-  <!-- 搜索工作栏 -->
   <ContentWrap>
   <ContentWrap>
-    <Search :schema="allSchemas.searchSchema" @reset="setSearchParams" @search="setSearchParams">
-      <!-- 新增等操作按钮 -->
-      <template #actionMore>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="活动名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入活动名称"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="活动状态" prop="status">
+        <el-select
+          v-model="queryParams.status"
+          placeholder="请选择活动状态"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
         <el-button
         <el-button
-          v-hasPermi="['promotion:seckill-activity:create']"
-          plain
           type="primary"
           type="primary"
+          plain
           @click="openForm('create')"
           @click="openForm('create')"
+          v-hasPermi="['promotion:seckill-activity:create']"
         >
         >
-          <Icon class="mr-5px" icon="ep:plus" /> 新增
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
         </el-button>
         </el-button>
-      </template>
-    </Search>
+      </el-form-item>
+    </el-form>
   </ContentWrap>
   </ContentWrap>
 
 
   <!-- 列表 -->
   <!-- 列表 -->
   <ContentWrap>
   <ContentWrap>
-    <Table
-      v-model:currentPage="tableObject.currentPage"
-      v-model:pageSize="tableObject.pageSize"
-      :columns="allSchemas.tableColumns"
-      :data="tableObject.tableList"
-      :expand="true"
-      :loading="tableObject.loading"
-      :pagination="{
-        total: tableObject.total
-      }"
-      @expand-change="expandChange"
-    >
-      <template #expand> 展示活动商品和商品相关属性活动配置</template>
-      <template #spuId="{ row }">
-        <el-image
-          :src="row.picUrl"
-          class="mr-5px h-30px w-30px align-middle"
-          @click="imagePreview(row.picUrl)"
-        />
-        <span class="align-middle">{{ row.spuName }}</span>
-      </template>
-      <template #configIds="{ row }">
-        <el-tag v-for="(name, index) in convertSeckillConfigNames(row)" :key="index" class="mr-5px">
-          {{ name }}
-        </el-tag>
-      </template>
-      <template #action="{ row }">
-        <el-button
-          v-hasPermi="['promotion:seckill-activity:update']"
-          link
-          type="primary"
-          @click="openForm('update', row.id)"
-        >
-          编辑
-        </el-button>
-        <el-button
-          v-hasPermi="['promotion:seckill-activity:delete']"
-          link
-          type="danger"
-          @click="handleDelete(row.id)"
-        >
-          删除
-        </el-button>
-      </template>
-    </Table>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="活动编号" prop="id" min-width="80" />
+      <el-table-column label="活动名称" prop="name" min-width="140" />
+      <el-table-column
+        label="秒杀时段"
+        prop="configIds"
+        width="220px"
+        :show-overflow-tooltip="false"
+      >
+        <template #default="scope">
+          <el-tag v-for="(configId, index) in scope.row.configIds" :key="index" class="mr-5px">
+            {{ formatConfigNames(configId) }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="活动时间" min-width="210">
+        <template #default="scope">
+          {{ formatDate(scope.row.startTime, 'YYYY-MM-DD') }}
+          ~ {{ formatDate(scope.row.endTime, 'YYYY-MM-DD') }}
+        </template>
+      </el-table-column>
+      <el-table-column label="商品图片" prop="spuName" min-width="80">
+        <template #default="scope">
+          <el-image
+            :src="scope.row.picUrl"
+            class="h-40px w-40px"
+            :preview-src-list="[scope.row.picUrl]"
+            preview-teleported
+          />
+        </template>
+      </el-table-column>
+      <el-table-column label="商品标题" prop="spuName" min-width="300" />
+      <el-table-column
+        label="原价"
+        prop="marketPrice"
+        min-width="100"
+        :formatter="fenToYuanFormat"
+      />
+      <el-table-column label="原价" prop="marketPrice" min-width="100" />
+      <el-table-column label="秒杀价" prop="seckillPrice" min-width="100">
+        <template #default="scope">
+          {{ formatSeckillPrice(scope.row.products) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="活动状态" align="center" prop="status" min-width="100">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column label="库存" align="center" prop="stock" min-width="80" />
+      <el-table-column label="总库存" align="center" prop="totalStock" min-width="80" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="操作" align="center" width="150px" fixed="right">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['promotion:seckill-activity:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleClose(scope.row.id)"
+            v-if="scope.row.status === 0"
+            v-hasPermi="['promotion:seckill-activity:close']"
+          >
+            关闭
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-else
+            v-hasPermi="['promotion:seckill-activity:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
   </ContentWrap>
   </ContentWrap>
 
 
   <!-- 表单弹窗:添加/修改 -->
   <!-- 表单弹窗:添加/修改 -->
   <SeckillActivityForm ref="formRef" @success="getList" />
   <SeckillActivityForm ref="formRef" @success="getList" />
 </template>
 </template>
-<script lang="ts" setup>
-import { allSchemas } from './seckillActivity.data'
-import { getSimpleSeckillConfigList } from '@/api/mall/promotion/seckill/seckillConfig'
+
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
 import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity'
 import * as SeckillActivityApi from '@/api/mall/promotion/seckill/seckillActivity'
+import * as SeckillConfigApi from '@/api/mall/promotion/seckill/seckillConfig'
 import SeckillActivityForm from './SeckillActivityForm.vue'
 import SeckillActivityForm from './SeckillActivityForm.vue'
-import { createImageViewer } from '@/components/ImageViewer'
-import { sortTableColumns } from '@/hooks/web/useCrudSchemas'
+import { formatDate } from '@/utils/formatTime'
+import { fenToYuanFormat } from '@/utils/formatter'
+import { fenToYuan } from '@/utils'
 
 
-defineOptions({ name: 'PromotionSeckillActivity' })
+defineOptions({ name: 'SeckillActivity' })
 
 
-// tableObject:表格的属性对象,可获得分页大小、条数等属性
-// tableMethods:表格的操作对象,可进行获得分页、删除记录等操作
-// 详细可见:https://doc.iocoder.cn/vue3/crud-schema/
-const { tableObject, tableMethods } = useTable({
-  getListApi: SeckillActivityApi.getSeckillActivityPage, // 分页接口
-  delListApi: SeckillActivityApi.deleteSeckillActivity // 删除接口
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: null,
+  status: null
 })
 })
-// 获得表格的各种操作
-const { getList, setSearchParams } = tableMethods
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await SeckillActivityApi.getSeckillActivityPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
 
 
 /** 添加/修改操作 */
 /** 添加/修改操作 */
 const formRef = ref()
 const formRef = ref()
@@ -96,37 +208,47 @@ const openForm = (type: string, id?: number) => {
   formRef.value.open(type, id)
   formRef.value.open(type, id)
 }
 }
 
 
-/** 删除按钮操作 */
-const handleDelete = (id: number) => {
-  tableMethods.delList(id, false)
+/** 关闭按钮操作 */
+const handleClose = async (id: number) => {
+  try {
+    // 关闭的二次确认
+    await message.confirm('确认关闭该秒杀活动吗?')
+    // 发起关闭
+    await SeckillActivityApi.closeSeckillActivity(id)
+    message.success('关闭成功')
+    // 刷新列表
+    await getList()
+  } catch {}
 }
 }
 
 
-/** 商品图预览 */
-const imagePreview = (imgUrl: string) => {
-  createImageViewer({
-    urlList: [imgUrl]
-  })
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await SeckillActivityApi.deleteSeckillActivity(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
 }
 }
 
 
 const configList = ref([]) // 时段配置精简列表
 const configList = ref([]) // 时段配置精简列表
-const convertSeckillConfigNames = computed(
-  () => (row) =>
-    configList.value
-      ?.filter((item) => row.configIds.includes(item.id))
-      ?.map((config) => config.name)
-)
+const formatConfigNames = (configId) => {
+  const config = configList.value.find((item) => item.id === configId)
+  return config != null ? `${config.name}[${config.startTime} ~ ${config.endTime}]` : ''
+}
 
 
-const expandChange = (row, expandedRows) => {
-  // TODO puhui:等 CRUD 完事后弄
-  console.log(row, expandedRows)
+const formatSeckillPrice = (products) => {
+  const seckillPrice = Math.min(...products.map((item) => item.seckillPrice))
+  return `¥${fenToYuan(seckillPrice)}`
 }
 }
 
 
 /** 初始化 **/
 /** 初始化 **/
 onMounted(async () => {
 onMounted(async () => {
-  // 获得活动列表
-  sortTableColumns(allSchemas.tableColumns, 'spuId')
   await getList()
   await getList()
   // 获得秒杀时间段
   // 获得秒杀时间段
-  configList.value = await getSimpleSeckillConfigList()
+  configList.value = await SeckillConfigApi.getSimpleSeckillConfigList()
 })
 })
 </script>
 </script>

+ 0 - 96
src/views/mall/promotion/seckill/activity/seckillActivity.data.ts

@@ -94,42 +94,6 @@ const crudSchemas = reactive<CrudSchema[]>([
       width: 300
       width: 300
     }
     }
   },
   },
-  {
-    label: '新增订单数',
-    field: 'orderCount',
-    isForm: false,
-    form: {
-      component: 'InputNumber',
-      value: 0
-    },
-    table: {
-      width: 120
-    }
-  },
-  {
-    label: '付款人数',
-    field: 'userCount',
-    isForm: false,
-    form: {
-      component: 'InputNumber',
-      value: 0
-    },
-    table: {
-      width: 120
-    }
-  },
-  {
-    label: '订单实付金额',
-    field: 'totalPrice',
-    isForm: false,
-    form: {
-      component: 'InputNumber',
-      value: 0
-    },
-    table: {
-      width: 120
-    }
-  },
   {
   {
     label: '总限购数量',
     label: '总限购数量',
     field: 'totalLimitCount',
     field: 'totalLimitCount',
@@ -163,26 +127,6 @@ const crudSchemas = reactive<CrudSchema[]>([
       width: 80
       width: 80
     }
     }
   },
   },
-  {
-    label: '秒杀库存',
-    field: 'stock',
-    isForm: false,
-    form: {
-      component: 'InputNumber',
-      value: 0
-    },
-    table: {
-      width: 120
-    }
-  },
-  {
-    label: '秒杀总库存',
-    field: 'totalStock',
-    isForm: false,
-    table: {
-      width: 120
-    }
-  },
   {
   {
     label: '秒杀活动商品',
     label: '秒杀活动商品',
     field: 'spuId',
     field: 'spuId',
@@ -197,37 +141,6 @@ const crudSchemas = reactive<CrudSchema[]>([
       width: 300
       width: 300
     }
     }
   },
   },
-  {
-    label: '创建时间',
-    field: 'createTime',
-    formatter: dateFormatter,
-    search: {
-      component: 'DatePicker',
-      componentProps: {
-        valueFormat: 'YYYY-MM-DD HH:mm:ss',
-        type: 'daterange',
-        defaultTime: [new Date('1 00:00:00'), new Date('1 23:59:59')]
-      }
-    },
-    isForm: false,
-    table: {
-      width: 120
-    }
-  },
-  {
-    label: '状态',
-    field: 'status',
-    dictType: DICT_TYPE.COMMON_STATUS,
-    dictClass: 'number',
-    isForm: false,
-    isSearch: true,
-    form: {
-      component: 'Radio'
-    },
-    table: {
-      width: 80
-    }
-  },
   {
   {
     label: '备注',
     label: '备注',
     field: 'remark',
     field: 'remark',
@@ -245,15 +158,6 @@ const crudSchemas = reactive<CrudSchema[]>([
     table: {
     table: {
       width: 300
       width: 300
     }
     }
-  },
-  {
-    label: '操作',
-    field: 'action',
-    isForm: false,
-    table: {
-      width: 120,
-      fixed: 'right'
-    }
   }
   }
 ])
 ])
 export const { allSchemas } = useCrudSchemas(crudSchemas)
 export const { allSchemas } = useCrudSchemas(crudSchemas)

+ 484 - 0
src/views/mall/statistics/member/index.vue

@@ -0,0 +1,484 @@
+<template>
+  <div class="flex flex-col">
+    <el-row :gutter="16" class="summary">
+      <el-col :sm="6" :xs="12" v-loading="loading">
+        <TradeTrendValue
+          title="累计会员数"
+          icon="fa-solid:users"
+          icon-color="bg-blue-100"
+          icon-bg-color="text-blue-500"
+          :value="summary?.userCount || 0"
+        />
+      </el-col>
+      <el-col :sm="6" :xs="12" v-loading="loading">
+        <TradeTrendValue
+          title="累计充值人数"
+          icon="fa-solid:user"
+          icon-color="bg-purple-100"
+          icon-bg-color="text-purple-500"
+          :value="summary?.rechargeUserCount || 0"
+        />
+      </el-col>
+      <el-col :sm="6" :xs="12" v-loading="loading">
+        <TradeTrendValue
+          title="累计充值金额"
+          icon="fa-solid:money-check-alt"
+          icon-color="bg-yellow-100"
+          icon-bg-color="text-yellow-500"
+          prefix="¥"
+          :decimals="2"
+          :value="fenToYuan(summary?.rechargePrice || 0)"
+        />
+      </el-col>
+      <el-col :sm="6" :xs="12" v-loading="loading">
+        <TradeTrendValue
+          title="累计消费金额"
+          icon="fa-solid:yen-sign"
+          icon-color="bg-green-100"
+          icon-bg-color="text-green-500"
+          prefix="¥"
+          :decimals="2"
+          :value="fenToYuan(summary?.expensePrice || 0)"
+        />
+      </el-col>
+    </el-row>
+    <el-row :gutter="16" class="mb-4">
+      <el-col :md="18" :sm="24">
+        <el-card shadow="never">
+          <template #header>
+            <div class="flex flex-row items-center justify-between">
+              <span>会员概览</span>
+              <!-- 查询条件 -->
+              <div class="my--2 flex flex-row items-center gap-2">
+                <el-radio-group v-model="shortcutDays" @change="handleDateTypeChange">
+                  <el-radio-button :label="1">昨天</el-radio-button>
+                  <el-radio-button :label="7">最近7天</el-radio-button>
+                  <el-radio-button :label="30">最近30天</el-radio-button>
+                </el-radio-group>
+                <el-date-picker
+                  v-model="queryParams.times"
+                  value-format="YYYY-MM-DD HH:mm:ss"
+                  type="daterange"
+                  start-placeholder="开始日期"
+                  end-placeholder="结束日期"
+                  :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+                  :shortcuts="shortcuts"
+                  class="!w-240px"
+                  @change="getMemberAnalyse"
+                />
+              </div>
+            </div>
+          </template>
+          <div class="min-w-225 py-1.75" v-loading="analyseLoading">
+            <div class="relative h-24 flex">
+              <div class="h-full w-75% bg-blue-50 <lg:w-35% <xl:w-55%">
+                <div class="ml-15 h-full flex flex-col justify-center">
+                  <div class="font-bold">
+                    注册用户数量:{{ analyseData?.comparison?.value?.userCount || 0 }}
+                  </div>
+                  <div class="mt-2 text-3.5">
+                    环比增长率:{{
+                      calculateRelativeRate(
+                        analyseData?.comparison?.value?.userCount,
+                        analyseData?.comparison?.reference?.userCount
+                      )
+                    }}%
+                  </div>
+                </div>
+              </div>
+              <div
+                class="trapezoid1 ml--38.5 mt-1.5 h-full w-77 flex flex-col items-center justify-center bg-blue-5 text-3.5 text-white"
+              >
+                <span class="text-6 font-bold">{{ analyseData?.visitorCount || 0 }}</span>
+                <span>访客</span>
+              </div>
+            </div>
+            <div class="relative h-24 flex">
+              <div class="h-full w-75% flex bg-cyan-50 <lg:w-35% <xl:w-55%">
+                <div class="ml-15 h-full flex flex-col justify-center">
+                  <div class="font-bold">
+                    活跃用户数量:{{ analyseData?.comparison?.value?.activeUserCount || 0 }}
+                  </div>
+                  <div class="mt-2 text-3.5">
+                    环比增长率:{{
+                      calculateRelativeRate(
+                        analyseData?.comparison?.value?.activeUserCount,
+                        analyseData?.comparison?.reference?.activeUserCount
+                      )
+                    }}%
+                  </div>
+                </div>
+              </div>
+              <div
+                class="trapezoid2 ml--28 mt-1.7 h-25 w-56 flex flex-col items-center justify-center bg-cyan-5 text-3.5 text-white"
+              >
+                <span class="text-6 font-bold">{{ analyseData?.orderUserCount || 0 }}</span>
+                <span>下单</span>
+              </div>
+            </div>
+            <div class="relative h-24 flex">
+              <div class="w-75% flex bg-slate-50 <lg:w-35% <xl:w-55%">
+                <div class="ml-15 h-full flex flex-row gap-x-16">
+                  <div class="flex flex-col justify-center">
+                    <div class="font-bold">
+                      充值用户数量:{{ analyseData?.comparison?.value?.rechargeUserCount || 0 }}
+                    </div>
+                    <div class="mt-2 text-3.5">
+                      环比增长率:{{
+                        calculateRelativeRate(
+                          analyseData?.comparison?.value?.rechargeUserCount,
+                          analyseData?.comparison?.reference?.rechargeUserCount
+                        )
+                      }}%
+                    </div>
+                  </div>
+                  <div class="flex flex-col justify-center">
+                    <div class="font-bold">客单价:{{ fenToYuan(analyseData?.atv || 0) }}</div>
+                  </div>
+                </div>
+              </div>
+              <div
+                class="trapezoid3 ml--18 mt-3.25 h-23 w-36 flex flex-col items-center justify-center bg-slate-5 text-3.5 text-white"
+              >
+                <span class="text-6 font-bold">{{ analyseData?.payUserCount || 0 }}</span>
+                <span>成交用户</span>
+              </div>
+            </div>
+          </div>
+        </el-card>
+      </el-col>
+      <el-col :md="6" :sm="24">
+        <el-card shadow="never" header="会员终端" v-loading="loading">
+          <Echart :height="300" :options="terminalChartOptions" />
+        </el-card>
+      </el-col>
+    </el-row>
+    <el-row :gutter="16">
+      <el-col :md="18" :sm="24">
+        <el-card shadow="never" header="会员地域分布">
+          <el-row v-loading="loading">
+            <el-col :span="10">
+              <Echart :height="300" :options="areaChartOptions" />
+            </el-col>
+            <el-col :span="14">
+              <el-table :data="areaStatisticsList" :height="300">
+                <el-table-column
+                  label="省份"
+                  prop="areaName"
+                  align="center"
+                  min-width="80"
+                  show-overflow-tooltip
+                  sortable
+                  :sort-method="(obj1, obj2) => obj1.areaName.localeCompare(obj2.areaName, 'zh-CN')"
+                />
+                <el-table-column
+                  label="会员数量"
+                  prop="userCount"
+                  align="center"
+                  min-width="105"
+                  sortable
+                />
+                <el-table-column
+                  label="订单创建数量"
+                  prop="orderCreateCount"
+                  align="center"
+                  min-width="135"
+                  sortable
+                />
+                <el-table-column
+                  label="订单支付数量"
+                  prop="orderPayCount"
+                  align="center"
+                  min-width="135"
+                  sortable
+                />
+                <el-table-column
+                  label="订单支付金额"
+                  prop="orderPayPrice"
+                  align="center"
+                  min-width="135"
+                  sortable
+                  :formatter="fenToYuanFormat"
+                />
+              </el-table>
+            </el-col>
+          </el-row>
+        </el-card>
+      </el-col>
+      <el-col :md="6" :sm="24">
+        <el-card shadow="never" header="会员性别比例" v-loading="loading">
+          <Echart :height="300" :options="sexChartOptions" />
+        </el-card>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+<script lang="ts" setup>
+import * as TradeMemberApi from '@/api/mall/statistics/member'
+import TradeTrendValue from '../trade/components/TradeTrendValue.vue'
+import { EChartsOption } from 'echarts'
+import china from '@/assets/map/json/china.json'
+import dayjs from 'dayjs'
+import { fenToYuan } from '@/utils'
+import * as DateUtil from '@/utils/formatTime'
+import {
+  MemberAnalyseRespVO,
+  MemberAreaStatisticsRespVO,
+  MemberSexStatisticsRespVO,
+  MemberAnalyseReqVO,
+  MemberSummaryRespVO,
+  MemberTerminalStatisticsRespVO
+} from '@/api/mall/statistics/member'
+import { DICT_TYPE, DictDataType, getIntDictOptions } from '@/utils/dict'
+import echarts from '@/plugins/echarts'
+import { fenToYuanFormat } from '@/utils/formatter'
+
+/** 会员统计 */
+defineOptions({ name: 'MemberStatistics' })
+
+const loading = ref(true) // 加载中
+const analyseLoading = ref(true) // 会员概览加载中
+const queryParams = reactive<MemberAnalyseReqVO>({ times: ['', ''] }) // 会员概览查询参数
+const shortcutDays = ref(7) // 日期快捷天数(单选按钮组), 默认7天
+const summary = ref<MemberSummaryRespVO>() // 会员统计数据
+const analyseData = ref<MemberAnalyseRespVO>() // 会员分析数据
+const areaStatisticsList = shallowRef<MemberAreaStatisticsRespVO[]>() // 省份会员统计
+
+// 注册地图
+echarts?.registerMap('china', china!)
+
+/** 日期快捷选择 */
+const shortcuts = [
+  {
+    text: '昨天',
+    value: () => DateUtil.getDayRange(new Date(), -1)
+  },
+  {
+    text: '最近7天',
+    value: () => DateUtil.getLast7Days()
+  },
+  {
+    text: '本月',
+    value: () => [dayjs().startOf('M'), dayjs().subtract(1, 'd')]
+  },
+  {
+    text: '最近30天',
+    value: () => DateUtil.getLast30Days()
+  },
+  {
+    text: '最近1年',
+    value: () => DateUtil.getLast1Year()
+  }
+]
+
+/** 会员终端统计图配置 */
+const terminalChartOptions = reactive<EChartsOption>({
+  tooltip: {
+    trigger: 'item',
+    confine: true,
+    formatter: '{a} <br/>{b} : {c} ({d}%)'
+  },
+  legend: {
+    orient: 'vertical',
+    left: 'right'
+  },
+  roseType: 'area',
+  series: [
+    {
+      name: '会员终端',
+      type: 'pie',
+      label: {
+        show: false
+      },
+      labelLine: {
+        show: false
+      },
+      data: []
+    }
+  ]
+}) as EChartsOption
+
+/** 会员性别统计图配置 */
+const sexChartOptions = reactive<EChartsOption>({
+  tooltip: {
+    trigger: 'item',
+    confine: true,
+    formatter: '{a} <br/>{b} : {c} ({d}%)'
+  },
+  legend: {
+    orient: 'vertical',
+    left: 'right'
+  },
+  roseType: 'area',
+  series: [
+    {
+      name: '会员性别',
+      type: 'pie',
+      label: {
+        show: false
+      },
+      labelLine: {
+        show: false
+      },
+      data: []
+    }
+  ]
+}) as EChartsOption
+
+const areaChartOptions = reactive<EChartsOption>({
+  tooltip: {
+    trigger: 'item',
+    formatter: (params: any) => {
+      return `${params?.data?.areaName || params?.name}<br/>
+会员数量:${params?.data?.userCount || 0}<br/>
+订单创建数量:${params?.data?.orderCreateCount || 0}<br/>
+订单支付数量:${params?.data?.orderPayCount || 0}<br/>
+订单支付金额:${fenToYuan(params?.data?.orderPayPrice || 0)}`
+    }
+  },
+  visualMap: {
+    text: ['高', '低'],
+    realtime: false,
+    calculable: true,
+    top: 'middle',
+    inRange: {
+      color: ['#fff', '#3b82f6']
+    }
+  },
+  series: [
+    {
+      name: '会员地域分布',
+      type: 'map',
+      map: 'china',
+      roam: false,
+      selectedMode: false,
+      data: []
+    }
+  ]
+}) as EChartsOption
+
+/** 计算环比 */
+const calculateRelativeRate = (value?: number, reference?: number) => {
+  // 防止除0
+  if (!reference) return 0
+
+  return ((100 * ((value || 0) - reference)) / reference).toFixed(0)
+}
+
+/** 设置时间范围 */
+function setTimes() {
+  const beginDate = dayjs().subtract(shortcutDays.value, 'd')
+  const yesterday = dayjs().subtract(1, 'd')
+  queryParams.times = DateUtil.getDateRange(beginDate, yesterday)
+}
+
+/** 处理会员概览查询(日期单选按钮组选择后) */
+const handleDateTypeChange = async () => {
+  // 设置时间范围
+  setTimes()
+  // 查询数据
+  await getMemberAnalyse()
+}
+
+/** 查询会员统计 */
+const getMemberSummary = async () => {
+  summary.value = await TradeMemberApi.getMemberSummary()
+}
+
+/** 按照省份,查询会员统计列表 */
+const getMemberAreaStatisticsList = async () => {
+  const list = await TradeMemberApi.getMemberAreaStatisticsList()
+  areaStatisticsList.value = list.map((item: MemberAreaStatisticsRespVO) => {
+    return {
+      ...item,
+      areaName: item.areaName
+        .replace('维吾尔自治区', '')
+        .replace('壮族自治区', '')
+        .replace('回族自治区', '')
+        .replace('自治区', '')
+        .replace('省', '')
+    }
+  })
+  let min = 0
+  let max = 0
+  areaChartOptions.series[0].data = areaStatisticsList.value.map((item) => {
+    min = Math.min(min, item.orderPayCount)
+    max = Math.max(max, item.orderPayCount)
+    return { ...item, name: item.areaName, value: item.orderPayCount || 0 }
+  })
+  areaChartOptions.visualMap.min = min
+  areaChartOptions.visualMap.max = max
+}
+
+/** 按照性别,查询会员统计列表 */
+const getMemberSexStatisticsList = async () => {
+  const list = await TradeMemberApi.getMemberSexStatisticsList()
+  const dictDataList = getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)
+  sexChartOptions.series[0].data = dictDataList.map((dictData: DictDataType) => {
+    const userCount = list.find((item: MemberSexStatisticsRespVO) => item.sex === dictData.value)
+      ?.userCount
+    return {
+      name: dictData.label,
+      value: userCount || 0
+    }
+  })
+}
+
+/** 按照终端,查询会员统计列表 */
+const getMemberTerminalStatisticsList = async () => {
+  const list = await TradeMemberApi.getMemberTerminalStatisticsList()
+  const dictDataList = getIntDictOptions(DICT_TYPE.TERMINAL)
+  terminalChartOptions.series![0].data = dictDataList.map((dictData: DictDataType) => {
+    const userCount = list.find(
+      (item: MemberTerminalStatisticsRespVO) => item.terminal === dictData.value
+    )?.userCount
+    return {
+      name: dictData.label,
+      value: userCount || 0
+    }
+  })
+}
+
+/** 查询会员概览数据列表 */
+const getMemberAnalyse = async () => {
+  analyseLoading.value = true
+  const times = queryParams.times
+  // 开始与截止在同一天的, 环比出不来, 需要延长一天
+  if (DateUtil.isSameDay(times[0], times[1])) {
+    // 前天
+    times[0] = DateUtil.formatDate(dayjs(times[0]).subtract(1, 'd'))
+  }
+  // 查询数据
+  analyseData.value = await TradeMemberApi.getMemberAnalyse({ times })
+  analyseLoading.value = false
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  loading.value = true
+  await Promise.all([
+    getMemberSummary(),
+    getMemberTerminalStatisticsList(),
+    getMemberAreaStatisticsList(),
+    getMemberSexStatisticsList(),
+    handleDateTypeChange()
+  ])
+  loading.value = false
+})
+</script>
+<style lang="scss" scoped>
+.summary {
+  .el-col {
+    margin-bottom: 1rem;
+  }
+}
+.trapezoid1 {
+  transform: perspective(5em) rotateX(-11deg);
+}
+.trapezoid2 {
+  transform: perspective(7em) rotateX(-20deg);
+}
+.trapezoid3 {
+  transform: perspective(3em) rotateX(-13deg);
+}
+</style>

+ 36 - 0
src/views/mall/statistics/trade/components/TradeStatisticValue.vue

@@ -0,0 +1,36 @@
+<template>
+  <div class="flex flex-col gap-2 bg-[var(--el-bg-color-overlay)] p-6">
+    <div class="flex items-center justify-between text-gray-500">
+      <span>{{ title }}</span>
+      <el-tooltip :content="tooltip" placement="top-start" v-if="tooltip">
+        <Icon icon="ep:warning" />
+      </el-tooltip>
+    </div>
+    <div class="mb-4 text-3xl">
+      <CountTo :prefix="prefix" :end-val="value" :decimals="decimals" />
+    </div>
+    <div class="flex flex-row gap-1 text-sm">
+      <span class="text-gray-500">环比</span>
+      <span :class="toNumber(percent) > 0 ? 'text-red-500' : 'text-green-500'">
+        {{ Math.abs(toNumber(percent)) }}%
+        <Icon :icon="toNumber(percent) > 0 ? 'ep:caret-top' : 'ep:caret-bottom'" class="!text-sm" />
+      </span>
+    </div>
+  </div>
+</template>
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+import { toNumber } from 'lodash-es'
+
+/** 交易统计值组件 */
+defineOptions({ name: 'TradeStatisticValue' })
+
+defineProps({
+  tooltip: propTypes.string.def(''),
+  title: propTypes.string.def(''),
+  prefix: propTypes.string.def(''),
+  value: propTypes.number.def(0),
+  decimals: propTypes.number.def(0),
+  percent: propTypes.oneOfType([Number, String]).def(0)
+})
+</script>

+ 52 - 0
src/views/mall/statistics/trade/components/TradeTrendValue.vue

@@ -0,0 +1,52 @@
+<template>
+  <div class="flex flex-row items-center gap-3 rounded bg-[var(--el-bg-color-overlay)] p-4">
+    <div
+      class="h-12 w-12 flex flex-shrink-0 items-center justify-center rounded-1"
+      :class="`${iconColor} ${iconBgColor}`"
+    >
+      <Icon :icon="icon" class="!text-6" />
+    </div>
+    <div class="flex flex-col gap-1">
+      <div class="flex items-center gap-1 text-gray-500">
+        <span class="text-3.5">{{ title }}</span>
+        <el-tooltip :content="tooltip" placement="top-start" v-if="tooltip">
+          <Icon icon="ep:warning" class="item-center flex !text-3" />
+        </el-tooltip>
+      </div>
+      <div class="flex flex-row items-baseline gap-2">
+        <div class="text-7">
+          <CountTo :prefix="prefix" :end-val="value" :decimals="decimals" />
+        </div>
+        <span
+          v-if="percent != undefined"
+          :class="toNumber(percent) > 0 ? 'text-red-500' : 'text-green-500'"
+        >
+          <span class="text-sm">{{ Math.abs(toNumber(percent)) }}%</span>
+          <Icon
+            :icon="toNumber(percent) > 0 ? 'ep:caret-top' : 'ep:caret-bottom'"
+            class="ml-0.5 !text-3"
+          />
+        </span>
+      </div>
+    </div>
+  </div>
+</template>
+<script lang="ts" setup>
+import { propTypes } from '@/utils/propTypes'
+import { toNumber } from 'lodash-es'
+
+/** 交易状况统计值组件 */
+defineOptions({ name: 'TradeTrendValue' })
+
+defineProps({
+  title: propTypes.string.def(''),
+  tooltip: propTypes.string.def(''),
+  icon: propTypes.string.def(''),
+  iconColor: propTypes.string.def(''),
+  iconBgColor: propTypes.string.def(''),
+  prefix: propTypes.string.def(''),
+  value: propTypes.number.def(0),
+  decimals: propTypes.number.def(0),
+  percent: propTypes.oneOfType([Number, String]).def(undefined)
+})
+</script>

+ 428 - 0
src/views/mall/statistics/trade/index.vue

@@ -0,0 +1,428 @@
+<template>
+  <div class="flex flex-col">
+    <el-row :gutter="16" class="summary">
+      <el-col :sm="6" :xs="12">
+        <TradeStatisticValue
+          tooltip="昨日订单数量"
+          title="昨日订单数量"
+          :value="summary?.value?.yesterdayOrderCount || 0"
+          :percent="
+            calculateRelativeRate(
+              summary?.value?.yesterdayOrderCount,
+              summary?.reference?.yesterdayOrderCount
+            )
+          "
+        />
+      </el-col>
+      <el-col :sm="6" :xs="12">
+        <TradeStatisticValue
+          tooltip="本月订单数量"
+          title="本月订单数量"
+          :value="summary?.value?.monthOrderCount || 0"
+          :percent="
+            calculateRelativeRate(
+              summary?.value?.monthOrderCount,
+              summary?.reference?.monthOrderCount
+            )
+          "
+        />
+      </el-col>
+      <el-col :sm="6" :xs="12">
+        <TradeStatisticValue
+          tooltip="昨日支付金额"
+          title="昨日支付金额"
+          prefix="¥"
+          :decimals="2"
+          :value="fenToYuan(summary?.value?.yesterdayPayPrice || 0)"
+          :percent="
+            calculateRelativeRate(
+              summary?.value?.yesterdayPayPrice,
+              summary?.reference?.yesterdayPayPrice
+            )
+          "
+        />
+      </el-col>
+      <el-col :sm="6" :xs="12">
+        <TradeStatisticValue
+          tooltip="本月支付金额"
+          title="本月支付金额"
+          prefix="¥"
+          ::decimals="2"
+          :value="fenToYuan(summary?.value?.monthPayPrice || 0)"
+          :percent="
+            calculateRelativeRate(summary?.value?.monthPayPrice, summary?.reference?.monthPayPrice)
+          "
+        />
+      </el-col>
+    </el-row>
+    <el-card shadow="never">
+      <template #header>
+        <!-- 标题 -->
+        <div class="flex flex-row items-center justify-between">
+          <span>交易状况</span>
+          <!-- 查询条件 -->
+          <div class="flex flex-row items-center gap-2">
+            <el-radio-group v-model="shortcutDays" @change="handleDateTypeChange">
+              <el-radio-button :label="1">昨天</el-radio-button>
+              <el-radio-button :label="7">最近7天</el-radio-button>
+              <el-radio-button :label="30">最近30天</el-radio-button>
+            </el-radio-group>
+            <el-date-picker
+              v-model="queryParams.times"
+              value-format="YYYY-MM-DD HH:mm:ss"
+              type="daterange"
+              start-placeholder="开始日期"
+              end-placeholder="结束日期"
+              :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+              :shortcuts="shortcuts"
+              class="!w-240px"
+              @change="getTradeTrendData"
+            />
+            <el-button
+              class="ml-4"
+              @click="handleExport"
+              :loading="exportLoading"
+              v-hasPermi="['statistics:trade:export']"
+            >
+              <Icon icon="ep:download" class="mr-1" />导出
+            </el-button>
+          </div>
+        </div>
+      </template>
+      <!-- 统计值 -->
+      <el-row :gutter="16">
+        <el-col :md="6" :sm="12" :xs="24">
+          <TradeTrendValue
+            title="营业额"
+            tooltip="商品支付金额、充值金额"
+            icon="fa-solid:yen-sign"
+            icon-color="bg-blue-100"
+            icon-bg-color="text-blue-500"
+            prefix="¥"
+            :decimals="2"
+            :value="fenToYuan(trendSummary?.value?.turnover || 0)"
+            :percent="
+              calculateRelativeRate(
+                trendSummary?.value?.turnover,
+                trendSummary?.reference?.turnover
+              )
+            "
+          />
+        </el-col>
+        <el-col :md="6" :sm="12" :xs="24">
+          <TradeTrendValue
+            title="商品支付金额"
+            tooltip="用户购买商品的实际支付金额,包括微信支付、余额支付、支付宝支付、线下支付金额(拼团商品在成团之后计入,线下支付订单在后台确认支付后计入)"
+            icon="fa-solid:shopping-cart"
+            icon-color="bg-purple-100"
+            icon-bg-color="text-purple-500"
+            prefix="¥"
+            :decimals="2"
+            :value="fenToYuan(trendSummary?.value?.orderPayPrice || 0)"
+            :percent="
+              calculateRelativeRate(
+                trendSummary?.value?.orderPayPrice,
+                trendSummary?.reference?.orderPayPrice
+              )
+            "
+          />
+        </el-col>
+        <el-col :md="6" :sm="12" :xs="24">
+          <TradeTrendValue
+            title="充值金额"
+            tooltip="用户成功充值的金额"
+            icon="fa-solid:money-check-alt"
+            icon-color="bg-yellow-100"
+            icon-bg-color="text-yellow-500"
+            prefix="¥"
+            :decimals="2"
+            :value="fenToYuan(trendSummary?.value?.rechargePrice || 0)"
+            :percent="
+              calculateRelativeRate(
+                trendSummary?.value?.rechargePrice,
+                trendSummary?.reference?.rechargePrice
+              )
+            "
+          />
+        </el-col>
+        <el-col :md="6" :sm="12" :xs="24">
+          <TradeTrendValue
+            title="支出金额"
+            tooltip="余额支付金额、支付佣金金额、商品退款金额"
+            icon="ep:warning-filled"
+            icon-color="bg-green-100"
+            icon-bg-color="text-green-500"
+            prefix="¥"
+            :decimals="2"
+            :value="fenToYuan(trendSummary?.value?.expensePrice || 0)"
+            :percent="
+              calculateRelativeRate(
+                trendSummary?.value?.expensePrice,
+                trendSummary?.reference?.expensePrice
+              )
+            "
+          />
+        </el-col>
+        <el-col :md="6" :sm="12" :xs="24">
+          <TradeTrendValue
+            title="余额支付金额"
+            tooltip="用户下单时使用余额实际支付的金额"
+            icon="fa-solid:wallet"
+            icon-color="bg-cyan-100"
+            icon-bg-color="text-cyan-500"
+            prefix="¥"
+            :decimals="2"
+            :value="fenToYuan(trendSummary?.value?.balancePrice || 0)"
+            :percent="
+              calculateRelativeRate(
+                trendSummary?.value?.balancePrice,
+                trendSummary?.reference?.balancePrice
+              )
+            "
+          />
+        </el-col>
+        <el-col :md="6" :sm="12" :xs="24">
+          <TradeTrendValue
+            title="支付佣金金额"
+            tooltip="后台给推广员支付的推广佣金,以实际支付为准"
+            icon="fa-solid:award"
+            icon-color="bg-yellow-100"
+            icon-bg-color="text-yellow-500"
+            prefix="¥"
+            :decimals="2"
+            :value="fenToYuan(trendSummary?.value?.brokerageSettlementPrice || 0)"
+            :percent="
+              calculateRelativeRate(
+                trendSummary?.value?.brokerageSettlementPrice,
+                trendSummary?.reference?.brokerageSettlementPrice
+              )
+            "
+          />
+        </el-col>
+        <el-col :md="6" :sm="12" :xs="24">
+          <TradeTrendValue
+            title="商品退款金额"
+            tooltip="用户成功退款的商品金额"
+            icon="fa-solid:times-circle"
+            icon-color="bg-blue-100"
+            icon-bg-color="text-blue-500"
+            prefix="¥"
+            :decimals="2"
+            :value="fenToYuan(trendSummary?.value?.orderRefundPrice || 0)"
+            :percent="
+              calculateRelativeRate(
+                trendSummary?.value?.orderRefundPrice,
+                trendSummary?.reference?.orderRefundPrice
+              )
+            "
+          />
+        </el-col>
+      </el-row>
+      <!-- 折线图 -->
+      <el-skeleton :loading="trendLoading" animated>
+        <Echart :height="500" :options="lineChartOptions" />
+      </el-skeleton>
+    </el-card>
+  </div>
+</template>
+<script lang="ts" setup>
+import * as TradeStatisticsApi from '@/api/mall/statistics/trade'
+import TradeStatisticValue from './components/TradeStatisticValue.vue'
+import TradeTrendValue from './components/TradeTrendValue.vue'
+import { EChartsOption } from 'echarts'
+import {
+  TradeStatisticsComparisonRespVO,
+  TradeSummaryRespVO,
+  TradeTrendReqVO,
+  TradeTrendSummaryRespVO
+} from '@/api/mall/statistics/trade'
+import dayjs from 'dayjs'
+import { fenToYuan } from '@/utils'
+import * as DateUtil from '@/utils/formatTime'
+import download from '@/utils/download'
+
+/** 交易统计 */
+defineOptions({ name: 'TradeStatistics' })
+
+const message = useMessage() // 消息弹窗
+
+const loading = ref(true) // 加载中
+const trendLoading = ref(true) // 交易状态加载中
+const exportLoading = ref(false) // 导出的加载中
+const queryParams = reactive<TradeTrendReqVO>({ times: ['', ''] }) // 交易状况查询参数
+const shortcutDays = ref(7) // 日期快捷天数(单选按钮组), 默认7天
+const summary = ref<TradeStatisticsComparisonRespVO<TradeSummaryRespVO>>() // 交易统计数据
+const trendSummary = ref<TradeStatisticsComparisonRespVO<TradeTrendSummaryRespVO>>() // 交易状况统计数据
+
+/** 日期快捷选择 */
+const shortcuts = [
+  {
+    text: '昨天',
+    value: () => DateUtil.getDayRange(new Date(), -1)
+  },
+  {
+    text: '最近7天',
+    value: () => DateUtil.getLast7Days()
+  },
+  {
+    text: '本月',
+    value: () => [dayjs().startOf('M'), dayjs().subtract(1, 'd')]
+  },
+  {
+    text: '最近30天',
+    value: () => DateUtil.getLast30Days()
+  },
+  {
+    text: '最近1年',
+    value: () => DateUtil.getLast1Year()
+  }
+]
+
+/** 折线图配置 */
+const lineChartOptions = reactive<EChartsOption>({
+  dataset: {
+    dimensions: ['date', 'turnover', 'orderPayPrice', 'rechargePrice', 'expensePrice'],
+    source: []
+  },
+  grid: {
+    left: 20,
+    right: 20,
+    bottom: 20,
+    top: 80,
+    containLabel: true
+  },
+  legend: {
+    top: 50
+  },
+  series: [
+    { name: '营业额', type: 'line', smooth: true },
+    { name: '商品支付金额', type: 'line', smooth: true },
+    { name: '充值金额', type: 'line', smooth: true },
+    { name: '支出金额', type: 'line', smooth: true }
+  ],
+  toolbox: {
+    feature: {
+      // 数据区域缩放
+      dataZoom: {
+        yAxisIndex: false // Y轴不缩放
+      },
+      brush: {
+        type: ['lineX', 'clear'] // 区域缩放按钮、还原按钮
+      },
+      saveAsImage: { show: true, name: '交易状况' } // 保存为图片
+    }
+  },
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'cross'
+    },
+    padding: [5, 10]
+  },
+  xAxis: {
+    type: 'category',
+    boundaryGap: false,
+    axisTick: {
+      show: false
+    }
+  },
+  yAxis: {
+    axisTick: {
+      show: false
+    }
+  }
+}) as EChartsOption
+
+/** 计算环比 */
+const calculateRelativeRate = (value?: number, reference?: number) => {
+  // 防止除0
+  if (!reference) return 0
+
+  return ((100 * ((value || 0) - reference)) / reference).toFixed(0)
+}
+
+/** 设置时间范围 */
+function setTimes() {
+  const beginDate = dayjs().subtract(shortcutDays.value, 'd')
+  const yesterday = dayjs().subtract(1, 'd')
+  queryParams.times = DateUtil.getDateRange(beginDate, yesterday)
+}
+
+/** 处理交易状况查询(日期单选按钮组选择后) */
+const handleDateTypeChange = async () => {
+  // 设置时间范围
+  setTimes()
+  // 查询数据
+  await getTradeTrendData()
+}
+
+/** 处理交易状况查询 */
+const getTradeTrendData = async () => {
+  trendLoading.value = true
+  await Promise.all([getTradeTrendSummary(), getTradeTrendList()])
+  trendLoading.value = false
+}
+
+/** 查询交易统计 */
+const getTradeStatisticsSummary = async () => {
+  summary.value = await TradeStatisticsApi.getTradeStatisticsSummary()
+}
+
+/** 查询交易状况数据统计 */
+const getTradeTrendSummary = async () => {
+  loading.value = true
+  trendSummary.value = await TradeStatisticsApi.getTradeTrendSummary(queryParams)
+  loading.value = false
+}
+
+/** 查询交易状况数据列表 */
+const getTradeTrendList = async () => {
+  const times = queryParams.times
+  // 开始与截止在同一天的, 折线图出不来, 需要延长一天
+  if (DateUtil.isSameDay(times[0], times[1])) {
+    // 前天
+    times[0] = DateUtil.formatDate(dayjs(times[0]).subtract(1, 'd'))
+  }
+  // 查询数据
+  const list = await TradeStatisticsApi.getTradeTrendList({ times })
+  // 处理数据
+  for (let item of list) {
+    item.turnover = fenToYuan(item.turnover)
+    item.orderPayPrice = fenToYuan(item.orderPayPrice)
+    item.rechargePrice = fenToYuan(item.rechargePrice)
+    item.expensePrice = fenToYuan(item.expensePrice)
+  }
+  // 更新 Echarts 数据
+  if (lineChartOptions.dataset && lineChartOptions.dataset['source']) {
+    lineChartOptions.dataset['source'] = list
+  }
+}
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await TradeStatisticsApi.exportTradeTrend(queryParams)
+    download.excel(data, '交易状况.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+
+/** 初始化 **/
+onMounted(async () => {
+  await getTradeStatisticsSummary()
+  await handleDateTypeChange()
+})
+</script>
+<style lang="scss" scoped>
+.summary {
+  .el-col {
+    margin-bottom: 1rem;
+  }
+}
+</style>

+ 42 - 38
src/views/mall/trade/afterSale/detail/index.vue

@@ -6,7 +6,6 @@
       <el-descriptions-item label="配送方式: ">
       <el-descriptions-item label="配送方式: ">
         <dict-tag :type="DICT_TYPE.TRADE_DELIVERY_TYPE" :value="formData.order.deliveryType" />
         <dict-tag :type="DICT_TYPE.TRADE_DELIVERY_TYPE" :value="formData.order.deliveryType" />
       </el-descriptions-item>
       </el-descriptions-item>
-      <!-- TODO 营销活动待实现 -->
       <el-descriptions-item label="订单类型: ">
       <el-descriptions-item label="订单类型: ">
         <dict-tag :type="DICT_TYPE.TRADE_ORDER_TYPE" :value="formData.order.type" />
         <dict-tag :type="DICT_TYPE.TRADE_ORDER_TYPE" :value="formData.order.type" />
       </el-descriptions-item>
       </el-descriptions-item>
@@ -29,8 +28,7 @@
       <el-descriptions-item label="付款方式: ">
       <el-descriptions-item label="付款方式: ">
         <dict-tag :type="DICT_TYPE.PAY_CHANNEL_CODE" :value="formData.order.payChannelCode" />
         <dict-tag :type="DICT_TYPE.PAY_CHANNEL_CODE" :value="formData.order.payChannelCode" />
       </el-descriptions-item>
       </el-descriptions-item>
-      <!-- TODO 芋艿:待实现:跳转会员 -->
-      <!-- <el-descriptions-item label="买家: ">{{ formData.user.nickname }}</el-descriptions-item> -->
+      <el-descriptions-item label="买家: ">{{ formData?.user?.nickname }}</el-descriptions-item>
     </el-descriptions>
     </el-descriptions>
 
 
     <!-- 售后信息 -->
     <!-- 售后信息 -->
@@ -46,7 +44,7 @@
         <dict-tag :type="DICT_TYPE.TRADE_AFTER_SALE_WAY" :value="formData.way" />
         <dict-tag :type="DICT_TYPE.TRADE_AFTER_SALE_WAY" :value="formData.way" />
       </el-descriptions-item>
       </el-descriptions-item>
       <el-descriptions-item label="退款金额: ">
       <el-descriptions-item label="退款金额: ">
-        {{ floatToFixed2(formData.refundPrice) }}
+        {{ fenToYuan(formData.refundPrice) }}
       </el-descriptions-item>
       </el-descriptions-item>
       <el-descriptions-item label="退款原因: ">{{ formData.applyReason }}</el-descriptions-item>
       <el-descriptions-item label="退款原因: ">{{ formData.applyReason }}</el-descriptions-item>
       <el-descriptions-item label="补充描述: ">
       <el-descriptions-item label="补充描述: ">
@@ -92,7 +90,7 @@
       <el-descriptions-item labelClassName="no-colon">
       <el-descriptions-item labelClassName="no-colon">
         <el-row :gutter="20">
         <el-row :gutter="20">
           <el-col :span="15">
           <el-col :span="15">
-            <el-table :data="formData.items" border>
+            <el-table :data="[formData.orderItem]" border>
               <el-table-column label="商品" prop="spuName" width="auto">
               <el-table-column label="商品" prop="spuName" width="auto">
                 <template #default="{ row }">
                 <template #default="{ row }">
                   {{ row.spuName }}
                   {{ row.spuName }}
@@ -102,19 +100,11 @@
                 </template>
                 </template>
               </el-table-column>
               </el-table-column>
               <el-table-column label="商品原价" prop="price" width="150">
               <el-table-column label="商品原价" prop="price" width="150">
-                <template #default="{ row }">{{ floatToFixed2(row.price) }}元</template>
+                <template #default="{ row }">{{ fenToYuan(row.price) }} 元</template>
               </el-table-column>
               </el-table-column>
               <el-table-column label="数量" prop="count" width="100" />
               <el-table-column label="数量" prop="count" width="100" />
               <el-table-column label="合计" prop="payPrice" width="150">
               <el-table-column label="合计" prop="payPrice" width="150">
-                <template #default="{ row }">{{ floatToFixed2(row.payPrice) }}元</template>
-              </el-table-column>
-              <el-table-column label="售后状态" prop="afterSaleStatus" width="120">
-                <template #default="{ row }">
-                  <dict-tag
-                    :type="DICT_TYPE.TRADE_ORDER_ITEM_AFTER_SALE_STATUS"
-                    :value="row.afterSaleStatus"
-                  />
-                </template>
+                <template #default="{ row }">{{ fenToYuan(row.payPrice) }} 元</template>
               </el-table-column>
               </el-table-column>
             </el-table>
             </el-table>
           </el-col>
           </el-col>
@@ -122,6 +112,8 @@
         </el-row>
         </el-row>
       </el-descriptions-item>
       </el-descriptions-item>
     </el-descriptions>
     </el-descriptions>
+
+    <!-- 操作日志 -->
     <el-descriptions title="售后日志">
     <el-descriptions title="售后日志">
       <el-descriptions-item labelClassName="no-colon">
       <el-descriptions-item labelClassName="no-colon">
         <el-timeline>
         <el-timeline>
@@ -153,7 +145,7 @@
 </template>
 </template>
 <script lang="ts" setup>
 <script lang="ts" setup>
 import * as AfterSaleApi from '@/api/mall/trade/afterSale/index'
 import * as AfterSaleApi from '@/api/mall/trade/afterSale/index'
-import { floatToFixed2 } from '@/utils'
+import { fenToYuan } from '@/utils'
 import { DICT_TYPE, getDictLabel, getDictObj } from '@/utils/dict'
 import { DICT_TYPE, getDictLabel, getDictObj } from '@/utils/dict'
 import { formatDate } from '@/utils/formatTime'
 import { formatDate } from '@/utils/formatTime'
 import UpdateAuditReasonForm from '@/views/mall/trade/afterSale/form/AfterSaleDisagreeForm.vue'
 import UpdateAuditReasonForm from '@/views/mall/trade/afterSale/form/AfterSaleDisagreeForm.vue'
@@ -191,7 +183,7 @@ const getUserTypeColor = (type: number) => {
 
 
 /** 获得详情 */
 /** 获得详情 */
 const getDetail = async () => {
 const getDetail = async () => {
-  const id = params.orderId as unknown as number
+  const id = params.id as unknown as number
   if (id) {
   if (id) {
     const res = await AfterSaleApi.getAfterSale(id)
     const res = await AfterSaleApi.getAfterSale(id)
     // 没有表单信息则关闭页面返回
     // 没有表单信息则关闭页面返回
@@ -204,44 +196,56 @@ const getDetail = async () => {
 }
 }
 
 
 /** 同意售后 */
 /** 同意售后 */
-const agree = () => {
-  message.confirm('是否同意售后?').then(() => {
-    AfterSaleApi.agree(formData.value.id)
+const agree = async () => {
+  try {
+    // 二次确认
+    await message.confirm('是否同意售后?')
+    await AfterSaleApi.agree(formData.value.id)
+    // 提示成功
     message.success(t('common.success'))
     message.success(t('common.success'))
-    getDetail()
-  })
+    await getDetail()
+  } catch {}
 }
 }
 
 
 /** 拒绝售后 */
 /** 拒绝售后 */
-const disagree = () => {
+const disagree = async () => {
   updateAuditReasonFormRef.value?.open(formData.value)
   updateAuditReasonFormRef.value?.open(formData.value)
 }
 }
 
 
 /** 确认收货 */
 /** 确认收货 */
-const receive = () => {
-  message.confirm('是否确认收货?').then(() => {
-    AfterSaleApi.receive(formData.value.id)
+const receive = async () => {
+  try {
+    // 二次确认
+    await message.confirm('是否确认收货?')
+    await AfterSaleApi.receive(formData.value.id)
+    // 提示成功
     message.success(t('common.success'))
     message.success(t('common.success'))
-    getDetail()
-  })
+    await getDetail()
+  } catch {}
 }
 }
 
 
 /** 拒绝收货 */
 /** 拒绝收货 */
-const refuse = () => {
-  message.confirm('是否拒绝收货?').then(() => {
-    AfterSaleApi.refuse(formData.value.id)
+const refuse = async () => {
+  try {
+    // 二次确认
+    await message.confirm('是否拒绝收货?')
+    await AfterSaleApi.refuse(formData.value.id)
+    // 提示成功
     message.success(t('common.success'))
     message.success(t('common.success'))
-    getDetail()
-  })
+    await getDetail()
+  } catch {}
 }
 }
 
 
 /** 确认退款 */
 /** 确认退款 */
-const refund = () => {
-  message.confirm('是否确认退款?').then(() => {
-    AfterSaleApi.refund(formData.value.id)
+const refund = async () => {
+  try {
+    // 二次确认
+    await message.confirm('是否确认退款?')
+    await AfterSaleApi.refund(formData.value.id)
+    // 提示成功
     message.success(t('common.success'))
     message.success(t('common.success'))
-    getDetail()
-  })
+    await getDetail()
+  } catch {}
 }
 }
 
 
 /** 图片预览 */
 /** 图片预览 */

+ 4 - 5
src/views/mall/trade/afterSale/index.vue

@@ -135,17 +135,16 @@
       </el-table-column>
       </el-table-column>
       <el-table-column align="center" label="订单金额" prop="refundPrice">
       <el-table-column align="center" label="订单金额" prop="refundPrice">
         <template #default="scope">
         <template #default="scope">
-          <span>{{ floatToFixed2(scope.row.refundPrice) }}元</span>
+          <span>{{ fenToYuan(scope.row.refundPrice) }} 元</span>
         </template>
         </template>
       </el-table-column>
       </el-table-column>
-      <!-- TODO 芋艿:未来要加个会员链接 -->
       <el-table-column align="center" label="买家" prop="user.nickname" />
       <el-table-column align="center" label="买家" prop="user.nickname" />
       <el-table-column align="center" label="申请时间" prop="createTime" width="180">
       <el-table-column align="center" label="申请时间" prop="createTime" width="180">
         <template #default="scope">
         <template #default="scope">
           <span>{{ formatDate(scope.row.createTime) }}</span>
           <span>{{ formatDate(scope.row.createTime) }}</span>
         </template>
         </template>
       </el-table-column>
       </el-table-column>
-      <el-table-column align="center" label="售后状态">
+      <el-table-column align="center" label="售后状态" width="100">
         <template #default="scope">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.TRADE_AFTER_SALE_STATUS" :value="scope.row.status" />
           <dict-tag :type="DICT_TYPE.TRADE_AFTER_SALE_STATUS" :value="scope.row.status" />
         </template>
         </template>
@@ -177,7 +176,7 @@ import { formatDate } from '@/utils/formatTime'
 import { createImageViewer } from '@/components/ImageViewer'
 import { createImageViewer } from '@/components/ImageViewer'
 import { TabsPaneContext } from 'element-plus'
 import { TabsPaneContext } from 'element-plus'
 import { cloneDeep } from 'lodash-es'
 import { cloneDeep } from 'lodash-es'
-import { floatToFixed2 } from '@/utils'
+import { fenToYuan } from '@/utils'
 
 
 defineOptions({ name: 'TradeAfterSale' })
 defineOptions({ name: 'TradeAfterSale' })
 
 
@@ -240,7 +239,7 @@ const tabClick = async (tab: TabsPaneContext) => {
 
 
 /** 处理退款 */
 /** 处理退款 */
 const openAfterSaleDetail = (id: number) => {
 const openAfterSaleDetail = (id: number) => {
-  push({ name: 'TradeAfterSaleDetail', params: { orderId: id } })
+  push({ name: 'TradeAfterSaleDetail', params: { id } })
 }
 }
 
 
 /** 查看订单详情 */
 /** 查看订单详情 */

+ 2 - 2
src/views/mall/trade/brokerage/record/index.vue

@@ -96,14 +96,14 @@
         align="center"
         align="center"
         prop="unfreezeTime"
         prop="unfreezeTime"
         :formatter="dateFormatter"
         :formatter="dateFormatter"
-        width="170px"
+        width="180px"
       />
       />
       <el-table-column
       <el-table-column
         label="创建时间"
         label="创建时间"
         align="center"
         align="center"
         prop="createTime"
         prop="createTime"
         :formatter="dateFormatter"
         :formatter="dateFormatter"
-        width="170px"
+        width="180px"
       />
       />
     </el-table>
     </el-table>
     <!-- 分页 -->
     <!-- 分页 -->

+ 1 - 1
src/views/mall/trade/brokerage/user/BrokerageOrderListDialog.vue

@@ -77,7 +77,7 @@
           align="center"
           align="center"
           prop="createTime"
           prop="createTime"
           :formatter="dateFormatter"
           :formatter="dateFormatter"
-          width="170px"
+          width="180px"
         />
         />
       </el-table>
       </el-table>
       <!-- 分页 -->
       <!-- 分页 -->

+ 1 - 1
src/views/mall/trade/brokerage/user/BrokerageUserListDialog.vue

@@ -67,7 +67,7 @@
           align="center"
           align="center"
           prop="bindUserTime"
           prop="bindUserTime"
           :formatter="dateFormatter"
           :formatter="dateFormatter"
-          width="170px"
+          width="180px"
         />
         />
       </el-table>
       </el-table>
       <!-- 分页 -->
       <!-- 分页 -->

+ 2 - 2
src/views/mall/trade/brokerage/user/UpdateBindUserForm.vue

@@ -19,6 +19,7 @@
         </el-input>
         </el-input>
       </el-form-item>
       </el-form-item>
     </el-form>
     </el-form>
+    <!-- 展示上级推广人的信息 -->
     <el-descriptions v-if="bindUser" :column="1" border>
     <el-descriptions v-if="bindUser" :column="1" border>
       <el-descriptions-item label="头像">
       <el-descriptions-item label="头像">
         <el-avatar :src="bindUser.avatar" />
         <el-avatar :src="bindUser.avatar" />
@@ -79,7 +80,7 @@ const submitForm = async () => {
   if (!formRef) return
   if (!formRef) return
   const valid = await formRef.value.validate()
   const valid = await formRef.value.validate()
   if (!valid) return
   if (!valid) return
-
+  // 未查找到合适的上级
   if (!bindUser.value) {
   if (!bindUser.value) {
     message.error('请先查询并确认推广人')
     message.error('请先查询并确认推广人')
     return
     return
@@ -116,7 +117,6 @@ const handleGetUser = async () => {
     message.error('不能绑定自己为推广人')
     message.error('不能绑定自己为推广人')
     return
     return
   }
   }
-
   formLoading.value = true
   formLoading.value = true
   bindUser.value = await BrokerageUserApi.getBrokerageUser(formData.value.bindUserId)
   bindUser.value = await BrokerageUserApi.getBrokerageUser(formData.value.bindUserId)
   if (!bindUser.value) {
   if (!bindUser.value) {

+ 4 - 4
src/views/mall/trade/brokerage/user/index.vue

@@ -109,7 +109,7 @@
         align="center"
         align="center"
         prop="brokerageTime"
         prop="brokerageTime"
         :formatter="dateFormatter"
         :formatter="dateFormatter"
-        width="170px"
+        width="180px"
       />
       />
       <el-table-column label="上级推广员编号" align="center" prop="bindUserId" width="150px" />
       <el-table-column label="上级推广员编号" align="center" prop="bindUserId" width="150px" />
       <el-table-column
       <el-table-column
@@ -117,7 +117,7 @@
         align="center"
         align="center"
         prop="bindUserTime"
         prop="bindUserTime"
         :formatter="dateFormatter"
         :formatter="dateFormatter"
-        width="170px"
+        width="180px"
       />
       />
       <el-table-column label="操作" align="center" width="150px" fixed="right">
       <el-table-column label="操作" align="center" width="150px" fixed="right">
         <template #default="scope">
         <template #default="scope">
@@ -204,7 +204,7 @@ const queryParams = reactive({
   pageNo: 1,
   pageNo: 1,
   pageSize: 10,
   pageSize: 10,
   bindUserId: null,
   bindUserId: null,
-  brokerageEnabled: null,
+  brokerageEnabled: true,
   createTime: []
   createTime: []
 })
 })
 const queryFormRef = ref() // 搜索的表单
 const queryFormRef = ref() // 搜索的表单
@@ -281,7 +281,7 @@ const handleClearBindUser = async (row: BrokerageUserApi.BrokerageUserVO) => {
   } catch {}
   } catch {}
 }
 }
 
 
-/** 推广资格 开通/关闭 */
+/** 推广资格开通/关闭 */
 const handleBrokerageEnabledChange = async (row: BrokerageUserApi.BrokerageUserVO) => {
 const handleBrokerageEnabledChange = async (row: BrokerageUserApi.BrokerageUserVO) => {
   try {
   try {
     // 二次确认
     // 二次确认

+ 6 - 4
src/views/mall/trade/brokerage/withdraw/index.vue

@@ -104,8 +104,8 @@
         <template #default="scope">
         <template #default="scope">
           <div v-if="scope.row.type === BrokerageWithdrawTypeEnum.WALLET.type"> 余额 </div>
           <div v-if="scope.row.type === BrokerageWithdrawTypeEnum.WALLET.type"> 余额 </div>
           <div v-else>
           <div v-else>
-            {{ getDictLabel(DICT_TYPE.BROKERAGE_WITHDRAW_TYPE, scope.row.type) }}账号:
-            {{ scope.row.accountNo }}
+            {{ getDictLabel(DICT_TYPE.BROKERAGE_WITHDRAW_TYPE, scope.row.type) }}
+            <span v-if="scope.row.accountNo">账号:{{ scope.row.accountNo }}</span>
           </div>
           </div>
           <template v-if="scope.row.type === BrokerageWithdrawTypeEnum.BANK.type">
           <template v-if="scope.row.type === BrokerageWithdrawTypeEnum.BANK.type">
             <div>真实姓名:{{ scope.row.name }}</div>
             <div>真实姓名:{{ scope.row.name }}</div>
@@ -117,14 +117,16 @@
           </template>
           </template>
         </template>
         </template>
       </el-table-column>
       </el-table-column>
-      <el-table-column label="收款码" align="left" prop="accountQrCodeUrl" width="70px">
+      <el-table-column label="收款码" align="left" prop="accountQrCodeUrl" min-width="70px">
         <template #default="scope">
         <template #default="scope">
           <el-image
           <el-image
+            v-if="scope.row.accountQrCodeUrl"
             :src="scope.row.accountQrCodeUrl"
             :src="scope.row.accountQrCodeUrl"
             class="w-40px h-40px"
             class="w-40px h-40px"
             :preview-src-list="[scope.row.accountQrCodeUrl]"
             :preview-src-list="[scope.row.accountQrCodeUrl]"
             preview-teleported
             preview-teleported
           />
           />
+          <span v-else>无</span>
         </template>
         </template>
       </el-table-column>
       </el-table-column>
       <el-table-column
       <el-table-column
@@ -132,7 +134,7 @@
         align="left"
         align="left"
         prop="createTime"
         prop="createTime"
         :formatter="dateFormatter"
         :formatter="dateFormatter"
-        width="170px"
+        width="180px"
       />
       />
       <el-table-column label="备注" align="left" prop="remark" />
       <el-table-column label="备注" align="left" prop="remark" />
       <el-table-column label="状态" align="left" prop="status" min-width="120px">
       <el-table-column label="状态" align="left" prop="status" min-width="120px">

+ 84 - 21
src/views/mall/trade/config/index.vue

@@ -10,8 +10,43 @@
       <el-form-item label="hideId" v-show="false">
       <el-form-item label="hideId" v-show="false">
         <el-input v-model="formData.id" />
         <el-input v-model="formData.id" />
       </el-form-item>
       </el-form-item>
-
       <el-tabs>
       <el-tabs>
+        <!-- 售后 -->
+        <el-tab-pane label="售后">
+          <el-form-item label="退款理由" prop="afterSaleRefundReasons">
+            <el-select
+              v-model="formData.afterSaleRefundReasons"
+              allow-create
+              filterable
+              multiple
+              placeholder="请直接输入退款理由"
+            >
+              <el-option
+                v-for="reason in formData.afterSaleRefundReasons"
+                :key="reason"
+                :label="reason"
+                :value="reason"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="退货理由" prop="afterSaleReturnReasons">
+            <el-select
+              v-model="formData.afterSaleReturnReasons"
+              allow-create
+              filterable
+              multiple
+              placeholder="请直接输入退货理由"
+            >
+              <el-option
+                v-for="reason in formData.afterSaleReturnReasons"
+                :key="reason"
+                :label="reason"
+                :value="reason"
+              />
+            </el-select>
+          </el-form-item>
+        </el-tab-pane>
+        <!-- 配送 -->
         <el-tab-pane label="配送">
         <el-tab-pane label="配送">
           <el-form-item label="启用包邮" prop="deliveryExpressFreeEnabled">
           <el-form-item label="启用包邮" prop="deliveryExpressFreeEnabled">
             <el-switch v-model="formData.deliveryExpressFreeEnabled" style="user-select: none" />
             <el-switch v-model="formData.deliveryExpressFreeEnabled" style="user-select: none" />
@@ -22,10 +57,18 @@
               v-model="formData.deliveryExpressFreePrice"
               v-model="formData.deliveryExpressFreePrice"
               placeholder="请输入满额包邮"
               placeholder="请输入满额包邮"
               class="!w-xs"
               class="!w-xs"
+              :precision="2"
+              :min="0"
             />
             />
-            <el-text class="w-full" size="small" type="info"> 商城商品满多少金额即可包邮 </el-text>
+            <el-text class="w-full" size="small" type="info">
+              商城商品满多少金额即可包邮,单位:元
+            </el-text>
+          </el-form-item>
+          <el-form-item label="启用门店自提" prop="deliveryPickUpEnabled">
+            <el-switch v-model="formData.deliveryPickUpEnabled" style="user-select: none" />
           </el-form-item>
           </el-form-item>
         </el-tab-pane>
         </el-tab-pane>
+        <!-- 分销 -->
         <el-tab-pane label="分销">
         <el-tab-pane label="分销">
           <el-form-item label="分佣启用" prop="brokerageEnabled">
           <el-form-item label="分佣启用" prop="brokerageEnabled">
             <el-switch v-model="formData.brokerageEnabled" style="user-select: none" />
             <el-switch v-model="formData.brokerageEnabled" style="user-select: none" />
@@ -59,16 +102,16 @@
               </el-radio>
               </el-radio>
             </el-radio-group>
             </el-radio-group>
             <el-text class="w-full" size="small" type="info">
             <el-text class="w-full" size="small" type="info">
-              没有推广人:只要用户没有推广人,随时都可以绑定推广关系
+              首次绑定:只要用户没有推广人,随时都可以绑定推广关系
             </el-text>
             </el-text>
             <el-text class="w-full" size="small" type="info">
             <el-text class="w-full" size="small" type="info">
-              新用户:只有新用户注册时或首次进入系统时才可以绑定推广关系
+              注册绑定:只有新用户注册时或首次进入系统时才可以绑定推广关系
             </el-text>
             </el-text>
           </el-form-item>
           </el-form-item>
           <el-form-item label="分销海报图">
           <el-form-item label="分销海报图">
-            <UploadImgs v-model="formData.brokeragePostUrls" width="75px" height="125px" />
+            <UploadImgs v-model="formData.brokeragePosterUrls" width="75px" height="125px" />
             <el-text class="w-full" size="small" type="info">
             <el-text class="w-full" size="small" type="info">
-              个人中心分销海报图片,建议尺寸600x1000
+              个人中心分销海报图片,建议尺寸 600x1000
             </el-text>
             </el-text>
           </el-form-item>
           </el-form-item>
           <el-form-item label="一级返佣比例" prop="brokerageFirstPercent">
           <el-form-item label="一级返佣比例" prop="brokerageFirstPercent">
@@ -76,6 +119,8 @@
               v-model="formData.brokerageFirstPercent"
               v-model="formData.brokerageFirstPercent"
               placeholder="请输入一级返佣比例"
               placeholder="请输入一级返佣比例"
               class="!w-xs"
               class="!w-xs"
+              :min="0"
+              :max="100"
             />
             />
             <el-text class="w-full" size="small" type="info">
             <el-text class="w-full" size="small" type="info">
               订单交易成功后给推广人返佣的百分比
               订单交易成功后给推广人返佣的百分比
@@ -86,6 +131,8 @@
               v-model="formData.brokerageSecondPercent"
               v-model="formData.brokerageSecondPercent"
               placeholder="请输入二级返佣比例"
               placeholder="请输入二级返佣比例"
               class="!w-xs"
               class="!w-xs"
+              :min="0"
+              :max="100"
             />
             />
             <el-text class="w-full" size="small" type="info">
             <el-text class="w-full" size="small" type="info">
               订单交易成功后给推广人的推荐人返佣的百分比
               订单交易成功后给推广人的推荐人返佣的百分比
@@ -96,6 +143,7 @@
               v-model="formData.brokerageFrozenDays"
               v-model="formData.brokerageFrozenDays"
               placeholder="请输入佣金冻结天数"
               placeholder="请输入佣金冻结天数"
               class="!w-xs"
               class="!w-xs"
+              :min="0"
             />
             />
             <el-text class="w-full" size="small" type="info">
             <el-text class="w-full" size="small" type="info">
               防止用户退款,佣金被提现了,所以需要设置佣金冻结时间,单位:天
               防止用户退款,佣金被提现了,所以需要设置佣金冻结时间,单位:天
@@ -106,6 +154,8 @@
               v-model="formData.brokerageWithdrawMinPrice"
               v-model="formData.brokerageWithdrawMinPrice"
               placeholder="请输入提现最低金额"
               placeholder="请输入提现最低金额"
               class="!w-xs"
               class="!w-xs"
+              :precision="2"
+              :min="0"
             />
             />
             <el-text class="w-full" size="small" type="info">
             <el-text class="w-full" size="small" type="info">
               用户提现最低金额限制,单位:元
               用户提现最低金额限制,单位:元
@@ -116,13 +166,16 @@
               v-model="formData.brokerageWithdrawFeePercent"
               v-model="formData.brokerageWithdrawFeePercent"
               placeholder="请输入提现手续费"
               placeholder="请输入提现手续费"
               class="!w-xs"
               class="!w-xs"
+              :min="0"
+              :max="100"
             />
             />
             <el-text class="w-full" size="small" type="info">
             <el-text class="w-full" size="small" type="info">
-              提现手续费百分比,范围0-100,0为无提现手续费,例:设置10,即收取10%手续费,提现100元,到账90元,10元手续费
+              提现手续费百分比,范围 0-100,0 为无提现手续费。例:设置 10,即收取 10% 手续费,提现
+              10 元,到账 9 元,1 元手续费
             </el-text>
             </el-text>
           </el-form-item>
           </el-form-item>
-          <el-form-item label="提现方式" prop="brokerageWithdrawType">
-            <el-checkbox-group v-model="formData.brokerageWithdrawType">
+          <el-form-item label="提现方式" prop="brokerageWithdrawTypes">
+            <el-checkbox-group v-model="formData.brokerageWithdrawTypes">
               <el-checkbox
               <el-checkbox
                 v-for="dict in getIntDictOptions(DICT_TYPE.BROKERAGE_WITHDRAW_TYPE)"
                 v-for="dict in getIntDictOptions(DICT_TYPE.BROKERAGE_WITHDRAW_TYPE)"
                 :key="dict.value"
                 :key="dict.value"
@@ -146,7 +199,7 @@
           </el-form-item>
           </el-form-item>
         </el-tab-pane>
         </el-tab-pane>
       </el-tabs>
       </el-tabs>
-
+      <!-- 保存 -->
       <el-form-item>
       <el-form-item>
         <el-button type="primary" @click="submitForm" :loading="formLoading"> 保存 </el-button>
         <el-button type="primary" @click="submitForm" :loading="formLoading"> 保存 </el-button>
       </el-form-item>
       </el-form-item>
@@ -156,7 +209,6 @@
 
 
 <script setup lang="ts">
 <script setup lang="ts">
 import * as ConfigApi from '@/api/mall/trade/config'
 import * as ConfigApi from '@/api/mall/trade/config'
-import { BrokerageBindModeEnum, BrokerageEnabledConditionEnum } from '@/utils/constants'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 
 
 defineOptions({ name: 'TradeConfig' })
 defineOptions({ name: 'TradeConfig' })
@@ -167,19 +219,22 @@ const formLoading = ref(false) // 表单的加载中:1)修改时的数据加
 const formRef = ref()
 const formRef = ref()
 const formData = ref({
 const formData = ref({
   id: null,
   id: null,
-  deliveryExpressFreeEnabled: true,
+  afterSaleRefundReasons: [],
+  afterSaleReturnReasons: [],
+  deliveryExpressFreeEnabled: false,
   deliveryExpressFreePrice: 0,
   deliveryExpressFreePrice: 0,
-  brokerageEnabled: true,
-  brokerageEnabledCondition: BrokerageEnabledConditionEnum.ALL.condition,
-  brokerageBindMode: BrokerageBindModeEnum.ANYTIME.mode,
-  brokeragePostUrls: [],
+  deliveryPickUpEnabled: false,
+  brokerageEnabled: false,
+  brokerageEnabledCondition: undefined,
+  brokerageBindMode: undefined,
+  brokeragePosterUrls: [],
   brokerageFirstPercent: 0,
   brokerageFirstPercent: 0,
   brokerageSecondPercent: 0,
   brokerageSecondPercent: 0,
   brokerageWithdrawMinPrice: 0,
   brokerageWithdrawMinPrice: 0,
   brokerageWithdrawFeePercent: 0,
   brokerageWithdrawFeePercent: 0,
   brokerageBankNames: [],
   brokerageBankNames: [],
   brokerageFrozenDays: 0,
   brokerageFrozenDays: 0,
-  brokerageWithdrawType: []
+  brokerageWithdrawTypes: []
 })
 })
 const formRules = reactive({
 const formRules = reactive({
   deliveryExpressFreePrice: [{ required: true, message: '满额包邮不能为空', trigger: 'blur' }],
   deliveryExpressFreePrice: [{ required: true, message: '满额包邮不能为空', trigger: 'blur' }],
@@ -193,7 +248,7 @@ const formRules = reactive({
   brokerageWithdrawFeePercent: [{ required: true, message: '提现手续费不能为空', trigger: 'blur' }],
   brokerageWithdrawFeePercent: [{ required: true, message: '提现手续费不能为空', trigger: 'blur' }],
   brokerageBankNames: [{ required: true, message: '提现银行不能为空', trigger: 'blur' }],
   brokerageBankNames: [{ required: true, message: '提现银行不能为空', trigger: 'blur' }],
   brokerageFrozenDays: [{ required: true, message: '佣金冻结时间不能为空', trigger: 'blur' }],
   brokerageFrozenDays: [{ required: true, message: '佣金冻结时间不能为空', trigger: 'blur' }],
-  brokerageWithdrawType: [
+  brokerageWithdrawTypes: [
     {
     {
       required: true,
       required: true,
       message: '提现方式不能为空',
       message: '提现方式不能为空',
@@ -211,10 +266,15 @@ const submitForm = async () => {
   // 提交请求
   // 提交请求
   formLoading.value = true
   formLoading.value = true
   try {
   try {
-    const data = formData.value as unknown as ConfigApi.ConfigVO
-    data.brokeragePostUrls = formData.value.brokeragePostUrls.map((item: any) => {
+    const data = {
+      ...formData.value
+    } as unknown as ConfigApi.ConfigVO
+    data.brokeragePosterUrls = formData.value.brokeragePosterUrls.map((item: any) => {
       return item?.url ? item.url : item
       return item?.url ? item.url : item
     })
     })
+    // 金额放大
+    data.deliveryExpressFreePrice = data.deliveryExpressFreePrice * 100
+    data.brokerageWithdrawMinPrice = data.brokerageWithdrawMinPrice * 100
     await ConfigApi.saveTradeConfig(data)
     await ConfigApi.saveTradeConfig(data)
     message.success('保存成功')
     message.success('保存成功')
   } finally {
   } finally {
@@ -228,8 +288,11 @@ const getConfig = async () => {
   try {
   try {
     const data = await ConfigApi.getTradeConfig()
     const data = await ConfigApi.getTradeConfig()
     if (data != null) {
     if (data != null) {
-      data.brokeragePostUrls = data.brokeragePostUrls.map((url) => ({ url }))
+      data.brokeragePosterUrls = data.brokeragePosterUrls.map((url) => ({ url }))
       formData.value = data
       formData.value = data
+      // 金额缩小
+      formData.value.deliveryExpressFreePrice = data.deliveryExpressFreePrice / 100
+      formData.value.brokerageWithdrawMinPrice = data.brokerageWithdrawMinPrice / 100
     }
     }
   } finally {
   } finally {
     formLoading.value = false
     formLoading.value = false

+ 4 - 4
src/views/mall/trade/delivery/express/ExpressForm.vue

@@ -7,17 +7,17 @@
       label-width="120px"
       label-width="120px"
       v-loading="formLoading"
       v-loading="formLoading"
     >
     >
-      <el-form-item label="快递公司编码" prop="code">
+      <el-form-item label="公司编码" prop="code">
         <el-input v-model="formData.code" placeholder="请输入快递编码" />
         <el-input v-model="formData.code" placeholder="请输入快递编码" />
       </el-form-item>
       </el-form-item>
-      <el-form-item label="快递公司名称" prop="name">
+      <el-form-item label="公司名称" prop="name">
         <el-input v-model="formData.name" placeholder="请输入快递名称" />
         <el-input v-model="formData.name" placeholder="请输入快递名称" />
       </el-form-item>
       </el-form-item>
-      <el-form-item label="快递公司 logo" prop="logo">
+      <el-form-item label="公司 logo" prop="logo">
         <UploadImg v-model="formData.logo" :limit="1" :is-show-tip="false" />
         <UploadImg v-model="formData.logo" :limit="1" :is-show-tip="false" />
         <div style="font-size: 10px" class="pl-10px">推荐 180x180 图片分辨率</div>
         <div style="font-size: 10px" class="pl-10px">推荐 180x180 图片分辨率</div>
       </el-form-item>
       </el-form-item>
-      <el-form-item label="分类排序" prop="sort">
+      <el-form-item label="排序" prop="sort">
         <el-input-number v-model="formData.sort" controls-position="right" :min="0" />
         <el-input-number v-model="formData.sort" controls-position="right" :min="0" />
       </el-form-item>
       </el-form-item>
       <el-form-item label="开启状态" prop="status">
       <el-form-item label="开启状态" prop="status">

+ 4 - 4
src/views/mall/trade/delivery/express/index.vue

@@ -53,11 +53,11 @@
   <!-- 列表 -->
   <!-- 列表 -->
   <ContentWrap>
   <ContentWrap>
     <el-table v-loading="loading" :data="list">
     <el-table v-loading="loading" :data="list">
-      <el-table-column label="快递公司编号" prop="code" />
-      <el-table-column label="快递公司名称" prop="name" />
-      <el-table-column label="快递公司 logo " prop="logo">
+      <el-table-column label="公司编码" prop="code" />
+      <el-table-column label="公司名称" prop="name" />
+      <el-table-column label="公司 logo " prop="logo">
         <template #default="scope">
         <template #default="scope">
-          <img v-if="scope.row.logo" :src="scope.row.logo" alt="快递公司logo" class="h-100px" />
+          <img v-if="scope.row.logo" :src="scope.row.logo" alt="公司logo" class="h-40px" />
         </template>
         </template>
       </el-table-column>
       </el-table-column>
       <el-table-column label="排序" align="center" prop="sort" />
       <el-table-column label="排序" align="center" prop="sort" />

+ 46 - 159
src/views/mall/trade/delivery/expressTemplate/ExpressTemplateForm.vue

@@ -1,5 +1,5 @@
 <template>
 <template>
-  <Dialog :title="dialogTitle" v-model="dialogVisible" width="80%">
+  <Dialog :title="dialogTitle" v-model="dialogVisible" width="1300px">
     <el-form
     <el-form
       ref="formRef"
       ref="formRef"
       :model="formData"
       :model="formData"
@@ -21,23 +21,19 @@
           </el-radio>
           </el-radio>
         </el-radio-group>
         </el-radio-group>
       </el-form-item>
       </el-form-item>
-      <el-form-item label="运费" prop="templateCharge">
-        <el-table border style="width: 100%" :data="formData.templateCharge">
-          <el-table-column align="center" label="区域" width="180">
+      <el-form-item label="运费" prop="charges">
+        <el-table border style="width: 100%" :data="formData.charges">
+          <el-table-column align="center" label="区域" width="360">
             <template #default="{ row }">
             <template #default="{ row }">
-              <!--   区域数据太多,用赖加载方式,要不然性能有问题 -->
-              <el-tree-select
+              <el-cascader
                 v-model="row.areaIds"
                 v-model="row.areaIds"
-                :load="loadChargeArea"
-                :props="defaultProps"
-                node-key="id"
-                multiple
-                check-strictly
-                show-checkbox
-                lazy
-                check-on-click-node
-                :render-after-expand="false"
-                :cache-data="areaCache"
+                :options="areaTree"
+                :props="defaultProps2"
+                class="w-1/1"
+                clearable
+                placeholder="请选择商品分类"
+                filterable
+                collapse-tags
               />
               />
             </template>
             </template>
           </el-table-column>
           </el-table-column>
@@ -85,23 +81,19 @@
           <Icon icon="ep:plus" class="mr-5px" /> 添加区域
           <Icon icon="ep:plus" class="mr-5px" /> 添加区域
         </el-button>
         </el-button>
       </el-form-item>
       </el-form-item>
-      <el-form-item label="包邮区域" prop="templateFree">
-        <el-table border style="width: 100%" :data="formData.templateFree">
-          <el-table-column align="center" label="区域">
+      <el-form-item label="包邮区域" prop="frees">
+        <el-table border style="width: 100%" :data="formData.frees">
+          <el-table-column align="center" label="区域" width="360">
             <template #default="{ row }">
             <template #default="{ row }">
-              <!-- 区域数据太多,用赖加载方式,要不然性能有问题 -->
-              <el-tree-select
+              <el-cascader
                 v-model="row.areaIds"
                 v-model="row.areaIds"
-                multiple
-                lazy
-                :load="loadFreeArea"
-                :props="defaultProps"
-                node-key="id"
-                check-strictly
-                show-checkbox
-                check-on-click-node
-                :render-after-expand="true"
-                :cache-data="areaCache"
+                :options="areaTree"
+                :props="defaultProps2"
+                class="w-1/1"
+                clearable
+                placeholder="请选择商品分类"
+                filterable
+                collapse-tags
               />
               />
             </template>
             </template>
           </el-table-column>
           </el-table-column>
@@ -140,13 +132,18 @@
 <script lang="ts" setup>
 <script lang="ts" setup>
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import * as DeliveryExpressTemplateApi from '@/api/mall/trade/delivery/expressTemplate'
 import * as DeliveryExpressTemplateApi from '@/api/mall/trade/delivery/expressTemplate'
+import * as AreaApi from '@/api/system/area'
 import { defaultProps } from '@/utils/tree'
 import { defaultProps } from '@/utils/tree'
 import { yuanToFen, fenToYuan } from '@/utils'
 import { yuanToFen, fenToYuan } from '@/utils'
-import { getChildrenArea, getAreaListByIds } from '@/api/system/area'
 import { cloneDeep } from 'lodash-es'
 import { cloneDeep } from 'lodash-es'
 const { t } = useI18n() // 国际化
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 const message = useMessage() // 消息弹窗
 
 
+const defaultProps2 = {
+  ...defaultProps,
+  multiple: true
+}
+
 const dialogVisible = ref(false) // 弹窗的是否展示
 const dialogVisible = ref(false) // 弹窗的是否展示
 const dialogTitle = ref('') // 弹窗的标题
 const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
@@ -156,8 +153,8 @@ const formData = ref({
   name: '',
   name: '',
   chargeMode: 1,
   chargeMode: 1,
   sort: 0,
   sort: 0,
-  templateCharge: [],
-  templateFree: []
+  charges: [],
+  frees: []
 })
 })
 const columnTitleMap = new Map()
 const columnTitleMap = new Map()
 const columnTitle = ref({
 const columnTitle = ref({
@@ -171,9 +168,6 @@ const formRules = reactive({
   sort: [{ required: true, message: '分类排序不能为空', trigger: 'blur' }]
   sort: [{ required: true, message: '分类排序不能为空', trigger: 'blur' }]
 })
 })
 const formRef = ref() // 表单 Ref
 const formRef = ref() // 表单 Ref
-const areaCache = ref([]) // 由于区域节点懒加载,已选区域节点需要缓存展示
-// TODO @jason:配送的时候,只允许选择省市级别,不允许选择区;如果这样的话,是不是打开弹窗,直接把城市都请求过来;
-// TODO @jaosn:因为只有省市两级,感觉就不用特殊做全国逻辑;选择全国,就默认把子节点都选择上;另外,选择父节点,要把子节点选中哈;
 
 
 /** 打开弹窗 */
 /** 打开弹窗 */
 const open = async (type: string, id?: number) => {
 const open = async (type: string, id?: number) => {
@@ -187,30 +181,14 @@ const open = async (type: string, id?: number) => {
       formLoading.value = true
       formLoading.value = true
       formData.value = await DeliveryExpressTemplateApi.getDeliveryExpressTemplate(id)
       formData.value = await DeliveryExpressTemplateApi.getDeliveryExpressTemplate(id)
       columnTitle.value = columnTitleMap.get(formData.value.chargeMode)
       columnTitle.value = columnTitleMap.get(formData.value.chargeMode)
-      const chargeAreaIds = []
-      const freeAreaIds = []
-      formData.value.templateCharge.forEach((item) => {
-        for (let i = 0; i < item.areaIds.length; i++) {
-          if (!chargeAreaIds.includes(item.areaIds[i])) {
-            chargeAreaIds.push(item.areaIds[i])
-          }
-        }
-        //前端价格以元展示
+      formData.value.charges.forEach((item) => {
+        // 前端价格以元展示
         item.startPrice = fenToYuan(item.startPrice)
         item.startPrice = fenToYuan(item.startPrice)
         item.extraPrice = fenToYuan(item.extraPrice)
         item.extraPrice = fenToYuan(item.extraPrice)
       })
       })
-      formData.value.templateFree.forEach((item) => {
-        for (let i = 0; i < item.areaIds.length; i++) {
-          if (!chargeAreaIds.includes(item.areaIds[i]) && !freeAreaIds.includes(item.areaIds[i])) {
-            freeAreaIds.push(item.areaIds[i])
-          }
-        }
+      formData.value.frees.forEach((item) => {
         item.freePrice = fenToYuan(item.freePrice)
         item.freePrice = fenToYuan(item.freePrice)
       })
       })
-      // 已选的区域节点
-      const areaIds = chargeAreaIds.concat(freeAreaIds)
-      // 区域节点,懒加载方式。已选节点需要缓存展示
-      areaCache.value = await getAreaListByIds(areaIds.join(','))
     }
     }
   } finally {
   } finally {
     formLoading.value = false
     formLoading.value = false
@@ -228,14 +206,13 @@ const submitForm = async () => {
   // 提交请求
   // 提交请求
   formLoading.value = true
   formLoading.value = true
   try {
   try {
-    const data = formData.value as DeliveryExpressTemplateApi.DeliveryExpressTemplateVO
+    const data = cloneDeep(formData.value) as DeliveryExpressTemplateApi.DeliveryExpressTemplateVO
     // 前端价格以元展示,提交到后端。用分计算
     // 前端价格以元展示,提交到后端。用分计算
-    // TODO @jason:不能直接这样改,要复制出来改。不然后端操作失败,数据已经被改了
-    data.templateCharge.forEach((item) => {
+    data.charges.forEach((item) => {
       item.startPrice = yuanToFen(item.startPrice)
       item.startPrice = yuanToFen(item.startPrice)
       item.extraPrice = yuanToFen(item.extraPrice)
       item.extraPrice = yuanToFen(item.extraPrice)
     })
     })
-    data.templateFree.forEach((item) => {
+    data.frees.forEach((item) => {
       item.freePrice = yuanToFen(item.freePrice)
       item.freePrice = yuanToFen(item.freePrice)
     })
     })
     if (formType.value === 'create') {
     if (formType.value === 'create') {
@@ -259,7 +236,7 @@ const resetForm = () => {
     id: undefined,
     id: undefined,
     name: '',
     name: '',
     chargeMode: 1,
     chargeMode: 1,
-    templateCharge: [
+    charges: [
       {
       {
         areaIds: [1],
         areaIds: [1],
         startCount: 2,
         startCount: 2,
@@ -268,7 +245,7 @@ const resetForm = () => {
         extraPrice: 10
         extraPrice: 10
       }
       }
     ],
     ],
-    templateFree: [],
+    frees: [],
     sort: 0
     sort: 0
   }
   }
   columnTitle.value = columnTitleMap.get(1)
   columnTitle.value = columnTitleMap.get(1)
@@ -279,37 +256,10 @@ const resetForm = () => {
 const changeChargeMode = (chargeMode: number) => {
 const changeChargeMode = (chargeMode: number) => {
   columnTitle.value = columnTitleMap.get(chargeMode)
   columnTitle.value = columnTitleMap.get(chargeMode)
 }
 }
-const defaultArea = [{ id: 1, name: '全国', disabled: false }]
 
 
 /** 初始化数据 */
 /** 初始化数据 */
-// TODO @jason:是不是不用写这样一个初始化方法,columnTitleMap 直接就可以了呀
-// const columnTitleMap = {
-//   '1': {
-//     startCountTitle: '首件',
-//     extraCountTitle: '续件',
-//     freeCountTitle: '包邮件数'
-//   },
-//   '2': {
-//     startCountTitle: '首件重量(kg)',
-//     extraCountTitle: '续件重量(kg)',
-//     freeCountTitle: '包邮重量(kg)'
-//   },
-//   '3': {
-//     startCountTitle: '首件体积(m³)',
-//     extraCountTitle: '续件体积(m³)',
-//     freeCountTitle: '包邮体积(m³)'
-//   }
-// }
+const areaTree = ref([])
 const initData = async () => {
 const initData = async () => {
-  // TODO 从服务端全量加载数据, 后面看懒加载是不是可以从前端获取数据。 目前从后端获取数据
-  // formLoading.value = true
-  // try {
-  //   const data = await getAreaTree()
-  //   areaTree = data
-  //   console.log('areaTree', areaTree)
-  // } finally {
-  //   formLoading.value = false
-  // }
   // 表头标题和计费方式的映射
   // 表头标题和计费方式的映射
   columnTitleMap.set(1, {
   columnTitleMap.set(1, {
     startCountTitle: '首件',
     startCountTitle: '首件',
@@ -326,77 +276,14 @@ const initData = async () => {
     extraCountTitle: '续件体积(m³)',
     extraCountTitle: '续件体积(m³)',
     freeCountTitle: '包邮体积(m³)'
     freeCountTitle: '包邮体积(m³)'
   })
   })
+  // 加载区域数据
+  areaTree.value = await AreaApi.getAreaTree()
 }
 }
 
 
-/** 懒加载运费区域树 */
-const loadChargeArea = async (node, resolve) => {
-  //已选区域需要禁止再次选择
-  const areaIds = []
-  formData.value.templateCharge.forEach((item) => {
-    if (item.areaIds.length > 0) {
-      item.areaIds.forEach((areaId) => areaIds.push(areaId))
-    }
-  })
-  if (node.isLeaf) return resolve([])
-  const length = node.data.length
-  if (length === 0) {
-    const data = cloneDeep(defaultArea)
-    const item = data[0]
-    if (areaIds.includes(item.id)) {
-      // TODO 禁止选中的区域有些问题, 导致修改时候不能重新选择 不知道如何处理。 暂时注释掉 @芋艿 有空瞅瞅
-      // TODO @jason:先不做这个功能哈。
-      //item.disabled = true
-    }
-    resolve(data)
-  } else {
-    const id = node.data.id
-    const data = await getChildrenArea(id)
-    data.forEach((item) => {
-      if (areaIds.includes(item.id)) {
-        //item.disabled = true
-      }
-    })
-    resolve(data)
-  }
-}
-
-/** 懒加载包邮区域树 */
-const loadFreeArea = async (node, resolve) => {
-  if (node.isLeaf) return resolve([])
-  //已选区域需要禁止再次选择
-  const areaIds = []
-  formData.value.templateFree.forEach((item) => {
-    if (item.areaIds.length > 0) {
-      item.areaIds.forEach((areaId) => areaIds.push(areaId))
-    }
-  })
-  const length = node.data.length
-  if (length === 0) {
-    // 为空,从全国开始选择。全国 id == 1
-    const data = cloneDeep(defaultArea)
-    const item = data[0]
-    if (areaIds.includes(item.id)) {
-      //item.disabled = true
-    }
-    resolve(data)
-  } else {
-    const id = node.data.id
-    const data = await getChildrenArea(id)
-    // 已选区域需要禁止再次选择
-    data.forEach((item) => {
-      if (areaIds.includes(item.id)) {
-        // TODO 禁止选中的区域有些问题, 导致修改时候不能重新选择 不知道如何处理。 暂时注释掉 @芋艿 有空瞅瞅
-        // TODO @jason:先不做这个功能哈。
-        //item.disabled = true
-      }
-    })
-    resolve(data)
-  }
-}
 /** 添加计费区域 */
 /** 添加计费区域 */
 const addChargeArea = () => {
 const addChargeArea = () => {
   const data = formData.value
   const data = formData.value
-  data.templateCharge.push({
+  data.charges.push({
     areaIds: [],
     areaIds: [],
     startCount: 1,
     startCount: 1,
     startPrice: 1,
     startPrice: 1,
@@ -408,13 +295,13 @@ const addChargeArea = () => {
 /** 删除计费区域 */
 /** 删除计费区域 */
 const deleteChargeArea = (index) => {
 const deleteChargeArea = (index) => {
   const data = formData.value
   const data = formData.value
-  data.templateCharge.splice(index, 1)
+  data.charges.splice(index, 1)
 }
 }
 
 
 /** 添加包邮区域 */
 /** 添加包邮区域 */
 const addFreeArea = () => {
 const addFreeArea = () => {
   const data = formData.value
   const data = formData.value
-  data.templateFree.push({
+  data.frees.push({
     areaIds: [],
     areaIds: [],
     freeCount: 1,
     freeCount: 1,
     freePrice: 1
     freePrice: 1
@@ -424,7 +311,7 @@ const addFreeArea = () => {
 /** 删除包邮区域 */
 /** 删除包邮区域 */
 const deleteFreeArea = (index) => {
 const deleteFreeArea = (index) => {
   const data = formData.value
   const data = formData.value
-  data.templateFree.splice(index, 1)
+  data.frees.splice(index, 1)
 }
 }
 
 
 /** 初始化 **/
 /** 初始化 **/

+ 4 - 4
src/views/mall/trade/delivery/expressTemplate/index.vue

@@ -51,14 +51,14 @@
   <!-- 列表 -->
   <!-- 列表 -->
   <ContentWrap>
   <ContentWrap>
     <el-table v-loading="loading" :data="list">
     <el-table v-loading="loading" :data="list">
-      <el-table-column label="编号" prop="id" />
-      <el-table-column label="模板名称" prop="name" />
-      <el-table-column label="计费方式" prop="chargeMode" align="center">
+      <el-table-column label="编号" min-width="60" prop="id" />
+      <el-table-column label="模板名称" min-width="100" prop="name" />
+      <el-table-column label="计费方式" prop="chargeMode" min-width="100" align="center">
         <template #default="scope">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.EXPRESS_CHARGE_MODE" :value="scope.row.chargeMode" />
           <dict-tag :type="DICT_TYPE.EXPRESS_CHARGE_MODE" :value="scope.row.chargeMode" />
         </template>
         </template>
       </el-table-column>
       </el-table-column>
-      <el-table-column label="排序" prop="sort" />
+      <el-table-column label="排序" min-width="100" prop="sort" />
       <el-table-column
       <el-table-column
         label="创建时间"
         label="创建时间"
         align="center"
         align="center"

+ 12 - 26
src/views/mall/trade/delivery/pickUpStore/PickUpStoreForm.vue

@@ -51,7 +51,7 @@
       <el-row>
       <el-row>
         <el-col :span="12">
         <el-col :span="12">
           <el-form-item label="门店所在地区" prop="areaId">
           <el-form-item label="门店所在地区" prop="areaId">
-            <el-cascader v-model="formData.areaId" :options="areaList" :props="areaTreeProps" />
+            <el-cascader v-model="formData.areaId" :options="areaList" :props="defaultProps" />
           </el-form-item>
           </el-form-item>
         </el-col>
         </el-col>
         <el-col :span="12">
         <el-col :span="12">
@@ -99,7 +99,7 @@
         </el-col>
         </el-col>
       </el-row>
       </el-row>
       <el-form-item label="获取经纬度">
       <el-form-item label="获取经纬度">
-        <el-button type="primary" @click="mapDialogVisible.value = true">获取</el-button>
+        <el-button type="primary" @click="mapDialogVisible = true">获取</el-button>
       </el-form-item>
       </el-form-item>
     </el-form>
     </el-form>
     <template #footer>
     <template #footer>
@@ -121,8 +121,9 @@
 import * as DeliveryPickUpStoreApi from '@/api/mall/trade/delivery/pickUpStore'
 import * as DeliveryPickUpStoreApi from '@/api/mall/trade/delivery/pickUpStore'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { CommonStatusEnum } from '@/utils/constants'
 import { CommonStatusEnum } from '@/utils/constants'
+import { defaultProps } from '@/utils/tree'
 import { getAreaTree } from '@/api/system/area'
 import { getAreaTree } from '@/api/system/area'
-import * as ConfigApi from '@/api/infra/config'
+import * as ConfigApi from '@/api/mall/trade/config'
 const { t } = useI18n() // 国际化
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 const message = useMessage() // 消息弹窗
 
 
@@ -161,12 +162,6 @@ const formRules = reactive({
   status: [{ required: true, message: '开启状态不能为空', trigger: 'blur' }]
   status: [{ required: true, message: '开启状态不能为空', trigger: 'blur' }]
 })
 })
 const formRef = ref() // 表单 Ref
 const formRef = ref() // 表单 Ref
-const areaTreeProps = {
-  children: 'children',
-  label: 'name',
-  value: 'id',
-  emitPath: false
-}
 const areaList = ref() // 区域树
 const areaList = ref() // 区域树
 const tencentLbsUrl = ref('') // 腾讯位置服务 url
 const tencentLbsUrl = ref('') // 腾讯位置服务 url
 
 
@@ -244,16 +239,8 @@ const selectAddress = function (loc: any): void {
   mapDialogVisible.value = false
   mapDialogVisible.value = false
 }
 }
 
 
-/** 初始化数据 */
-const initData = async () => {
-  formLoading.value = true
-  try {
-    const data = await getAreaTree()
-    areaList.value = data
-  } finally {
-    formLoading.value = false
-  }
-  // TODO @jason:要不创建一个 initTencentLbsMap
+/** 初始化腾讯地图 */
+const initTencentLbsMap = async () => {
   window.selectAddress = selectAddress
   window.selectAddress = selectAddress
   window.addEventListener(
   window.addEventListener(
     'message',
     'message',
@@ -267,17 +254,16 @@ const initData = async () => {
     },
     },
     false
     false
   )
   )
-  const data = await ConfigApi.getConfigKey('tencent.lbs.key')
-  let key = ''
-  if (data && data.length > 0) {
-    key = data
-  }
+  const data = await ConfigApi.getTradeConfig()
+  const key = data.tencentLbsKey
   tencentLbsUrl.value = `https://apis.map.qq.com/tools/locpicker?type=1&key=${key}&referer=myapp`
   tencentLbsUrl.value = `https://apis.map.qq.com/tools/locpicker?type=1&key=${key}&referer=myapp`
 }
 }
 
 
 /** 初始化 **/
 /** 初始化 **/
-onMounted(() => {
-  initData()
+onMounted(async () => {
+  areaList.value = await getAreaTree()
+  // 加载地图
+  await initTencentLbsMap()
 })
 })
 </script>
 </script>
 <style lang="scss">
 <style lang="scss">

+ 12 - 7
src/views/mall/trade/delivery/pickUpStore/index.vue

@@ -65,16 +65,21 @@
   <!-- 列表 -->
   <!-- 列表 -->
   <ContentWrap>
   <ContentWrap>
     <el-table v-loading="loading" :data="list">
     <el-table v-loading="loading" :data="list">
-      <el-table-column label="编号" prop="id" />
-      <el-table-column label="门店 logo" prop="logo">
+      <el-table-column label="编号" min-width="80" prop="id" />
+      <el-table-column label="门店 logo" min-width="100" prop="logo">
         <template #default="scope">
         <template #default="scope">
-          <img v-if="scope.row.logo" :src="scope.row.logo" alt="门店 logo" class="h-100px" />
+          <img v-if="scope.row.logo" :src="scope.row.logo" alt="门店 logo" class="h-50px" />
         </template>
         </template>
       </el-table-column>
       </el-table-column>
-      <el-table-column label="门店名称" prop="name" />
-      <el-table-column label="门店手机" prop="phone" />
-      <el-table-column align="center" label="门店详细地址" prop="detailAddress" />
-      <el-table-column align="center" label="开启状态" prop="status">
+      <el-table-column label="门店名称" min-width="150" prop="name" />
+      <el-table-column label="门店手机" min-width="100" prop="phone" />
+      <el-table-column label="地址" min-width="100" prop="detailAddress" />
+      <el-table-column label="营业时间" min-width="180">
+        <template #default="scope">
+          {{ scope.row.openingTime }} ~ {{ scope.row.closingTime }}
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="开启状态" min-width="100" prop="status">
         <template #default="scope">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
           <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
         </template>
         </template>

+ 134 - 72
src/views/mall/trade/order/detail/index.vue

@@ -3,35 +3,21 @@
     <!-- 订单信息 -->
     <!-- 订单信息 -->
     <el-descriptions title="订单信息">
     <el-descriptions title="订单信息">
       <el-descriptions-item label="订单号: ">{{ formData.no }}</el-descriptions-item>
       <el-descriptions-item label="订单号: ">{{ formData.no }}</el-descriptions-item>
-      <el-descriptions-item label="配送方式: ">
-        <dict-tag :type="DICT_TYPE.TRADE_DELIVERY_TYPE" :value="formData.deliveryType!" />
-      </el-descriptions-item>
-      <!-- TODO 营销活动待实现     -->
-      <el-descriptions-item label="营销活动: ">秒杀活动</el-descriptions-item>
+      <el-descriptions-item label="买家: ">{{ formData?.user?.nickname }}</el-descriptions-item>
       <el-descriptions-item label="订单类型: ">
       <el-descriptions-item label="订单类型: ">
         <dict-tag :type="DICT_TYPE.TRADE_ORDER_TYPE" :value="formData.type!" />
         <dict-tag :type="DICT_TYPE.TRADE_ORDER_TYPE" :value="formData.type!" />
       </el-descriptions-item>
       </el-descriptions-item>
-      <el-descriptions-item label="收货人: ">{{ formData.receiverName }}</el-descriptions-item>
-      <el-descriptions-item label="买家留言: ">{{ formData.userRemark }}</el-descriptions-item>
       <el-descriptions-item label="订单来源: ">
       <el-descriptions-item label="订单来源: ">
         <dict-tag :type="DICT_TYPE.TERMINAL" :value="formData.terminal!" />
         <dict-tag :type="DICT_TYPE.TERMINAL" :value="formData.terminal!" />
       </el-descriptions-item>
       </el-descriptions-item>
-      <el-descriptions-item label="联系电话: ">{{ formData.receiverMobile }}</el-descriptions-item>
+      <el-descriptions-item label="买家留言: ">{{ formData.userRemark }}</el-descriptions-item>
       <el-descriptions-item label="商家备注: ">{{ formData.remark }}</el-descriptions-item>
       <el-descriptions-item label="商家备注: ">{{ formData.remark }}</el-descriptions-item>
       <el-descriptions-item label="支付单号: ">{{ formData.payOrderId }}</el-descriptions-item>
       <el-descriptions-item label="支付单号: ">{{ formData.payOrderId }}</el-descriptions-item>
       <el-descriptions-item label="付款方式: ">
       <el-descriptions-item label="付款方式: ">
         <dict-tag :type="DICT_TYPE.PAY_CHANNEL_CODE" :value="formData.payChannelCode!" />
         <dict-tag :type="DICT_TYPE.PAY_CHANNEL_CODE" :value="formData.payChannelCode!" />
       </el-descriptions-item>
       </el-descriptions-item>
-      <!-- <el-descriptions-item label="买家: ">{{ formData.user.nickname }}</el-descriptions-item> -->
-      <!-- TODO @puhui999:待实现:跳转会员 -->
-      <el-descriptions-item label="收货地址: ">
-        {{ formData.receiverAreaName }} {{ formData.receiverDetailAddress }}
-        <el-link
-          v-clipboard:copy="formData.receiverAreaName + ' ' + formData.receiverDetailAddress"
-          v-clipboard:success="clipboardSuccess"
-          icon="ep:document-copy"
-          type="primary"
-        />
+      <el-descriptions-item label="推广用户: " v-if="formData.brokerageUser">
+        {{ formData.brokerageUser?.nickname }}
       </el-descriptions-item>
       </el-descriptions-item>
     </el-descriptions>
     </el-descriptions>
 
 
@@ -41,16 +27,40 @@
         <dict-tag :type="DICT_TYPE.TRADE_ORDER_STATUS" :value="formData.status!" />
         <dict-tag :type="DICT_TYPE.TRADE_ORDER_STATUS" :value="formData.status!" />
       </el-descriptions-item>
       </el-descriptions-item>
       <el-descriptions-item label-class-name="no-colon">
       <el-descriptions-item label-class-name="no-colon">
-        <el-button v-if="formData.status! === 0" type="primary" @click="updatePrice">
+        <el-button
+          v-if="formData.status! === TradeOrderStatusEnum.UNPAID.status"
+          type="primary"
+          @click="updatePrice"
+        >
           调整价格
           调整价格
         </el-button>
         </el-button>
         <el-button type="primary" @click="remark">备注</el-button>
         <el-button type="primary" @click="remark">备注</el-button>
-        <el-button v-if="formData.status! === 10" type="primary" @click="delivery">
-          发货
-        </el-button>
-        <el-button v-if="formData.status! === 10" type="primary" @click="updateAddress">
-          修改地址
-        </el-button>
+        <!-- 待发货 -->
+        <template v-if="formData.status! === TradeOrderStatusEnum.UNDELIVERED.status">
+          <!-- 快递发货 -->
+          <el-button
+            v-if="formData.deliveryType === DeliveryTypeEnum.EXPRESS.type"
+            type="primary"
+            @click="delivery"
+          >
+            发货
+          </el-button>
+          <el-button
+            v-if="formData.deliveryType === DeliveryTypeEnum.EXPRESS.type"
+            type="primary"
+            @click="updateAddress"
+          >
+            修改地址
+          </el-button>
+          <!-- 到店自提 -->
+          <el-button
+            v-if="formData.deliveryType === DeliveryTypeEnum.PICK_UP.type"
+            type="primary"
+            @click="handlePickUp"
+          >
+            核销
+          </el-button>
+        </template>
       </el-descriptions-item>
       </el-descriptions-item>
       <el-descriptions-item>
       <el-descriptions-item>
         <template #label><span style="color: red">提醒: </span></template>
         <template #label><span style="color: red">提醒: </span></template>
@@ -75,11 +85,11 @@
                 </template>
                 </template>
               </el-table-column>
               </el-table-column>
               <el-table-column label="商品原价" prop="price" width="150">
               <el-table-column label="商品原价" prop="price" width="150">
-                <template #default="{ row }">{{ floatToFixed2(row.price) }}元</template>
+                <template #default="{ row }">{{ fenToYuan(row.price) }}元</template>
               </el-table-column>
               </el-table-column>
               <el-table-column label="数量" prop="count" width="100" />
               <el-table-column label="数量" prop="count" width="100" />
               <el-table-column label="合计" prop="payPrice" width="150">
               <el-table-column label="合计" prop="payPrice" width="150">
-                <template #default="{ row }">{{ floatToFixed2(row.payPrice) }}元</template>
+                <template #default="{ row }">{{ fenToYuan(row.payPrice) }}元</template>
               </el-table-column>
               </el-table-column>
               <el-table-column label="售后状态" prop="afterSaleStatus" width="120">
               <el-table-column label="售后状态" prop="afterSaleStatus" width="120">
                 <template #default="{ row }">
                 <template #default="{ row }">
@@ -95,64 +105,91 @@
         </el-row>
         </el-row>
       </el-descriptions-item>
       </el-descriptions-item>
     </el-descriptions>
     </el-descriptions>
-    <el-descriptions :column="6">
+    <el-descriptions :column="4">
+      <!-- 第一层 -->
       <el-descriptions-item label="商品总额: ">
       <el-descriptions-item label="商品总额: ">
-        {{ floatToFixed2(formData.totalPrice!) }}
+        {{ fenToYuan(formData.totalPrice!) }} 
       </el-descriptions-item>
       </el-descriptions-item>
       <el-descriptions-item label="运费金额: ">
       <el-descriptions-item label="运费金额: ">
-        {{ floatToFixed2(formData.deliveryPrice!) }}
+        {{ fenToYuan(formData.deliveryPrice!) }} 
       </el-descriptions-item>
       </el-descriptions-item>
       <el-descriptions-item label="订单调价: ">
       <el-descriptions-item label="订单调价: ">
-        {{ floatToFixed2(formData.adjustPrice!) }}元
+        {{ fenToYuan(formData.adjustPrice!) }} 元
+      </el-descriptions-item>
+      <el-descriptions-item v-for="item in 1" :key="item" label-class-name="no-colon" />
+      <!-- 第二层 -->
+      <el-descriptions-item>
+        <template #label><span style="color: red">优惠劵优惠: </span></template>
+        {{ fenToYuan(formData.couponPrice!) }} 元
       </el-descriptions-item>
       </el-descriptions-item>
-
       <el-descriptions-item>
       <el-descriptions-item>
-        <template #label><span style="color: red">商品优惠: </span></template>
-        {{ floatToFixed2(formData.couponPrice!) }}元
+        <template #label><span style="color: red">VIP 优惠: </span></template>
+        {{ fenToYuan(formData.vipPrice!) }} 
       </el-descriptions-item>
       </el-descriptions-item>
       <el-descriptions-item>
       <el-descriptions-item>
-        <template #label><span style="color: red">订单优惠: </span></template>
-        {{ floatToFixed2(formData.discountPrice!) }}
+        <template #label><span style="color: red">活动优惠: </span></template>
+        {{ fenToYuan(formData.discountPrice!) }} 
       </el-descriptions-item>
       </el-descriptions-item>
       <el-descriptions-item>
       <el-descriptions-item>
         <template #label><span style="color: red">积分抵扣: </span></template>
         <template #label><span style="color: red">积分抵扣: </span></template>
-        {{ floatToFixed2(formData.pointPrice!) }}
+        {{ fenToYuan(formData.pointPrice!) }} 
       </el-descriptions-item>
       </el-descriptions-item>
-
-      <el-descriptions-item v-for="item in 5" :key="item" label-class-name="no-colon" />
-      <!-- 占位 -->
+      <!-- 第三层 -->
+      <el-descriptions-item v-for="item in 3" :key="item" label-class-name="no-colon" />
       <el-descriptions-item label="应付金额: ">
       <el-descriptions-item label="应付金额: ">
-        {{ floatToFixed2(formData.payPrice!) }}
+        {{ fenToYuan(formData.payPrice!) }} 
       </el-descriptions-item>
       </el-descriptions-item>
     </el-descriptions>
     </el-descriptions>
 
 
-    <!-- TODO 芋艿:需要改改 -->
-    <el-descriptions :column="4" title="物流信息">
-      <el-descriptions-item label="物流公司: ">
-        {{ deliveryExpressList.find((item) => item.id === formData.logisticsId)?.name }}
-      </el-descriptions-item>
-      <el-descriptions-item label="运单号: ">{{ formData.logisticsNo }}</el-descriptions-item>
-      <el-descriptions-item label="发货时间: ">
-        {{ formatDate(formData.deliveryTime!) }}
-      </el-descriptions-item>
-      <el-descriptions-item label="物流状态: ">
-        <!-- TODO 物流状态怎么获取? -->
-        <dict-tag :type="DICT_TYPE.TRADE_ORDER_STATUS" :value="formData.deliveryStatus!" />
-      </el-descriptions-item>
-      <!-- 占位 4 -->
-      <el-descriptions-item v-for="item in 4" :key="item" label-class-name="no-colon" />
-      <el-descriptions-item label="物流详情: ">
-        <el-timeline>
-          <el-timeline-item
-            v-for="(express, index) in expressTrackList"
-            :key="index"
-            :timestamp="formatDate(express.time)"
-          >
-            {{ express.content }}
-          </el-timeline-item>
-        </el-timeline>
+    <!-- 物流信息 -->
+    <el-descriptions :column="4" title="收货信息">
+      <el-descriptions-item label="配送方式: ">
+        <dict-tag :type="DICT_TYPE.TRADE_DELIVERY_TYPE" :value="formData.deliveryType!" />
       </el-descriptions-item>
       </el-descriptions-item>
+      <el-descriptions-item label="收货人: ">{{ formData.receiverName }}</el-descriptions-item>
+      <el-descriptions-item label="联系电话: ">{{ formData.receiverMobile }}</el-descriptions-item>
+      <!-- 快递配送 -->
+      <div v-if="formData.deliveryType === DeliveryTypeEnum.EXPRESS.type">
+        <el-descriptions-item label="收货地址: " v-if="formData.receiverDetailAddress">
+          {{ formData.receiverAreaName }} {{ formData.receiverDetailAddress }}
+          <el-link
+            v-clipboard:copy="formData.receiverAreaName + ' ' + formData.receiverDetailAddress"
+            v-clipboard:success="clipboardSuccess"
+            icon="ep:document-copy"
+            type="primary"
+          />
+        </el-descriptions-item>
+        <el-descriptions-item label="物流公司: " v-if="formData.logisticsId">
+          {{ deliveryExpressList.find((item) => item.id === formData.logisticsId)?.name }}
+        </el-descriptions-item>
+        <el-descriptions-item label="运单号: " v-if="formData.logisticsId">
+          {{ formData.logisticsNo }}
+        </el-descriptions-item>
+        <el-descriptions-item label="发货时间: " v-if="formatDate.deliveryTime">
+          {{ formatDate(formData.deliveryTime) }}
+        </el-descriptions-item>
+        <el-descriptions-item v-for="item in 2" :key="item" label-class-name="no-colon" />
+        <el-descriptions-item label="物流详情: " v-if="expressTrackList.length > 0">
+          <el-timeline>
+            <el-timeline-item
+              v-for="(express, index) in expressTrackList"
+              :key="index"
+              :timestamp="formatDate(express.time)"
+            >
+              {{ express.content }}
+            </el-timeline-item>
+          </el-timeline>
+        </el-descriptions-item>
+      </div>
+      <!-- 自提门店 -->
+      <div v-if="formData.deliveryType === DeliveryTypeEnum.PICK_UP.type">
+        <el-descriptions-item label="自提门店: " v-if="formData.pickUpStoreId">
+          {{ pickUpStore?.name }}
+        </el-descriptions-item>
+      </div>
     </el-descriptions>
     </el-descriptions>
+
+    <!-- 订单日志 -->
     <el-descriptions title="订单操作日志">
     <el-descriptions title="订单操作日志">
       <el-descriptions-item labelClassName="no-colon">
       <el-descriptions-item labelClassName="no-colon">
         <el-timeline>
         <el-timeline>
@@ -187,7 +224,7 @@
 </template>
 </template>
 <script lang="ts" setup>
 <script lang="ts" setup>
 import * as TradeOrderApi from '@/api/mall/trade/order'
 import * as TradeOrderApi from '@/api/mall/trade/order'
-import { floatToFixed2 } from '@/utils'
+import { fenToYuan } from '@/utils'
 import { formatDate } from '@/utils/formatTime'
 import { formatDate } from '@/utils/formatTime'
 import { DICT_TYPE, getDictLabel, getDictObj } from '@/utils/dict'
 import { DICT_TYPE, getDictLabel, getDictObj } from '@/utils/dict'
 import OrderUpdateRemarkForm from '@/views/mall/trade/order/form/OrderUpdateRemarkForm.vue'
 import OrderUpdateRemarkForm from '@/views/mall/trade/order/form/OrderUpdateRemarkForm.vue'
@@ -196,6 +233,8 @@ import OrderUpdateAddressForm from '@/views/mall/trade/order/form/OrderUpdateAdd
 import OrderUpdatePriceForm from '@/views/mall/trade/order/form/OrderUpdatePriceForm.vue'
 import OrderUpdatePriceForm from '@/views/mall/trade/order/form/OrderUpdatePriceForm.vue'
 import * as DeliveryExpressApi from '@/api/mall/trade/delivery/express'
 import * as DeliveryExpressApi from '@/api/mall/trade/delivery/express'
 import { useTagsViewStore } from '@/store/modules/tagsView'
 import { useTagsViewStore } from '@/store/modules/tagsView'
+import { DeliveryTypeEnum, TradeOrderStatusEnum } from '@/utils/constants'
+import * as DeliveryPickUpStoreApi from '@/api/mall/trade/delivery/pickUpStore'
 
 
 defineOptions({ name: 'TradeOrderDetail' })
 defineOptions({ name: 'TradeOrderDetail' })
 
 
@@ -240,14 +279,27 @@ const updatePrice = () => {
   updatePriceFormRef.value?.open(formData.value)
   updatePriceFormRef.value?.open(formData.value)
 }
 }
 
 
+/** 核销 */
+const handlePickUp = async () => {
+  try {
+    // 二次确认
+    await message.confirm('确认核销订单吗?')
+    // 提交
+    await TradeOrderApi.pickUpOrder(formData.value.id!)
+    message.success('核销成功')
+    // 刷新列表
+    await getDetail()
+  } catch {}
+}
+
 /** 获得详情 */
 /** 获得详情 */
 const { params } = useRoute() // 查询参数
 const { params } = useRoute() // 查询参数
 const getDetail = async () => {
 const getDetail = async () => {
-  const id = params.orderId as unknown as number
+  const id = params.id as unknown as number
   if (id) {
   if (id) {
     const res = (await TradeOrderApi.getOrder(id)) as TradeOrderApi.OrderVO
     const res = (await TradeOrderApi.getOrder(id)) as TradeOrderApi.OrderVO
     // 没有表单信息则关闭页面返回
     // 没有表单信息则关闭页面返回
-    if (res === null) {
+    if (!res) {
       message.error('交易订单不存在')
       message.error('交易订单不存在')
       close()
       close()
     }
     }
@@ -271,10 +323,20 @@ const clipboardSuccess = () => {
 /** 初始化 **/
 /** 初始化 **/
 const deliveryExpressList = ref([]) // 物流公司
 const deliveryExpressList = ref([]) // 物流公司
 const expressTrackList = ref([]) // 物流详情
 const expressTrackList = ref([]) // 物流详情
+const pickUpStore = ref({}) // 自提门店
 onMounted(async () => {
 onMounted(async () => {
   await getDetail()
   await getDetail()
-  deliveryExpressList.value = await DeliveryExpressApi.getSimpleDeliveryExpressList()
-  expressTrackList.value = await TradeOrderApi.getExpressTrackList(formData.value.id!)
+  // 如果配送方式为快递,则查询物流公司
+  if (formData.value.deliveryType === DeliveryTypeEnum.EXPRESS.type) {
+    deliveryExpressList.value = await DeliveryExpressApi.getSimpleDeliveryExpressList()
+    if (form.value.logisticsId) {
+      expressTrackList.value = await TradeOrderApi.getExpressTrackList(formData.value.id!)
+    }
+  } else if (formData.value.deliveryType === DeliveryTypeEnum.PICK_UP.type) {
+    pickUpStore.value = await DeliveryPickUpStoreApi.getDeliveryPickUpStore(
+      formData.value.pickUpStoreId
+    )
+  }
 })
 })
 </script>
 </script>
 <style lang="scss" scoped>
 <style lang="scss" scoped>
@@ -312,7 +374,7 @@ onMounted(async () => {
 
 
 // 时间线样式调整
 // 时间线样式调整
 :deep(.el-timeline) {
 :deep(.el-timeline) {
-  margin: 10px 0px 0px 160px;
+  margin: 10px 0 0 160px;
 
 
   .el-timeline-item__wrapper {
   .el-timeline-item__wrapper {
     position: relative;
     position: relative;

+ 2 - 2
src/views/mall/trade/order/form/OrderDeliveryForm.vue

@@ -54,7 +54,7 @@ const open = async (row: TradeOrderApi.OrderVO) => {
   resetForm()
   resetForm()
   // 设置数据
   // 设置数据
   copyValueToTarget(formData.value, row)
   copyValueToTarget(formData.value, row)
-  if (row.logisticsId === null || row.logisticsId === 0) {
+  if (row.logisticsId === 0) {
     expressType.value = 'none'
     expressType.value = 'none'
   }
   }
   dialogVisible.value = true
   dialogVisible.value = true
@@ -73,7 +73,7 @@ const submitForm = async () => {
       data.logisticsId = 0
       data.logisticsId = 0
       data.logisticsNo = ''
       data.logisticsNo = ''
     }
     }
-    await TradeOrderApi.delivery(data)
+    await TradeOrderApi.deliveryOrder(data)
     message.success(t('common.updateSuccess'))
     message.success(t('common.updateSuccess'))
     dialogVisible.value = false
     dialogVisible.value = false
     // 发送操作成功的事件
     // 发送操作成功的事件

+ 1 - 1
src/views/mall/trade/order/form/OrderUpdateAddressForm.vue

@@ -69,7 +69,7 @@ const submitForm = async () => {
   formLoading.value = true
   formLoading.value = true
   try {
   try {
     const data = unref(formData)
     const data = unref(formData)
-    await TradeOrderApi.updateAddress(data)
+    await TradeOrderApi.updateOrderAddress(data)
     message.success(t('common.updateSuccess'))
     message.success(t('common.updateSuccess'))
     dialogVisible.value = false
     dialogVisible.value = false
     // 发送操作成功的事件
     // 发送操作成功的事件

+ 1 - 1
src/views/mall/trade/order/form/OrderUpdatePriceForm.vue

@@ -69,7 +69,7 @@ const submitForm = async () => {
     data.adjustPrice = convertToInteger(data.adjustPrice)
     data.adjustPrice = convertToInteger(data.adjustPrice)
     delete data.payPrice
     delete data.payPrice
     delete data.newPayPrice
     delete data.newPayPrice
-    await TradeOrderApi.updatePrice(data)
+    await TradeOrderApi.updateOrderPrice(data)
     message.success(t('common.updateSuccess'))
     message.success(t('common.updateSuccess'))
     dialogVisible.value = false
     dialogVisible.value = false
     // 发送操作成功的事件
     // 发送操作成功的事件

+ 1 - 1
src/views/mall/trade/order/form/OrderUpdateRemarkForm.vue

@@ -49,7 +49,7 @@ const submitForm = async () => {
   formLoading.value = true
   formLoading.value = true
   try {
   try {
     const data = unref(formData)
     const data = unref(formData)
-    await TradeOrderApi.updateRemark(data)
+    await TradeOrderApi.updateOrderRemark(data)
     message.success(t('common.updateSuccess'))
     message.success(t('common.updateSuccess'))
     dialogVisible.value = false
     dialogVisible.value = false
     // 发送操作成功的事件
     // 发送操作成功的事件

+ 48 - 18
src/views/mall/trade/order/index.vue

@@ -74,7 +74,11 @@
           />
           />
         </el-select>
         </el-select>
       </el-form-item>
       </el-form-item>
-      <el-form-item v-if="queryParams.deliveryType === 1" label="快递公司">
+      <el-form-item
+        v-if="queryParams.deliveryType === DeliveryTypeEnum.EXPRESS.type"
+        label="快递公司"
+        prop="logisticsId"
+      >
         <el-select v-model="queryParams.logisticsId" class="!w-280px" clearable placeholder="全部">
         <el-select v-model="queryParams.logisticsId" class="!w-280px" clearable placeholder="全部">
           <el-option
           <el-option
             v-for="item in deliveryExpressList"
             v-for="item in deliveryExpressList"
@@ -84,7 +88,11 @@
           />
           />
         </el-select>
         </el-select>
       </el-form-item>
       </el-form-item>
-      <el-form-item v-if="queryParams.deliveryType === 2" label="自提门店">
+      <el-form-item
+        v-if="queryParams.deliveryType === DeliveryTypeEnum.PICK_UP.type"
+        label="自提门店"
+        prop="pickUpStoreId"
+      >
         <el-select
         <el-select
           v-model="queryParams.pickUpStoreId"
           v-model="queryParams.pickUpStoreId"
           class="!w-280px"
           class="!w-280px"
@@ -100,26 +108,37 @@
           />
           />
         </el-select>
         </el-select>
       </el-form-item>
       </el-form-item>
-      <!-- TODO puhui 聚合搜索等售后结束后实现-->
-      <!-- TODO puhui999:尽量不要用 .k 这样的参数,完整拼写,有完整的业务含义 -->
+      <el-form-item
+        v-if="queryParams.deliveryType === DeliveryTypeEnum.PICK_UP.type"
+        label="核销码"
+        prop="pickUpVerifyCode"
+      >
+        <el-input
+          v-model="queryParams.pickUpVerifyCode"
+          class="!w-280px"
+          clearable
+          placeholder="请输入自提核销码"
+          @keyup.enter="handleQuery"
+        />
+      </el-form-item>
       <el-form-item label="聚合搜索">
       <el-form-item label="聚合搜索">
         <el-input
         <el-input
           v-show="true"
           v-show="true"
-          v-model="queryParams[queryType.k]"
+          v-model="queryParams[queryType.queryParam]"
           class="!w-280px"
           class="!w-280px"
           clearable
           clearable
           placeholder="请输入"
           placeholder="请输入"
         >
         >
           <template #prepend>
           <template #prepend>
             <el-select
             <el-select
-              v-model="queryType.k"
+              v-model="queryType.queryParam"
               class="!w-110px"
               class="!w-110px"
               clearable
               clearable
               placeholder="全部"
               placeholder="全部"
               @change="inputChangeSelect"
               @change="inputChangeSelect"
             >
             >
               <el-option
               <el-option
-                v-for="dict in searchList"
+                v-for="dict in dynamicSearchList"
                 :key="dict.value"
                 :key="dict.value"
                 :label="dict.label"
                 :label="dict.label"
                 :value="dict.value"
                 :value="dict.value"
@@ -234,7 +253,10 @@
             <el-table-column label="买家/收货人" min-width="160">
             <el-table-column label="买家/收货人" min-width="160">
               <template #default>
               <template #default>
                 <!-- 快递发货  -->
                 <!-- 快递发货  -->
-                <div v-if="scope.row.deliveryType === 1" class="flex flex-col">
+                <div
+                  v-if="scope.row.deliveryType === DeliveryTypeEnum.EXPRESS.type"
+                  class="flex flex-col"
+                >
                   <span>买家:{{ scope.row.user.nickname }}</span>
                   <span>买家:{{ scope.row.user.nickname }}</span>
                   <span>
                   <span>
                     收货人:{{ scope.row.receiverName }} {{ scope.row.receiverMobile }}
                     收货人:{{ scope.row.receiverName }} {{ scope.row.receiverMobile }}
@@ -242,7 +264,10 @@
                   </span>
                   </span>
                 </div>
                 </div>
                 <!-- 自提  -->
                 <!-- 自提  -->
-                <div v-if="scope.row.deliveryType === 2" class="flex flex-col">
+                <div
+                  v-if="scope.row.deliveryType === DeliveryTypeEnum.PICK_UP.type"
+                  class="flex flex-col"
+                >
                   <span>
                   <span>
                     门店名称:
                     门店名称:
                     {{ pickUpStoreList.find((p) => p.id === scope.row.pickUpStoreId)?.name }}
                     {{ pickUpStoreList.find((p) => p.id === scope.row.pickUpStoreId)?.name }}
@@ -273,7 +298,7 @@
             <el-table-column align="center" fixed="right" label="操作" width="160">
             <el-table-column align="center" fixed="right" label="操作" width="160">
               <template #default>
               <template #default>
                 <!-- TODO 权限后续补齐 -->
                 <!-- TODO 权限后续补齐 -->
-                <div class="flex justify-center items-center">
+                <div class="flex items-center justify-center">
                   <el-button link type="primary" @click="openDetail(scope.row.id)">
                   <el-button link type="primary" @click="openDetail(scope.row.id)">
                     <Icon icon="ep:notification" />
                     <Icon icon="ep:notification" />
                     详情
                     详情
@@ -287,7 +312,10 @@
                       <el-dropdown-menu>
                       <el-dropdown-menu>
                         <!-- 如果是【快递】,并且【未发货】,则展示【发货】按钮 -->
                         <!-- 如果是【快递】,并且【未发货】,则展示【发货】按钮 -->
                         <el-dropdown-item
                         <el-dropdown-item
-                          v-if="scope.row.deliveryType === 1 && scope.row.status === 10"
+                          v-if="
+                            scope.row.deliveryType === DeliveryTypeEnum.EXPRESS.type &&
+                            scope.row.status === TradeOrderStatusEnum.UNDELIVERED.status
+                          "
                           command="delivery"
                           command="delivery"
                         >
                         >
                           <Icon icon="ep:takeaway-box" />
                           <Icon icon="ep:takeaway-box" />
@@ -332,6 +360,7 @@ import { formatDate } from '@/utils/formatTime'
 import { floatToFixed2 } from '@/utils'
 import { floatToFixed2 } from '@/utils'
 import { createImageViewer } from '@/components/ImageViewer'
 import { createImageViewer } from '@/components/ImageViewer'
 import * as DeliveryExpressApi from '@/api/mall/trade/delivery/express'
 import * as DeliveryExpressApi from '@/api/mall/trade/delivery/express'
+import { DeliveryTypeEnum, TradeOrderStatusEnum } from '@/utils/constants'
 
 
 defineOptions({ name: 'TradeOrder' })
 defineOptions({ name: 'TradeOrder' })
 
 
@@ -352,13 +381,13 @@ const queryParams = ref({
   type: null, // 订单类型
   type: null, // 订单类型
   deliveryType: null, // 配送方式
   deliveryType: null, // 配送方式
   logisticsId: null, // 快递公司
   logisticsId: null, // 快递公司
-  pickUpStoreId: null // 自提门店
+  pickUpStoreId: null, // 自提门店
+  pickUpVerifyCode: null // 自提核销码
 })
 })
-const queryType = reactive({ k: '' }) // 订单搜索类型 k
+const queryType = reactive({ queryParam: '' }) // 订单搜索类型 queryParam
 
 
-// 订单聚合搜索 select 类型配置
-// TODO @puhui999:dynamicSearchList,动态搜索;其它相关的变量和方法,都可以朝着这个变量靠哈;这样更容易理解;
-const searchList = ref([
+// 订单聚合搜索 select 类型配置(动态搜索)
+const dynamicSearchList = ref([
   { value: 'no', label: '订单号' },
   { value: 'no', label: '订单号' },
   { value: 'userId', label: '用户UID' },
   { value: 'userId', label: '用户UID' },
   { value: 'userNickname', label: '用户昵称' },
   { value: 'userNickname', label: '用户昵称' },
@@ -369,7 +398,7 @@ const searchList = ref([
  * @param val
  * @param val
  */
  */
 const inputChangeSelect = (val: string) => {
 const inputChangeSelect = (val: string) => {
-  searchList.value
+  dynamicSearchList.value
     .filter((item) => item.value !== val)
     .filter((item) => item.value !== val)
     ?.forEach((item1) => {
     ?.forEach((item1) => {
       // 清除集合搜索无用属性
       // 清除集合搜索无用属性
@@ -443,6 +472,7 @@ const handleQuery = async () => {
 const resetQuery = () => {
 const resetQuery = () => {
   queryFormRef.value?.resetFields()
   queryFormRef.value?.resetFields()
   queryParams.value = {
   queryParams.value = {
+    pickUpVerifyCode: null, // 自提核销码
     pageNo: 1, // 页数
     pageNo: 1, // 页数
     pageSize: 10, // 每页显示数量
     pageSize: 10, // 每页显示数量
     status: null, // 订单状态
     status: null, // 订单状态
@@ -466,7 +496,7 @@ const imagePreview = (imgUrl: string) => {
 
 
 /** 查看订单详情 */
 /** 查看订单详情 */
 const openDetail = (id: number) => {
 const openDetail = (id: number) => {
-  push({ name: 'TradeOrderDetail', params: { orderId: id } })
+  push({ name: 'TradeOrderDetail', params: { id } })
 }
 }
 
 
 /** 操作分发 */
 /** 操作分发 */

+ 19 - 19
src/views/member/point/config/index.vue → src/views/member/config/index.vue

@@ -13,13 +13,13 @@
 
 
       <el-tabs>
       <el-tabs>
         <el-tab-pane label="积分">
         <el-tab-pane label="积分">
-          <el-form-item label="积分抵扣" prop="tradeDeductEnable">
-            <el-switch v-model="formData.tradeDeductEnable" style="user-select: none" />
+          <el-form-item label="积分抵扣" prop="pointTradeDeductEnable">
+            <el-switch v-model="formData.pointTradeDeductEnable" style="user-select: none" />
             <el-text class="w-full" size="small" type="info">下单积分是否抵用订单金额</el-text>
             <el-text class="w-full" size="small" type="info">下单积分是否抵用订单金额</el-text>
           </el-form-item>
           </el-form-item>
-          <el-form-item label="积分抵扣" prop="tradeDeductUnitPrice">
+          <el-form-item label="积分抵扣" prop="pointTradeDeductUnitPrice">
             <el-input-number
             <el-input-number
-              v-model="computedTradeDeductUnitPrice"
+              v-model="computedPointTradeDeductUnitPrice"
               placeholder="请输入积分抵扣金额"
               placeholder="请输入积分抵扣金额"
               :precision="2"
               :precision="2"
             />
             />
@@ -27,18 +27,18 @@
               积分抵用比例(1 积分抵多少金额),单位:元
               积分抵用比例(1 积分抵多少金额),单位:元
             </el-text>
             </el-text>
           </el-form-item>
           </el-form-item>
-          <el-form-item label="积分抵扣最大值" prop="tradeDeductMaxPrice">
+          <el-form-item label="积分抵扣最大值" prop="pointTradeDeductMaxPrice">
             <el-input-number
             <el-input-number
-              v-model="formData.tradeDeductMaxPrice"
+              v-model="formData.pointTradeDeductMaxPrice"
               placeholder="请输入积分抵扣最大值"
               placeholder="请输入积分抵扣最大值"
             />
             />
             <el-text class="w-full" size="small" type="info">
             <el-text class="w-full" size="small" type="info">
               单次下单积分使用上限,0 不限制
               单次下单积分使用上限,0 不限制
             </el-text>
             </el-text>
           </el-form-item>
           </el-form-item>
-          <el-form-item label="1 元赠送多少分" prop="tradeGivePoint">
+          <el-form-item label="1 元赠送多少分" prop="pointTradeGivePoint">
             <el-input-number
             <el-input-number
-              v-model="formData.tradeGivePoint"
+              v-model="formData.pointTradeGivePoint"
               placeholder="请输入 1 元赠送多少积分"
               placeholder="请输入 1 元赠送多少积分"
             />
             />
             <el-text class="w-full" size="small" type="info">
             <el-text class="w-full" size="small" type="info">
@@ -55,9 +55,9 @@
   </ContentWrap>
   </ContentWrap>
 </template>
 </template>
 <script lang="ts" setup>
 <script lang="ts" setup>
-import * as ConfigApi from '@/api/member/point/config'
+import * as ConfigApi from '@/api/member/config'
 
 
-defineOptions({ name: 'MemberPointConfig' })
+defineOptions({ name: 'MemberConfig' })
 
 
 const { t } = useI18n() // 国际化
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 const message = useMessage() // 消息弹窗
@@ -66,17 +66,17 @@ const dialogVisible = ref(false) // 弹窗的是否展示
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formData = ref({
 const formData = ref({
   id: undefined,
   id: undefined,
-  tradeDeductEnable: true,
-  tradeDeductUnitPrice: 0,
-  tradeDeductMaxPrice: 0,
-  tradeGivePoint: 0
+  pointTradeDeductEnable: true,
+  pointTradeDeductUnitPrice: 0,
+  pointTradeDeductMaxPrice: 0,
+  pointTradeGivePoint: 0
 })
 })
 
 
-// 创建一个计算属性,用于将 tradeDeductUnitPrice 显示为带两位小数的形式
-const computedTradeDeductUnitPrice = computed({
-  get: () => (formData.value.tradeDeductUnitPrice / 100).toFixed(2),
-  set: (newValue) => {
-    formData.value.tradeDeductUnitPrice = Math.round(newValue * 100)
+// 创建一个计算属性,用于将 pointTradeDeductUnitPrice 显示为带两位小数的形式
+const computedPointTradeDeductUnitPrice = computed({
+  get: () => (formData.value.pointTradeDeductUnitPrice / 100).toFixed(2),
+  set: (newValue: number) => {
+    formData.value.pointTradeDeductUnitPrice = Math.round(newValue * 100)
   }
   }
 })
 })
 
 

+ 32 - 13
src/views/member/signin/config/SignInConfigForm.vue

@@ -13,8 +13,11 @@
           只允许设置 1-7,默认签到 7 天为一个周期
           只允许设置 1-7,默认签到 7 天为一个周期
         </el-text>
         </el-text>
       </el-form-item>
       </el-form-item>
-      <el-form-item label="签到分数" prop="point">
-        <el-input-number v-model="formData.point" :precision="0" />
+      <el-form-item label="奖励积分" prop="point">
+        <el-input-number v-model="formData.point" :min="0" :precision="0" />
+      </el-form-item>
+      <el-form-item label="奖励经验" prop="experience">
+        <el-input-number v-model="formData.experience" :min="0" :precision="0" />
       </el-form-item>
       </el-form-item>
       <el-form-item label="开启状态" prop="status">
       <el-form-item label="开启状态" prop="status">
         <el-radio-group v-model="formData.status">
         <el-radio-group v-model="formData.status">
@@ -46,12 +49,30 @@ const dialogVisible = ref(false) // 弹窗的是否展示
 const dialogTitle = ref('') // 弹窗的标题
 const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
-const formData = ref({
-  id: undefined,
-  day: undefined,
-  point: undefined
+const formData = ref<SignInConfigApi.SignInConfigVO>({} as SignInConfigApi.SignInConfigVO)
+// 奖励校验规则
+const awardValidator = (rule: any, _value: any, callback: any) => {
+  if (!formData.value.point && !formData.value.experience) {
+    callback(new Error('奖励积分与奖励经验至少配置一个'))
+    return
+  }
+
+  // 清除另一个字段的错误提示
+  const otherAwardField = rule?.field === 'point' ? 'experience' : 'point'
+  formRef.value.validateField(otherAwardField, () => null)
+  callback()
+}
+const formRules = reactive({
+  day: [{ required: true, message: '签到天数不能空', trigger: 'blur' }],
+  point: [
+    { required: true, message: '奖励积分不能空', trigger: 'blur' },
+    { validator: awardValidator, trigger: 'blur' }
+  ],
+  experience: [
+    { required: true, message: '奖励经验不能空', trigger: 'blur' },
+    { validator: awardValidator, trigger: 'blur' }
+  ]
 })
 })
-const formRules = reactive({})
 const formRef = ref() // 表单 Ref
 const formRef = ref() // 表单 Ref
 
 
 /** 打开弹窗 */
 /** 打开弹窗 */
@@ -82,14 +103,11 @@ const submitForm = async () => {
   // 提交请求
   // 提交请求
   formLoading.value = true
   formLoading.value = true
   try {
   try {
-    const data = formData.value as unknown as SignInConfigApi.SignInConfigVO
     if (formType.value === 'create') {
     if (formType.value === 'create') {
-      //默认新创建的自动启动
-      data.enable = true
-      await SignInConfigApi.createSignInConfig(data)
+      await SignInConfigApi.createSignInConfig(formData.value)
       message.success(t('common.createSuccess'))
       message.success(t('common.createSuccess'))
     } else {
     } else {
-      await SignInConfigApi.updateSignInConfig(data)
+      await SignInConfigApi.updateSignInConfig(formData.value)
       message.success(t('common.updateSuccess'))
       message.success(t('common.updateSuccess'))
     }
     }
     dialogVisible.value = false
     dialogVisible.value = false
@@ -105,7 +123,8 @@ const resetForm = () => {
   formData.value = {
   formData.value = {
     id: undefined,
     id: undefined,
     day: undefined,
     day: undefined,
-    point: undefined,
+    point: 0,
+    experience: 0,
     status: CommonStatusEnum.ENABLE
     status: CommonStatusEnum.ENABLE
   }
   }
   formRef.value?.resetFields()
   formRef.value?.resetFields()

+ 2 - 1
src/views/member/signin/config/index.vue

@@ -20,7 +20,8 @@
         prop="day"
         prop="day"
         :formatter="(_, __, cellValue) => ['第', cellValue, '天'].join(' ')"
         :formatter="(_, __, cellValue) => ['第', cellValue, '天'].join(' ')"
       />
       />
-      <el-table-column label="获得积分" align="center" prop="point" />
+      <el-table-column label="奖励积分" align="center" prop="point" />
+      <el-table-column label="奖励经验" align="center" prop="experience" />
       <el-table-column label="状态" align="center" prop="status">
       <el-table-column label="状态" align="center" prop="status">
         <template #default="scope">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
           <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />

+ 0 - 0
src/views/member/user/UpdateLevelForm.vue → src/views/member/user/UserLevelUpdateForm.vue


+ 128 - 0
src/views/member/user/UserPointUpdateForm.vue

@@ -0,0 +1,128 @@
+<template>
+  <Dialog title="修改用户积分" v-model="dialogVisible" width="600">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="用户编号" prop="id">
+        <el-input v-model="formData.id" class="!w-240px" disabled />
+      </el-form-item>
+      <el-form-item label="用户昵称" prop="nickname">
+        <el-input v-model="formData.nickname" class="!w-240px" disabled />
+      </el-form-item>
+      <el-form-item label="变动前积分" prop="point">
+        <el-input-number v-model="formData.point" class="!w-240px" disabled />
+      </el-form-item>
+      <el-form-item label="变动类型" prop="changeType">
+        <el-radio-group v-model="formData.changeType">
+          <el-radio :label="1">增加</el-radio>
+          <el-radio :label="-1">减少</el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="变动积分" prop="changePoint">
+        <el-input-number v-model="formData.changePoint" class="!w-240px" :min="0" :precision="0" />
+      </el-form-item>
+      <el-form-item label="变动后积分">
+        <el-input-number v-model="pointResult" class="!w-240px" disabled />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import * as UserApi from '@/api/member/user'
+
+/** 修改用户积分表单 */
+defineOptions({ name: 'UpdatePointForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formData = ref({
+  id: undefined,
+  nickname: undefined,
+  point: 0,
+  changePoint: 0,
+  changeType: 1
+})
+const formRules = reactive({
+  changePoint: [{ required: true, message: '变动积分不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (id?: number) => {
+  dialogVisible.value = true
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await UserApi.getUser(id)
+      formData.value.changeType = 1 // 默认增加积分
+      formData.value.changePoint = 0 // 变动积分默认0
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  if (!formRef) return
+  const valid = await formRef.value.validate()
+  if (!valid) return
+
+  if (formData.value.changePoint < 1) {
+    message.error('变动积分不能小于 1')
+    return
+  }
+  if (pointResult.value < 0) {
+    message.error('变动后的积分不能小于 0')
+    return
+  }
+
+  // 提交请求
+  formLoading.value = true
+  try {
+    await UserApi.updateUserPoint({
+      id: formData.value.id,
+      point: formData.value.changePoint * formData.value.changeType
+    })
+
+    message.success(t('common.updateSuccess'))
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    nickname: undefined,
+    levelId: undefined,
+    reason: undefined
+  }
+  formRef.value?.resetFields()
+}
+
+/** 变动后的积分 */
+const pointResult = computed(
+  () => formData.value.point + formData.value.changePoint * formData.value.changeType
+)
+</script>

+ 0 - 14
src/views/member/user/components/growth-list.vue

@@ -1,14 +0,0 @@
-<script lang="ts">
-import { defineComponent } from 'vue'
-
-export default defineComponent({
-  name: 'GrowthList'
-})
-</script>
-
-<!-- TODO @梦:可以读取 member_experience_log 表 -->
-<template>
-  <div>成长值列表</div>
-</template>
-
-<style scoped lang="scss"></style>

+ 31 - 5
src/views/member/user/detail/UserAccountInfo.vue

@@ -24,31 +24,57 @@
       </template>
       </template>
       {{ user.totalPoint || 0 }}
       {{ user.totalPoint || 0 }}
     </el-descriptions-item>
     </el-descriptions-item>
-    <!-- TODO @疯狂:从 wallet 读取下对应字段 -->
     <el-descriptions-item>
     <el-descriptions-item>
       <template #label>
       <template #label>
         <descriptions-item-label label=" 当前余额 " icon="svg-icon:member_balance" />
         <descriptions-item-label label=" 当前余额 " icon="svg-icon:member_balance" />
       </template>
       </template>
-      {{ 0 }}
+      {{ fenToYuan(wallet.balance || 0) }}
     </el-descriptions-item>
     </el-descriptions-item>
     <el-descriptions-item>
     <el-descriptions-item>
       <template #label>
       <template #label>
         <descriptions-item-label label=" 支出金额 " icon="svg-icon:member_expenditure_balance" />
         <descriptions-item-label label=" 支出金额 " icon="svg-icon:member_expenditure_balance" />
       </template>
       </template>
-      {{ 0 }}
+      {{ fenToYuan(wallet.totalExpense || 0) }}
     </el-descriptions-item>
     </el-descriptions-item>
     <el-descriptions-item>
     <el-descriptions-item>
       <template #label>
       <template #label>
         <descriptions-item-label label=" 充值金额 " icon="svg-icon:member_recharge_balance" />
         <descriptions-item-label label=" 充值金额 " icon="svg-icon:member_recharge_balance" />
       </template>
       </template>
-      {{ 0 }}
+      {{ fenToYuan(wallet.totalRecharge || 0) }}
     </el-descriptions-item>
     </el-descriptions-item>
   </el-descriptions>
   </el-descriptions>
 </template>
 </template>
 <script setup lang="ts">
 <script setup lang="ts">
 import { DescriptionsItemLabel } from '@/components/Descriptions'
 import { DescriptionsItemLabel } from '@/components/Descriptions'
 import * as UserApi from '@/api/member/user'
 import * as UserApi from '@/api/member/user'
-const { user } = defineProps<{ user: UserApi.UserVO }>()
+import * as WalletApi from '@/api/pay/wallet'
+import { UserTypeEnum } from '@/utils/constants'
+import { fenToYuan } from '@/utils'
+
+const props = defineProps<{ user: UserApi.UserVO }>() // 用户信息
+const WALLET_INIT_DATA = {
+  balance: 0,
+  totalExpense: 0,
+  totalRecharge: 0
+} as WalletApi.WalletVO // 钱包初始化数据
+const wallet = ref<WalletApi.WalletVO>(WALLET_INIT_DATA) // 钱包信息
+
+/** 查询用户钱包信息 */
+const getUserWallet = async () => {
+  if (!props.user.id) {
+    wallet.value = WALLET_INIT_DATA
+    return
+  }
+  const params = { userId: props.user.id, userType: UserTypeEnum.MEMBER }
+  wallet.value = (await WalletApi.getWallet(params)) || WALLET_INIT_DATA
+}
+
+/** 监听用户编号变化 */
+watch(
+  () => props.user.id,
+  () => getUserWallet(),
+  { immediate: true }
+)
 </script>
 </script>
 <style scoped lang="scss">
 <style scoped lang="scss">
 .cell-item {
 .cell-item {

+ 125 - 0
src/views/member/user/detail/UserBrokerageList.vue

@@ -0,0 +1,125 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="85px"
+    >
+      <el-form-item label="用户类型" prop="level">
+        <el-radio-group v-model="queryParams.level" @change="handleQuery">
+          <el-radio-button checked>全部</el-radio-button>
+          <el-radio-button label="1">一级推广人</el-radio-button>
+          <el-radio-button label="2">二级推广人</el-radio-button>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="绑定时间" prop="bindUserTime">
+        <el-date-picker
+          v-model="queryParams.bindUserTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="用户编号" align="center" prop="id" min-width="80px" />
+      <el-table-column label="头像" align="center" prop="avatar" width="70px">
+        <template #default="scope">
+          <el-avatar :src="scope.row.avatar" />
+        </template>
+      </el-table-column>
+      <el-table-column label="昵称" align="center" prop="nickname" min-width="80px" />
+      <el-table-column label="等级" align="center" prop="level" min-width="80px">
+        <template #default="scope">
+          <el-tag v-if="scope.row.bindUserId === bindUserId">一级</el-tag>
+          <el-tag v-else>二级</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="绑定时间"
+        align="center"
+        prop="bindUserTime"
+        :formatter="dateFormatter"
+        width="170px"
+      />
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import * as BrokerageUserApi from '@/api/mall/trade/brokerage/user'
+
+/** 推广人列表 */
+defineOptions({ name: 'UserBrokerageList' })
+
+const { bindUserId }: { bindUserId: number } = defineProps({
+  bindUserId: {
+    type: Number,
+    required: true
+  }
+}) //用户编号
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  bindUserId: null,
+  level: '',
+  bindUserTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    queryParams.bindUserId = bindUserId
+    const data = await BrokerageUserApi.getBrokerageUserPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 190 - 0
src/views/member/user/detail/UserCouponList.vue

@@ -0,0 +1,190 @@
+<template>
+  <!-- 搜索工作栏 -->
+  <ContentWrap>
+    <el-form
+      ref="queryFormRef"
+      :inline="true"
+      :model="queryParams"
+      class="-mb-15px"
+      label-width="68px"
+    >
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"> <Icon icon="ep:search" class="mr-5px" />搜索 </el-button>
+        <el-button @click="resetQuery"> <Icon icon="ep:refresh" class="mr-5px" />重置 </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <ContentWrap>
+    <!-- Tab 选项:真正的内容在 Lab -->
+    <el-tabs v-model="activeTab" type="card" @tab-change="onTabChange">
+      <el-tab-pane
+        v-for="tab in statusTabs"
+        :key="tab.value"
+        :label="tab.label"
+        :name="tab.value"
+      />
+    </el-tabs>
+
+    <!-- 列表 -->
+    <el-table v-loading="loading" :data="list">
+      <el-table-column label="优惠劵" align="center" prop="name" />
+      <el-table-column label="优惠券类型" align="center" prop="discountType">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.PROMOTION_DISCOUNT_TYPE" :value="scope.row.discountType" />
+        </template>
+      </el-table-column>
+      <el-table-column label="领取方式" align="center" prop="takeType">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.PROMOTION_COUPON_TAKE_TYPE" :value="scope.row.takeType" />
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" align="center" prop="status">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.PROMOTION_COUPON_STATUS" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="领取时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180"
+      />
+      <el-table-column
+        label="使用时间"
+        align="center"
+        prop="useTime"
+        :formatter="dateFormatter"
+        width="180"
+      />
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template #default="scope">
+          <el-button
+            v-hasPermi="['promotion:coupon:delete']"
+            type="danger"
+            link
+            @click="handleDelete(scope.row.id)"
+          >
+            回收
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
+      @pagination="getList"
+    />
+  </ContentWrap>
+</template>
+
+<script setup lang="ts" name="UserCouponList">
+import { deleteCoupon, getCouponPage } from '@/api/mall/promotion/coupon/coupon'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+
+defineOptions({ name: 'UserCouponList' })
+
+const { userId }: { userId: number } = defineProps({
+  userId: {
+    type: Number,
+    required: true
+  }
+}) //用户编号
+
+const message = useMessage() // 消息弹窗
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 字典表格数据
+// 查询参数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  createTime: [],
+  status: undefined,
+  userIds: undefined
+})
+const queryFormRef = ref() // 搜索的表单
+
+const activeTab = ref('all') // Tab 筛选
+const statusTabs = reactive([
+  {
+    label: '全部',
+    value: 'all'
+  }
+])
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  // 执行查询
+  try {
+    queryParams.userIds = userId
+    const data = await getCouponPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 二次确认
+    await message.confirm(
+      '回收将会收回会员领取的待使用的优惠券,已使用的将无法回收,确定要回收所选优惠券吗?'
+    )
+    // 发起删除
+    await deleteCoupon(id)
+    message.notifySuccess('回收成功')
+    // 重新加载列表
+    await getList()
+  } catch {}
+}
+
+/** tab 切换 */
+const onTabChange = (tabName) => {
+  queryParams.status = tabName === 'all' ? undefined : tabName
+  getList()
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+  // 设置 statuses 过滤
+  for (const dict of getIntDictOptions(DICT_TYPE.PROMOTION_COUPON_STATUS)) {
+    statusTabs.push({
+      label: dict.label,
+      value: dict.value as string
+    })
+  }
+})
+</script>

+ 25 - 21
src/views/member/user/detail/index.vue

@@ -16,7 +16,7 @@
       </el-col>
       </el-col>
       <!-- 右上角:账户信息 -->
       <!-- 右上角:账户信息 -->
       <el-col :span="10" class="detail-info-item">
       <el-col :span="10" class="detail-info-item">
-        <el-card shadow="never">
+        <el-card shadow="never" class="h-full">
           <template #header>
           <template #header>
             <CardTitle title="账户信息" />
             <CardTitle title="账户信息" />
           </template>
           </template>
@@ -24,34 +24,37 @@
         </el-card>
         </el-card>
       </el-col>
       </el-col>
       <!-- 下边:账户明细 -->
       <!-- 下边:账户明细 -->
-      <!-- TODO 芋艿:【订单管理】【售后管理】【收藏记录】【优惠劵】 -->
+      <!-- TODO 芋艿:【订单管理】【售后管理】【收藏记录】-->
       <el-card header="账户明细" style="width: 100%; margin-top: 20px" shadow="never">
       <el-card header="账户明细" style="width: 100%; margin-top: 20px" shadow="never">
         <template #header>
         <template #header>
           <CardTitle title="账户明细" />
           <CardTitle title="账户明细" />
         </template>
         </template>
-        <el-tabs v-model="activeName">
-          <el-tab-pane label="积分" name="point">
+        <el-tabs>
+          <el-tab-pane label="积分">
             <UserPointList :user-id="id" />
             <UserPointList :user-id="id" />
           </el-tab-pane>
           </el-tab-pane>
-          <el-tab-pane label="签到" name="sign" lazy>
+          <el-tab-pane label="签到" lazy>
             <UserSignList :user-id="id" />
             <UserSignList :user-id="id" />
           </el-tab-pane>
           </el-tab-pane>
-          <el-tab-pane label="成长值" name="experience" lazy>
-            <UserExperienceRecordList :user-id="id"
-          /></el-tab-pane>
+          <el-tab-pane label="成长值" lazy>
+            <UserExperienceRecordList :user-id="id" />
+          </el-tab-pane>
           <!-- TODO @jason:增加一个余额变化; -->
           <!-- TODO @jason:增加一个余额变化; -->
-          <el-tab-pane label="余额" name="fourth">余额(WIP)</el-tab-pane>
-          <el-tab-pane label="收货地址" name="address" lazy>
+          <el-tab-pane label="余额" lazy>余额(WIP)</el-tab-pane>
+          <el-tab-pane label="收货地址" lazy>
             <UserAddressList :user-id="id" />
             <UserAddressList :user-id="id" />
           </el-tab-pane>
           </el-tab-pane>
-          <el-tab-pane label="订单管理" name="order" lazy>
+          <el-tab-pane label="订单管理" lazy>
             <UserOrderList :user-id="id" />
             <UserOrderList :user-id="id" />
           </el-tab-pane>
           </el-tab-pane>
-          <el-tab-pane label="售后管理" name="fourth">售后管理(WIP)</el-tab-pane>
-          <el-tab-pane label="收藏记录" name="fourth">收藏记录(WIP)</el-tab-pane>
-          <!-- TODO @疯狂:优惠劵的读取 -->
-          <el-tab-pane label="优惠劵" name="fourth">优惠劵(WIP)</el-tab-pane>
-          <!-- TODO @疯狂:增加获得分校用户;直接查询出所有;需要体现出是一级还是二级;用户编号、昵称、级别、绑定时间 -->
+          <el-tab-pane label="售后管理" lazy>售后管理(WIP)</el-tab-pane>
+          <el-tab-pane label="收藏记录" lazy>收藏记录(WIP)</el-tab-pane>
+          <el-tab-pane label="优惠劵" lazy>
+            <UserCouponList :user-id="id" />
+          </el-tab-pane>
+          <el-tab-pane label="推广用户" lazy>
+            <UserBrokerageList :bind-user-id="id" />
+          </el-tab-pane>
         </el-tabs>
         </el-tabs>
       </el-card>
       </el-card>
     </el-row>
     </el-row>
@@ -63,22 +66,23 @@
 <script setup lang="ts">
 <script setup lang="ts">
 import * as UserApi from '@/api/member/user'
 import * as UserApi from '@/api/member/user'
 import { useTagsViewStore } from '@/store/modules/tagsView'
 import { useTagsViewStore } from '@/store/modules/tagsView'
-import UserBasicInfo from './UserBasicInfo.vue'
 import UserForm from '@/views/member/user/UserForm.vue'
 import UserForm from '@/views/member/user/UserForm.vue'
 import UserAccountInfo from './UserAccountInfo.vue'
 import UserAccountInfo from './UserAccountInfo.vue'
 import UserAddressList from './UserAddressList.vue'
 import UserAddressList from './UserAddressList.vue'
+import UserBasicInfo from './UserBasicInfo.vue'
+import UserBrokerageList from './UserBrokerageList.vue'
+import UserCouponList from './UserCouponList.vue'
+import UserExperienceRecordList from './UserExperienceRecordList.vue'
+import UserOrderList from './UserOrderList.vue'
 import UserPointList from './UserPointList.vue'
 import UserPointList from './UserPointList.vue'
 import UserSignList from './UserSignList.vue'
 import UserSignList from './UserSignList.vue'
-import UserExperienceRecordList from './UserExperienceRecordList.vue'
 import { CardTitle } from '@/components/Card/index'
 import { CardTitle } from '@/components/Card/index'
-import UserOrderList from '@/views/member/user/detail/UserOrderList.vue'
 import { ElMessage } from 'element-plus'
 import { ElMessage } from 'element-plus'
 
 
 defineOptions({ name: 'MemberDetail' })
 defineOptions({ name: 'MemberDetail' })
 
 
-const activeName = ref('point') // 账户明细 选中的 tabs
 const loading = ref(true) // 加载中
 const loading = ref(true) // 加载中
-const user = ref<UserApi.UserVO>({})
+const user = ref<UserApi.UserVO>({} as UserApi.UserVO)
 
 
 /** 添加/修改操作 */
 /** 添加/修改操作 */
 const formRef = ref()
 const formRef = ref()

+ 76 - 23
src/views/member/user/index.vue

@@ -117,28 +117,56 @@
         :formatter="dateFormatter"
         :formatter="dateFormatter"
         width="180px"
         width="180px"
       />
       />
-      <el-table-column label="操作" align="center" width="180px" fixed="right">
+      <el-table-column
+        label="操作"
+        align="center"
+        width="100px"
+        fixed="right"
+        :show-overflow-tooltip="false"
+      >
         <template #default="scope">
         <template #default="scope">
-          <el-button link type="primary" @click="openDetail(scope.row.id)">详情</el-button>
-          <el-button
-            link
-            type="primary"
-            @click="openForm('update', scope.row.id)"
-            v-hasPermi="['member:user:update']"
-          >
-            编辑
-          </el-button>
-          <!-- todo @jason:增加一个【修改余额】 -->
-          <!-- todo @疯狂:增加一个【修改积分】,表单是:radio 增加/减少;input 具体的变化积分 -->
-          <!-- todo 放到更多菜单中 -->
-          <el-button
-            link
-            type="primary"
-            @click="updateLevelFormRef.open(scope.row.id)"
-            v-hasPermi="['member:user:update-level']"
-          >
-            修改等级
-          </el-button>
+          <div class="flex items-center justify-center">
+            <el-button link type="primary" @click="openDetail(scope.row.id)">详情</el-button>
+            <el-dropdown
+              @command="(command) => handleCommand(command, scope.row)"
+              v-hasPermi="[
+                'member:user:update',
+                'member:user:update-level',
+                'member:user:update-point',
+                'member:user:update-balance'
+              ]"
+            >
+              <el-button type="primary" link><Icon icon="ep:d-arrow-right" /> 更多</el-button>
+              <template #dropdown>
+                <el-dropdown-menu>
+                  <el-dropdown-item
+                    command="handleUpdate"
+                    v-if="checkPermi(['member:user:update'])"
+                  >
+                    编辑
+                  </el-dropdown-item>
+                  <el-dropdown-item
+                    command="handleUpdateLevel"
+                    v-if="checkPermi(['member:user:update-level'])"
+                  >
+                    修改等级
+                  </el-dropdown-item>
+                  <el-dropdown-item
+                    command="handleUpdatePoint"
+                    v-if="checkPermi(['member:user:update-point'])"
+                  >
+                    修改积分
+                  </el-dropdown-item>
+                  <el-dropdown-item
+                    command="handleUpdateBlance"
+                    v-if="checkPermi(['member:user:update-balance'])"
+                  >
+                    修改余额(WIP)
+                  </el-dropdown-item>
+                </el-dropdown-menu>
+              </template>
+            </el-dropdown>
+          </div>
         </template>
         </template>
       </el-table-column>
       </el-table-column>
     </el-table>
     </el-table>
@@ -154,7 +182,9 @@
   <!-- 表单弹窗:添加/修改 -->
   <!-- 表单弹窗:添加/修改 -->
   <UserForm ref="formRef" @success="getList" />
   <UserForm ref="formRef" @success="getList" />
   <!-- 修改用户等级弹窗 -->
   <!-- 修改用户等级弹窗 -->
-  <UpdateLevelForm ref="updateLevelFormRef" @success="getList" />
+  <UserLevelUpdateForm ref="updateLevelFormRef" @success="getList" />
+  <!-- 修改用户积分弹窗 -->
+  <UserPointUpdateForm ref="updatePointFormRef" @success="getList" />
   <!-- 发送优惠券弹窗 -->
   <!-- 发送优惠券弹窗 -->
   <CouponSendForm ref="couponSendFormRef" />
   <CouponSendForm ref="couponSendFormRef" />
 </template>
 </template>
@@ -166,8 +196,10 @@ import UserForm from './UserForm.vue'
 import MemberTagSelect from '@/views/member/tag/components/MemberTagSelect.vue'
 import MemberTagSelect from '@/views/member/tag/components/MemberTagSelect.vue'
 import MemberLevelSelect from '@/views/member/level/components/MemberLevelSelect.vue'
 import MemberLevelSelect from '@/views/member/level/components/MemberLevelSelect.vue'
 import MemberGroupSelect from '@/views/member/group/components/MemberGroupSelect.vue'
 import MemberGroupSelect from '@/views/member/group/components/MemberGroupSelect.vue'
-import UpdateLevelForm from '@/views/member/user/UpdateLevelForm.vue'
+import UserLevelUpdateForm from './UserLevelUpdateForm.vue'
+import UserPointUpdateForm from './UserPointUpdateForm.vue'
 import CouponSendForm from '@/views/mall/promotion/coupon/components/CouponSendForm.vue'
 import CouponSendForm from '@/views/mall/promotion/coupon/components/CouponSendForm.vue'
+import { checkPermi } from '@/utils/permission'
 
 
 defineOptions({ name: 'MemberUser' })
 defineOptions({ name: 'MemberUser' })
 
 
@@ -189,6 +221,7 @@ const queryParams = reactive({
 })
 })
 const queryFormRef = ref() // 搜索的表单
 const queryFormRef = ref() // 搜索的表单
 const updateLevelFormRef = ref() // 修改会员等级表单
 const updateLevelFormRef = ref() // 修改会员等级表单
+const updatePointFormRef = ref() // 修改会员积分表单
 const selectedIds = ref<number[]>([]) // 表格的选中 ID 数组
 const selectedIds = ref<number[]>([]) // 表格的选中 ID 数组
 
 
 /** 查询列表 */
 /** 查询列表 */
@@ -242,6 +275,26 @@ const openCoupon = () => {
   couponSendFormRef.value.open(selectedIds.value)
   couponSendFormRef.value.open(selectedIds.value)
 }
 }
 
 
+/** 操作分发 */
+const handleCommand = (command: string, row: UserApi.UserVO) => {
+  switch (command) {
+    case 'handleUpdate':
+      openForm('update', row.id)
+      break
+    case 'handleUpdateLevel':
+      updateLevelFormRef.value.open(row.id)
+      break
+    case 'handleUpdatePoint':
+      updatePointFormRef.value.open(row.id)
+      break
+    case 'handleUpdateBlance':
+      // todo @jason:增加一个【修改余额】
+      break
+    default:
+      break
+  }
+}
+
 /** 初始化 **/
 /** 初始化 **/
 onMounted(() => {
 onMounted(() => {
   getList()
   getList()

部分文件因为文件数量过多而无法显示