瀏覽代碼

!223 同步最新商城的代码
Merge pull request !223 from 芋道源码/dev

芋道源码 1 年之前
父節點
當前提交
ce84b14d15
共有 72 個文件被更改,包括 3679 次插入1649 次删除
  1. 49 0
      src/api/mall/product/comment.ts
  2. 6 5
      src/api/mall/product/spu.ts
  3. 6 4
      src/api/mall/promotion/bargain/bargainActivity.ts
  4. 5 0
      src/api/mall/trade/delivery/express/index.ts
  5. 6 6
      src/api/mall/trade/delivery/pickUpStore/index.ts
  6. 121 12
      src/api/mall/trade/order/index.ts
  7. 0 228
      src/api/mall/trade/order/type/orderType.ts
  8. 15 0
      src/api/member/address/index.ts
  9. 38 0
      src/api/member/group/index.ts
  10. 42 0
      src/api/member/level/index.ts
  11. 0 11
      src/api/member/point/record/index.ts
  12. 5 5
      src/api/member/signin/config/index.ts
  13. 1 10
      src/api/member/signin/record/index.ts
  14. 6 1
      src/api/member/tag/index.ts
  15. 20 27
      src/api/member/user/index.ts
  16. 1 0
      src/assets/svgs/member_balance.svg
  17. 0 0
      src/assets/svgs/member_expenditure_balance.svg
  18. 1 0
      src/assets/svgs/member_level.svg
  19. 0 0
      src/assets/svgs/member_point.svg
  20. 0 0
      src/assets/svgs/member_recharge_balance.svg
  21. 3 0
      src/components/Card/index.ts
  22. 1 1
      src/components/Card/src/CardTitle.vue
  23. 2 1
      src/components/Descriptions/index.ts
  24. 28 0
      src/components/Descriptions/src/DescriptionsItemLabel.vue
  25. 63 32
      src/router/modules/remaining.ts
  26. 3 2
      src/utils/dict.ts
  27. 1 1
      src/utils/formatTime.ts
  28. 25 1
      src/utils/index.ts
  29. 1 1
      src/views/Login/components/LoginForm.vue
  30. 2 2
      src/views/infra/codegen/PreviewCode.vue
  31. 192 0
      src/views/mall/product/comment/CommentForm.vue
  32. 76 0
      src/views/mall/product/comment/ReplyForm.vue
  33. 243 0
      src/views/mall/product/comment/index.vue
  34. 8 3
      src/views/mall/product/property/index.vue
  35. 22 12
      src/views/mall/product/spu/addForm.vue
  36. 5 4
      src/views/mall/product/spu/components/SkuList.vue
  37. 94 0
      src/views/mall/product/spu/components/SkuTableSelect.vue
  38. 167 0
      src/views/mall/product/spu/components/SpuTableSelect.vue
  39. 0 1
      src/views/mall/product/spu/components/index.ts
  40. 9 11
      src/views/mall/product/spu/index.vue
  41. 23 10
      src/views/mall/promotion/bargain/activity/BargainActivityForm.vue
  42. 3 3
      src/views/mall/promotion/combination/record/index.vue
  43. 26 7
      src/views/mall/promotion/components/SpuSelect.vue
  44. 0 12
      src/views/mall/promotion/seckill/activity/SeckillActivityForm.vue
  45. 31 49
      src/views/mall/trade/delivery/pickUpStore/index.vue
  46. 100 0
      src/views/mall/trade/order/components/OrderDeliveryForm.vue
  47. 98 0
      src/views/mall/trade/order/components/OrderUpdateAddressForm.vue
  48. 92 0
      src/views/mall/trade/order/components/OrderUpdatePriceForm.vue
  49. 71 0
      src/views/mall/trade/order/components/OrderUpdateRemarkForm.vue
  50. 358 365
      src/views/mall/trade/order/detail/index.vue
  51. 475 572
      src/views/mall/trade/order/index.vue
  52. 112 0
      src/views/member/group/GroupForm.vue
  53. 45 0
      src/views/member/group/components/MemberGroupSelect.vue
  54. 174 0
      src/views/member/group/index.vue
  55. 175 0
      src/views/member/level/LevelForm.vue
  56. 45 0
      src/views/member/level/components/MemberLevelSelect.vue
  57. 169 0
      src/views/member/level/index.vue
  58. 1 2
      src/views/member/tag/components/MemberTagSelect.vue
  59. 101 0
      src/views/member/user/UpdateLevelForm.vue
  60. 9 6
      src/views/member/user/UserForm.vue
  61. 0 43
      src/views/member/user/components/account-info.vue
  62. 0 13
      src/views/member/user/components/address-list.vue
  63. 1 0
      src/views/member/user/components/balance-list.vue
  64. 1 0
      src/views/member/user/components/growth-list.vue
  65. 60 0
      src/views/member/user/detail/UserAccountInfo.vue
  66. 54 0
      src/views/member/user/detail/UserAddressList.vue
  67. 85 0
      src/views/member/user/detail/UserBasicInfo.vue
  68. 8 9
      src/views/member/user/detail/UserPointList.vue
  69. 7 8
      src/views/member/user/detail/UserSignList.vue
  70. 51 152
      src/views/member/user/detail/index.vue
  71. 36 16
      src/views/member/user/index.vue
  72. 1 1
      src/views/pay/order/index.vue

+ 49 - 0
src/api/mall/product/comment.ts

@@ -0,0 +1,49 @@
+import request from '@/config/axios'
+
+export interface CommentVO {
+  id: number
+  userId: number
+  userNickname: string
+  userAvatar: string
+  anonymous: boolean
+  orderId: number
+  orderItemId: number
+  spuId: number
+  spuName: string
+  skuId: number
+  visible: boolean
+  scores: number
+  descriptionScores: number
+  benefitScores: number
+  content: string
+  picUrls: string
+  replyStatus: boolean
+  replyUserId: number
+  replyContent: string
+  replyTime: Date
+}
+
+// 查询商品评论列表
+export const getCommentPage = async (params) => {
+  return await request.get({ url: `/product/comment/page`, params })
+}
+
+// 查询商品评论详情
+export const getComment = async (id: number) => {
+  return await request.get({ url: `/product/comment/get?id=` + id })
+}
+
+// 添加自评
+export const createComment = async (data: CommentVO) => {
+  return await request.post({ url: `/product/comment/create`, data })
+}
+
+// 显示 / 隐藏评论
+export const updateCommentVisible = async (data: any) => {
+  return await request.put({ url: `/product/comment/update-visible`, data })
+}
+
+// 商家回复
+export const replyComment = async (data: any) => {
+  return await request.put({ url: `/product/comment/reply`, data })
+}

+ 6 - 5
src/api/mall/product/spu.ts

@@ -9,18 +9,19 @@ export interface Property {
 
 export interface Sku {
   id?: number // 商品 SKU 编号
+  name?: string // 商品 SKU 名称
   spuId?: number // SPU 编号
   properties?: Property[] // 属性数组
-  price?: number // 商品价格
-  marketPrice?: number // 市场价
-  costPrice?: number // 成本价
+  price?: number | string // 商品价格
+  marketPrice?: number | string // 市场价
+  costPrice?: number | string // 成本价
   barCode?: string // 商品条码
   picUrl?: string // 图片地址
   stock?: number // 库存
   weight?: number // 商品重量,单位:kg 千克
   volume?: number // 商品体积,单位:m^3 平米
-  subCommissionFirstPrice?: number // 一级分销的佣金
-  subCommissionSecondPrice?: number // 二级分销的佣金
+  subCommissionFirstPrice?: number | string // 一级分销的佣金
+  subCommissionSecondPrice?: number | string // 二级分销的佣金
   salesCount?: number // 商品销量
 }
 

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

@@ -7,18 +7,20 @@ export interface BargainActivityVO {
   startTime?: Date
   endTime?: Date
   status?: number
-  spuId?: number
   userSize?: number // 达到该人数,才能砍到低价
   bargainCount?: number // 最大帮砍次数
   totalLimitCount?: number // 最大购买次数
-  stock?: number // 活动总库存
+  spuId: number
+  skuId: number
+  bargainFirstPrice: number // 砍价起始价格,单位分
+  bargainPrice: number // 砍价底价
+  stock: number // 活动库存
   randomMinPrice?: number // 用户每次砍价的最小金额,单位:分
   randomMaxPrice?: number // 用户每次砍价的最大金额,单位:分
   successCount?: number // 砍价成功数量
-  products?: BargainProductVO[]
 }
 
-// 砍价活动所需属性
+// 砍价活动所需属性。选择的商品和属性的时候使用方便使用活动的通用封装
 export interface BargainProductVO {
   spuId: number
   skuId: number

+ 5 - 0
src/api/mall/trade/delivery/express/index.ts

@@ -19,6 +19,11 @@ export const getDeliveryExpress = async (id: number) => {
   return await request.get({ url: '/trade/delivery/express/get?id=' + id })
 }
 
+// 获得快递公司精简信息列表
+export const getSimpleDeliveryExpressList = () => {
+  return request.get({ url: '/trade/delivery/express/list-all-simple' })
+}
+
 // 新增快递公司
 export const createDeliveryExpress = async (data: DeliveryExpressVO) => {
   return await request.post({ url: '/trade/delivery/express/create', data })

+ 6 - 6
src/api/mall/trade/delivery/pickUpStore/index.ts

@@ -16,7 +16,7 @@ export interface DeliveryPickUpStoreVO {
 }
 
 // 查询自提门店列表
-export const getDeliveryPickUpStorePage = async (params: DeliveryPickUpStorePageReqVO) => {
+export const getDeliveryPickUpStorePage = async (params) => {
   return await request.get({ url: '/trade/delivery/pick-up-store/page', params })
 }
 
@@ -25,6 +25,11 @@ export const getDeliveryPickUpStore = async (id: number) => {
   return await request.get({ url: '/trade/delivery/pick-up-store/get?id=' + id })
 }
 
+// 查询自提门店精简列表
+export const getListAllSimple = async () => {
+  return await request.get({ url: '/trade/delivery/pick-up-store/list-all-simple' })
+}
+
 // 新增自提门店
 export const createDeliveryPickUpStore = async (data: DeliveryPickUpStoreVO) => {
   return await request.post({ url: '/trade/delivery/pick-up-store/create', data })
@@ -39,8 +44,3 @@ export const updateDeliveryPickUpStore = async (data: DeliveryPickUpStoreVO) =>
 export const deleteDeliveryPickUpStore = async (id: number) => {
   return await request.delete({ url: '/trade/delivery/pick-up-store/delete?id=' + id })
 }
-
-// 导出自提门店 Excel
-export const exportDeliveryPickUpStoreApi = async (params) => {
-  return await request.download({ url: '/trade/delivery/pick-up-store/export-excel', params })
-}

+ 121 - 12
src/api/mall/trade/order/index.ts

@@ -1,12 +1,121 @@
-import request from '@/config/axios'
-
-// 获得交易订单分页
-// TODO @xiaobai:改成 getOrderPage
-export const getOrderList = (params: PageParam) => {
-  return request.get({ url: '/trade/order/page', params })
-}
-
-// 获得交易订单详情
-export const getOrderDetail = (id: number) => {
-  return request.get({ url: '/trade/order/get-detail?id=' + id })
-}
+import request from '@/config/axios'
+
+export interface OrderVO {
+  id?: number | null // 订单编号
+  no?: string // 订单流水号
+  createTime?: Date | null // 下单时间
+  type?: number | null // 订单类型
+  terminal?: number | null // 订单来源
+  userId?: number | null // 用户编号
+  userIp?: string // 用户 IP
+  userRemark?: string // 用户备注
+  status?: number | null // 订单状态
+  productCount?: number | null // 购买的商品数量
+  finishTime?: Date | null // 订单完成时间
+  cancelTime?: Date | null // 订单取消时间
+  cancelType?: number | null // 取消类型
+  remark?: string // 商家备注
+  payOrderId: number | null // 支付订单编号
+  payed?: boolean // 是否已支付
+  payTime?: Date | null // 付款时间
+  payChannelCode?: string // 支付渠道
+  originalPrice?: number | null // 商品原价(总)
+  orderPrice?: number | null // 订单原价(总)
+  discountPrice?: number | null // 订单优惠(总)
+  deliveryPrice?: number | null // 运费金额
+  adjustPrice?: number | null // 订单调价(总)
+  payPrice?: number | null // 应付金额(总)
+  deliveryType?: number | null // 发货方式
+  deliveryTemplateId?: number | null // 配送模板编号
+  logisticsId?: number | null | null // 发货物流公司编号
+  logisticsNo?: string // 发货物流单号
+  deliveryStatus?: number | null // 发货状态
+  deliveryTime?: Date | null // 发货时间
+  receiveTime?: Date | null // 收货时间
+  receiverName?: string // 收件人名称
+  receiverMobile?: string // 收件人手机
+  receiverAreaId?: number | null // 收件人地区编号
+  receiverPostCode?: number | null // 收件人邮编
+  receiverDetailAddress?: string // 收件人详细地址
+  afterSaleStatus?: number | null // 售后状态
+  refundPrice?: number | null // 退款金额
+  couponId?: number | null // 优惠劵编号
+  couponPrice?: number | null // 优惠劵减免金额
+  pointPrice?: number | null // 积分抵扣的金额
+  receiverAreaName?: string //收件人地区名字
+  items?: OrderItemRespVO[] // 订单项列表
+  //用户信息
+  user?: {
+    id?: number | null
+    nickname?: string
+    avatar?: string
+  }
+}
+
+export interface OrderItemRespVO {
+  // ========== 订单项基本信息 ==========
+  id?: number | null // 编号
+  userId?: number | null // 用户编号
+  orderId?: number | null // 订单编号
+  // ========== 商品基本信息 ==========
+  spuId?: number | null // 商品 SPU 编号
+  spuName?: string //商品 SPU 名称
+  skuId?: number | null // 商品 SKU 编号
+  picUrl?: string //商品图片
+  count?: number | null //购买数量
+  // ========== 价格 + 支付基本信息 ==========
+  originalPrice?: number | null //商品原价(总)
+  originalUnitPrice?: number | null //商品原价(单)
+  discountPrice?: number | null //商品优惠(总)
+  payPrice?: number | null //商品实付金额(总)
+  orderPartPrice?: number | null //子订单分摊金额(总)
+  orderDividePrice?: number | null //分摊后子订单实付金额(总)
+  // ========== 营销基本信息 ==========
+  // TODO 芋艿:在捉摸一下
+  // ========== 售后基本信息 ==========
+  afterSaleStatus?: number | null // 售后状态
+  properties?: ProductPropertiesVO[] //属性数组
+}
+
+export interface ProductPropertiesVO {
+  propertyId?: number | null // 属性的编号
+  propertyName?: string // 属性的名称
+  valueId?: number | null //属性值的编号
+  valueName?: string // 属性值的名称
+}
+
+// 查询交易订单列表
+export const getOrderPage = async (params) => {
+  return await request.get({ url: `/trade/order/page`, params })
+}
+
+// 查询交易订单详情
+export const getOrder = async (id: number | null) => {
+  return await request.get({ url: `/trade/order/get-detail?id=` + id })
+}
+
+export interface DeliveryVO {
+  id: number // 订单编号
+  logisticsId: number | null // 物流公司编号
+  logisticsNo: string // 物流编号
+}
+
+// 订单发货
+export const delivery = async (data: DeliveryVO) => {
+  return await request.put({ url: `/trade/order/delivery`, data })
+}
+
+// 订单备注
+export const updateRemark = async (data: any) => {
+  return await request.put({ url: `/trade/order/update-remark`, data })
+}
+
+// 订单调价
+export const updatePrice = async (data: any) => {
+  return await request.put({ url: `/trade/order/update-price`, data })
+}
+
+// 修改订单地址
+export const updateAddress = async (data: any) => {
+  return await request.put({ url: `/trade/order/update-address`, data })
+}

+ 0 - 228
src/api/mall/trade/order/type/orderType.ts

@@ -1,228 +0,0 @@
-// TODO @xiaobai:这个放到 order/index.ts  里哈
-// TODO @xiaobai:注释放到变量后面,这样简洁一点
-// TODO @xiaobai:这个改成 TradeOrderRespVO
-export interface TradeOrderPageItemRespVO {
-  // 订单编号
-  id?: number
-  // 订单流水号
-  no?: string
-  // 下单时间
-  createTime?: Date
-  // 订单类型
-  type?: number
-  // 订单来源
-  terminal?: number
-  // 用户编号
-  userId?: number
-  // 用户 IP
-  userIp?: string
-  // 用户备注
-  userRemark?: string
-  // 订单状态
-  status?: number
-  // 购买的商品数量
-  productCount?: number
-  // 订单完成时间
-  finishTime?: Date
-  // 订单取消时间
-  cancelTime?: Date
-  // 取消类型
-  cancelType?: number
-  // 商家备注
-  remark?: string
-  // 支付订单编号
-  payOrderId: number
-  // 是否已支付
-  payed?: boolean
-  // 付款时间
-  payTime?: Date
-  // 支付渠道
-  payChannelCode?: string
-  // 商品原价(总)
-  originalPrice?: number
-  // 订单原价(总)
-  orderPrice?: number
-  // 订单优惠(总)
-  discountPrice?: number
-  // 运费金额
-  deliveryPrice?: number
-  // 订单调价(总)
-  adjustPrice?: number
-  // 应付金额(总)
-  payPrice?: number
-  // 配送模板编号
-  deliveryTemplateId?: number
-  // 发货物流公司编号
-  logisticsId?: number
-  // 发货物流单号
-  logisticsNo?: string
-  // 发货状态
-  deliveryStatus?: number
-  // 发货时间
-  deliveryTime?: Date
-  // 收货时间
-  receiveTime?: Date
-  // 收件人名称
-  receiverName?: string
-  // 收件人手机
-  receiverMobile?: string
-  // 收件人地区编号
-  receiverAreaId?: number
-  // 收件人邮编
-  receiverPostCode?: number
-  // 收件人详细地址
-  receiverDetailAddress?: string
-  // 售后状态
-  afterSaleStatus?: number
-  // 退款金额
-  refundPrice?: number
-  // 优惠劵编号
-  couponId?: number
-  // 优惠劵减免金额
-  couponPrice?: number
-  // 积分抵扣的金额
-  pointPrice?: number
-  //收件人地区名字
-  receiverAreaName?: string
-  // 订单项列表
-  items?: TradeOrderItemBaseVO[]
-  //用户信息
-  user?: MemberUserRespDTO
-}
-
-// TODO @xiaobai:这个改成 TradeOrderItemRespVO
-/**
- * 交易订单项 Base VO,提供给添加、修改、详细的子 VO 使用
- * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
- */
-export interface TradeOrderItemBaseVO {
-  // ========== 订单项基本信息 ==========
-  /**
-   * 编号
-   */
-  id?: number
-  /**
-   * 用户编号
-   */
-  userId?: number
-  /**
-   * 订单编号
-   */
-  orderId?: number
-  // ========== 商品基本信息 ==========
-  /**
-   * 商品 SPU 编号
-   */
-  spuId?: number
-  /**
-   * 商品 SPU 名称
-   */
-  spuName?: string
-  /**
-   * 商品 SKU 编号
-   */
-  skuId?: number
-  /**
-   * 商品图片
-   */
-  picUrl?: string
-  /**
-   * 购买数量
-   */
-  count?: number
-  // ========== 价格 + 支付基本信息 ==========
-  /**
-   * 商品原价(总)
-   */
-  originalPrice?: number
-  /**
-   * 商品原价(单)
-   */
-  originalUnitPrice?: number
-  /**
-   * 商品优惠(总)
-   */
-  discountPrice?: number
-  /**
-   * 商品实付金额(总)
-   */
-  payPrice?: number
-  /**
-   * 子订单分摊金额(总)
-   */
-  orderPartPrice?: number
-  /**
-   * 分摊后子订单实付金额(总)
-   */
-  orderDividePrice?: number
-  // ========== 营销基本信息 ==========
-  // TODO 芋艿:在捉摸一下
-  // ========== 售后基本信息 ==========
-  /**
-   * 售后状态
-   */
-  afterSaleStatus?: number
-  //属性数组
-  properties?: ProductPropertyValueDetailRespVO[]
-}
-
-/**
- * 管理后台 - 商品属性值的明细 Response VO
- */
-export interface ProductPropertyValueDetailRespVO {
-  /**
-   * 属性的编号
-   */
-  propertyId?: number
-  /**
-   * 属性的名称
-   */
-  propertyName?: string
-  /**
-   * 属性值的编号
-   */
-  valueId?: number
-  /**
-   * 属性值的名称
-   */
-  valueName?: string
-}
-
-/**
- * 订单详情查询 请求
- */
-export interface TradeOrderPageReqVO {
-  pageNo: number
-  pageSize: number
-  no?: string
-  userId?: string
-  userNickname?: string
-  userMobile?: string
-  receiverName?: string
-  receiverMobile?: string
-  terminal?: string
-  type?: number
-  status?: number
-  payChannelCode?: string
-  createTime?: [Date, Date]
-  spuName?: string
-  itemCount?: string
-  all?: string
-}
-
-//用户信息
-export interface MemberUserRespDTO {
-  id?: number
-  nickname?: string
-  status?: number
-  avatar?: string
-  mobile?: string
-}
-//订单详情选中type
-export interface SelectType {
-  queryParams: TradeOrderPageReqVO
-  selectTotal: number //选中的数量
-  selectAllFlag: boolean //全选标识
-  selectData: Map<number, Set<string>> //存放涉及选中得页面以及每页选中得数据订单号  全选时根据条件查询 排除取消的list订单
-  unSelectList: Set<string> //登记取消的list 全选标识为true 时登记单独取消的list,再次选中时排除, 全选标识为false 时清空list
-}

+ 15 - 0
src/api/member/address/index.ts

@@ -0,0 +1,15 @@
+import request from '@/config/axios'
+
+export interface AddressVO {
+  id: number
+  name: string
+  mobile: string
+  areaId: number
+  detailAddress: string
+  defaultStatus: boolean
+}
+
+// 查询用户收件地址列表
+export const getAddressList = async (params) => {
+  return await request.get({ url: `/member/address/list`, params })
+}

+ 38 - 0
src/api/member/group/index.ts

@@ -0,0 +1,38 @@
+import request from '@/config/axios'
+
+export interface GroupVO {
+  id: number
+  name: string
+  remark: string
+  status: number
+}
+
+// 查询用户分组列表
+export const getGroupPage = async (params: any) => {
+  return await request.get({ url: `/member/group/page`, params })
+}
+
+// 查询用户分组详情
+export const getGroup = async (id: number) => {
+  return await request.get({ url: `/member/group/get?id=` + id })
+}
+
+// 新增用户分组
+export const createGroup = async (data: GroupVO) => {
+  return await request.post({ url: `/member/group/create`, data })
+}
+
+// 查询用户分组 - 精简信息列表
+export const getSimpleGroupList = async () => {
+  return await request.get({ url: `/member/group/list-all-simple` })
+}
+
+// 修改用户分组
+export const updateGroup = async (data: GroupVO) => {
+  return await request.put({ url: `/member/group/update`, data })
+}
+
+// 删除用户分组
+export const deleteGroup = async (id: number) => {
+  return await request.delete({ url: `/member/group/delete?id=` + id })
+}

+ 42 - 0
src/api/member/level/index.ts

@@ -0,0 +1,42 @@
+import request from '@/config/axios'
+
+export interface LevelVO {
+  id: number
+  name: string
+  experience: number
+  value: number
+  discountPercent: number
+  icon: string
+  bgUrl: string
+  status: number
+}
+
+// 查询会员等级列表
+export const getLevelList = async (params) => {
+  return await request.get({ url: `/member/level/list`, params })
+}
+
+// 查询会员等级详情
+export const getLevel = async (id: number) => {
+  return await request.get({ url: `/member/level/get?id=` + id })
+}
+
+// 查询会员等级 - 精简信息列表
+export const getSimpleLevelList = async () => {
+  return await request.get({ url: `/member/level/list-all-simple` })
+}
+
+// 新增会员等级
+export const createLevel = async (data: LevelVO) => {
+  return await request.post({ url: `/member/level/create`, data })
+}
+
+// 修改会员等级
+export const updateLevel = async (data: LevelVO) => {
+  return await request.put({ url: `/member/level/update`, data })
+}
+
+// 删除会员等级
+export const deleteLevel = async (id: number) => {
+  return await request.delete({ url: `/member/level/delete?id=` + id })
+}

+ 0 - 11
src/api/member/point/record/index.ts

@@ -8,20 +8,9 @@ export interface RecordVO {
   description: string
   point: number
   totalPoint: number
-  status: number
   userId: number
-  freezingTime: Date
-  thawingTime: Date
   createDate: Date
 }
-export interface RecordQueryVO {
-  bizType: string | null | undefined
-  title: string | null | undefined
-  pageNo: number
-  pageSize: number
-  userId: number | null | undefined
-  createDate: string[]
-}
 
 // 查询用户积分记录列表
 export const getRecordPage = async (params) => {

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

@@ -9,25 +9,25 @@ export interface SignInConfigVO {
 
 // 查询积分签到规则列表
 export const getSignInConfigList = async () => {
-  return await request.get({ url: `/member/point/sign-in-config/list` })
+  return await request.get({ url: `/member/sign-in/config/list` })
 }
 
 // 查询积分签到规则详情
 export const getSignInConfig = async (id: number) => {
-  return await request.get({ url: `/member/point/sign-in-config/get?id=` + id })
+  return await request.get({ url: `/member/sign-in/config/get?id=` + id })
 }
 
 // 新增积分签到规则
 export const createSignInConfig = async (data: SignInConfigVO) => {
-  return await request.post({ url: `/member/point/sign-in-config/create`, data })
+  return await request.post({ url: `/member/sign-in/config/create`, data })
 }
 
 // 修改积分签到规则
 export const updateSignInConfig = async (data: SignInConfigVO) => {
-  return await request.put({ url: `/member/point/sign-in-config/update`, data })
+  return await request.put({ url: `/member/sign-in/config/update`, data })
 }
 
 // 删除积分签到规则
 export const deleteSignInConfig = async (id: number) => {
-  return await request.delete({ url: `/member/point/sign-in-config/delete?id=` + id })
+  return await request.delete({ url: `/member/sign-in/config/delete?id=` + id })
 }

+ 1 - 10
src/api/member/signin/record/index.ts

@@ -6,17 +6,8 @@ export interface SignInRecordVO {
   day: number
   point: number
 }
-export interface SignInRecordQueryVO {
-  pageNo: number
-  pageSize: number
-  userId?: number
-  nickname: number | undefined | null
-  day?: number | null | undefined
-  point?: number | null | undefined
-  createTime: string[] | null | undefined
-}
 
 // 查询用户签到积分列表
 export const getSignInRecordPage = async (params) => {
-  return await request.get({ url: `/member/point/sign-in-record/page`, params })
+  return await request.get({ url: `/member/sign-in/record/page`, params })
 }

+ 6 - 1
src/api/member/tag/index.ts

@@ -6,7 +6,7 @@ export interface TagVO {
 }
 
 // 查询会员标签列表
-export const getMemberTagPage = async (params) => {
+export const getMemberTagPage = async (params: any) => {
   return await request.get({ url: `/member/tag/page`, params })
 }
 
@@ -15,6 +15,11 @@ export const getMemberTag = async (id: number) => {
   return await request.get({ url: `/member/tag/get?id=` + id })
 }
 
+// 查询会员标签 - 精简信息列表
+export const getSimpleTagList = async () => {
+  return await request.get({ url: `/member/tag/list-all-simple` })
+}
+
 // 新增会员标签
 export const createMemberTag = async (data: TagVO) => {
   return await request.post({ url: `/member/tag/create`, data })

+ 20 - 27
src/api/member/user/index.ts

@@ -2,38 +2,26 @@ import request from '@/config/axios'
 
 export interface UserVO {
   id: number
-  mobile: string
-  password: string
-  status: number
-  registerIp: string
+  avatar: string | undefined
+  birthday: number | undefined
+  createTime: number | undefined
+  loginDate: number | undefined
   loginIp: string
-  loginDate: Date
-  nickname: string
-  avatar: string
-  name: string
-  sex: number
-  areaId: number
-  birthday: Date
   mark: string
-  createTime: Date
-}
-export interface UserBaseInfoVO {
-  id: number | undefined | null
   mobile: string
-  password: string | null | undefined
-  status: number
-  registerIp: string | null | undefined
-  loginIp: string | null | undefined
-  loginDate: Date | null | undefined
-  nickname: string | null | undefined
-  avatar: string | null | undefined
-  name: string | null | undefined
+  name: string | undefined
+  nickname: string | undefined
+  registerIp: string
   sex: number
-  areaId: number | null | undefined
-  birthday: Date | null | undefined
-  mark: string | null | undefined
-  createTime: Date | null | undefined
+  status: number
+  areaId: number | undefined
+  areaName: string | undefined
+  levelName: string | null
+  point: number | undefined | null
+  totalPoint: number | undefined | null
+  experience: number | null | undefined
 }
+
 // 查询会员用户列表
 export const getUserPage = async (params) => {
   return await request.get({ url: `/member/user/page`, params })
@@ -48,3 +36,8 @@ export const getUser = async (id: number) => {
 export const updateUser = async (data: UserVO) => {
   return await request.put({ url: `/member/user/update`, data })
 }
+
+// 修改会员用户等级
+export const updateUserLevel = async (data: any) => {
+  return await request.put({ url: `/member/user/update-level`, data })
+}

+ 1 - 0
src/assets/svgs/member_balance.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1693028338187" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="22985" width="128" height="128" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M983.8 312.7C958 251.7 921 197 874 150c-47-47-101.8-83.9-162.7-109.7C648.2 13.5 581.1 0 512 0S375.8 13.5 312.7 40.2C251.7 66 197 102.9 150 150c-47 47-83.9 101.8-109.7 162.7C13.5 375.8 0 442.9 0 512s13.5 136.2 40.2 199.3C66 772.3 102.9 827 150 874c47 47 101.8 83.9 162.7 109.7 63.1 26.7 130.2 40.2 199.3 40.2s136.2-13.5 199.3-40.2C772.3 958 827 921 874 874c47-47 83.9-101.8 109.7-162.7 26.7-63.1 40.2-130.2 40.2-199.3s-13.4-136.2-40.1-199.3z m-55.3 375.2c-22.8 53.8-55.4 102.2-96.9 143.7s-89.9 74.1-143.7 96.9C632.2 952.1 573 964 512 964s-120.2-11.9-175.9-35.5c-53.8-22.8-102.2-55.4-143.7-96.9s-74.1-89.9-96.9-143.7C71.9 632.2 60 573 60 512s11.9-120.2 35.5-175.9c22.8-53.8 55.4-102.2 96.9-143.7s89.9-74.1 143.7-96.9C391.8 71.9 451 60 512 60s120.2 11.9 175.9 35.5c53.8 22.8 102.2 55.4 143.7 96.9s74.1 89.9 96.9 143.7C952.1 391.8 964 451 964 512s-11.9 120.2-35.5 175.9z" fill="#000000" p-id="22986"></path><path d="M706 469.1H574.7l84.2-180.6c7-15 0.4-32.9-14.5-39.9-15-7-32.9-0.4-39.9 14.5L512 461.5l-92.5-198.3c-7-15-24.9-21.5-39.9-14.5s-21.5 24.9-14.5 39.9l84.2 180.6H318c-16.5 0-30 13.5-30 30s13.5 30 30 30h164v64h-92.5c-20.6 0-37.5 13.5-37.5 30s16.9 30 37.5 30H482v95c0 16.5 13.5 30 30 30s30-13.5 30-30v-95h92.5c20.6 0 37.5-13.5 37.5-30s-16.9-30-37.5-30H542v-64h164c16.5 0 30-13.5 30-30 0-16.6-13.5-30.1-30-30.1z" fill="#000000" p-id="22987"></path></svg>

文件差異過大導致無法顯示
+ 0 - 0
src/assets/svgs/member_expenditure_balance.svg


+ 1 - 0
src/assets/svgs/member_level.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1693027700643" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8876" width="128" height="128" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M936.96 385.877333l-203.434667-204.8-18.090667-7.68L308.565333 173.397333l-18.090667 7.68L87.04 385.877333c-9.728 9.898667-9.898667 25.941333-0.170667 35.84l406.869333 421.034667c4.778667 4.949333 11.434667 7.850667 18.432 7.850667 6.997333 0 13.653333-2.901333 18.432-7.850667l406.869333-421.034667C946.858667 411.648 946.688 395.776 936.96 385.877333zM868.522667 389.632l-141.994667 0-163.84-165.034667 141.994667 0L868.522667 389.632zM319.317333 224.768l143.018667 0-163.84 165.034667L155.477333 389.802667 319.317333 224.768zM176.469333 440.832l132.608 0 18.090667-7.509333 185.173333-186.538667 185.173333 186.538667 18.090667 7.509333 131.584 0L512 787.968 176.469333 440.832z" p-id="8877" fill="#000000"></path></svg>

文件差異過大導致無法顯示
+ 0 - 0
src/assets/svgs/member_point.svg


文件差異過大導致無法顯示
+ 0 - 0
src/assets/svgs/member_recharge_balance.svg


+ 3 - 0
src/components/Card/index.ts

@@ -0,0 +1,3 @@
+import CardTitle from './src/CardTitle.vue'
+
+export { CardTitle }

+ 1 - 1
src/views/member/user/components/card-title.vue → src/components/Card/src/CardTitle.vue

@@ -1,8 +1,8 @@
 <script lang="ts" setup>
-import { defineComponent } from 'vue'
 defineComponent({
   name: 'CardTitle'
 })
+
 const { title } = defineProps({
   title: {
     type: String,

+ 2 - 1
src/components/Descriptions/index.ts

@@ -1,3 +1,4 @@
 import Descriptions from './src/Descriptions.vue'
+import DescriptionsItemLabel from './src/DescriptionsItemLabel.vue'
 
-export { Descriptions }
+export { Descriptions, DescriptionsItemLabel }

+ 28 - 0
src/components/Descriptions/src/DescriptionsItemLabel.vue

@@ -0,0 +1,28 @@
+<script setup lang="ts">
+const { label } = defineProps({
+  label: {
+    type: String,
+    required: true
+  },
+  icon: {
+    type: String,
+    required: false
+  }
+})
+</script>
+
+<template>
+  <div class="cell-item">
+    <Icon :icon="icon" v-if="icon" style="vertical-align: middle" :size="18" />
+    {{ label }}
+  </div>
+</template>
+
+<style scoped lang="scss">
+.cell-item {
+  display: inline;
+}
+.cell-item::after {
+  content: ':';
+}
+</style>

+ 63 - 32
src/router/modules/remaining.ts

@@ -5,32 +5,32 @@ const { t } = useI18n()
  * redirect: noredirect        当设置 noredirect 的时候该路由在面包屑导航中不可被点击
  * name:'router-name'          设定路由的名字,一定要填写不然使用<keep-alive>时会出现各种问题
  * meta : {
-    hidden: true              当设置 true 的时候该路由不会再侧边栏出现 如404,login等页面(默认 false)
+ hidden: true              当设置 true 的时候该路由不会再侧边栏出现 如404,login等页面(默认 false)
 
-    alwaysShow: true          当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式,
-                              只有一个时,会将那个子路由当做根路由显示在侧边栏,
-                              若你想不管路由下面的 children 声明的个数都显示你的根路由,
-                              你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则,
-                              一直显示根路由(默认 false)
+ alwaysShow: true          当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式,
+ 只有一个时,会将那个子路由当做根路由显示在侧边栏,
+ 若你想不管路由下面的 children 声明的个数都显示你的根路由,
+ 你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则,
+ 一直显示根路由(默认 false)
 
-    title: 'title'            设置该路由在侧边栏和面包屑中展示的名字
+ title: 'title'            设置该路由在侧边栏和面包屑中展示的名字
 
-    icon: 'svg-name'          设置该路由的图标
+ icon: 'svg-name'          设置该路由的图标
 
-    noCache: true             如果设置为true,则不会被 <keep-alive> 缓存(默认 false)
+ noCache: true             如果设置为true,则不会被 <keep-alive> 缓存(默认 false)
 
-    breadcrumb: false         如果设置为false,则不会在breadcrumb面包屑中显示(默认 true)
+ breadcrumb: false         如果设置为false,则不会在breadcrumb面包屑中显示(默认 true)
 
-    affix: true               如果设置为true,则会一直固定在tag项中(默认 false)
+ affix: true               如果设置为true,则会一直固定在tag项中(默认 false)
 
-    noTagsView: true          如果设置为true,则不会出现在tag中(默认 false)
+ noTagsView: true          如果设置为true,则不会出现在tag中(默认 false)
 
-    activeMenu: '/dashboard'  显示高亮的路由路径
+ activeMenu: '/dashboard'  显示高亮的路由路径
 
-    followAuth: '/dashboard'  跟随哪个路由进行权限过滤
+ followAuth: '/dashboard'  跟随哪个路由进行权限过滤
 
-    canTo: true               设置为true即使hidden为true,也依然可以进行路由跳转(默认 false)
-  }
+ canTo: true               设置为true即使hidden为true,也依然可以进行路由跳转(默认 false)
+ }
  **/
 const remainingRouter: AppRouteRecordRaw[] = [
   {
@@ -346,22 +346,6 @@ const remainingRouter: AppRouteRecordRaw[] = [
       }
     ]
   },
-  {
-    path: '/property',
-    component: Layout,
-    name: 'property',
-    meta: {
-      hidden: true
-    },
-    children: [
-      {
-        path: 'value/:propertyId(\\d+)',
-        component: () => import('@/views/mall/product/property/value/index.vue'),
-        name: 'ProductPropertyValue',
-        meta: { title: '商品属性值', icon: '', activeMenu: '/product/property' }
-      }
-    ]
-  },
   {
     path: '/product',
     component: Layout,
@@ -408,6 +392,53 @@ const remainingRouter: AppRouteRecordRaw[] = [
           title: '商品详情',
           activeMenu: '/product/product-spu'
         }
+      },
+      {
+        path: 'property/value/:propertyId(\\d+)',
+        component: () => import('@/views/mall/product/property/value/index.vue'),
+        name: 'ProductPropertyValue',
+        meta: {
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          icon: 'ep:view',
+          title: '商品属性值',
+          activeMenu: '/product/property'
+        }
+      }
+    ]
+  },
+  {
+    path: '/trade/order',
+    component: Layout,
+    name: 'Detail',
+    meta: {
+      hidden: true
+    },
+    children: [
+      {
+        path: 'detail/:orderId(\\d+)',
+        component: () => import('@/views/mall/trade/order/detail/index.vue'),
+        name: 'TradeOrderDetailForm',
+        meta: { title: '订单详情', icon: '', activeMenu: '/trade/trade/order' }
+      }
+    ]
+  },
+  {
+    path: '/member',
+    component: Layout,
+    name: 'member',
+    meta: { hidden: true },
+    children: [
+      {
+        path: 'user/detail/:id',
+        name: 'MemberUserDetail',
+        meta: {
+          title: '会员详情',
+          noCache: true,
+          hidden: true
+        },
+        component: () => import('@/views/member/user/detail/index.vue')
       }
     ]
   },

+ 3 - 2
src/utils/dict.ts

@@ -90,6 +90,7 @@ export enum DICT_TYPE {
   USER_TYPE = 'user_type',
   COMMON_STATUS = 'common_status',
   SYSTEM_TENANT_PACKAGE_ID = 'system_tenant_package_id',
+  TERMINAL = 'terminal', // 终端
 
   // ========== SYSTEM 模块 ==========
   SYSTEM_USER_SEX = 'system_user_sex',
@@ -130,7 +131,7 @@ export enum DICT_TYPE {
   BPM_OA_LEAVE_TYPE = 'bpm_oa_leave_type',
 
   // ========== PAY 模块 ==========
-  PAY_CHANNEL_CODE = 'pay_channel_code', // 支付渠道编码类型
+  PAY_CHANNEL_CODE = 'pay_channel_code_type', // 支付渠道编码类型
   PAY_ORDER_STATUS = 'pay_order_status', // 商户支付订单状态
   PAY_REFUND_STATUS = 'pay_refund_status', // 退款订单状态
   PAY_NOTIFY_STATUS = 'pay_notify_status', // 商户支付回调状态
@@ -155,7 +156,7 @@ export enum DICT_TYPE {
   TRADE_ORDER_TYPE = 'trade_order_type', // 订单 - 类型
   TRADE_ORDER_STATUS = 'trade_order_status', // 订单 - 状态
   TRADE_ORDER_ITEM_AFTER_SALE_STATUS = 'trade_order_item_after_sale_status', // 订单项 - 售后状态
-  TERMINAL = 'terminal', // 终端
+  TRADE_DELIVERY_TYPE = 'trade_delivery_type', // 配送方式
 
   // ========== MALL - 营销模块 ==========
   PROMOTION_DISCOUNT_TYPE = 'promotion_discount_type', // 优惠类型

+ 1 - 1
src/utils/formatTime.ts

@@ -11,7 +11,7 @@ import dayjs from 'dayjs'
  * @description format 季度 + 星期 + 几周:"YYYY-mm-dd HH:MM:SS WWW QQQQ ZZZ"
  * @returns 返回拼接后的时间字符串
  */
-export function formatDate(date: Date, format?: string): string {
+export function formatDate(date: Date | number, format?: string): string {
   // 日期不存在,则返回空
   if (!date) {
     return ''

+ 25 - 1
src/utils/index.ts

@@ -174,7 +174,6 @@ export const copyValueToTarget = (target, source) => {
   Object.assign(target, newObj)
 }
 
-// TODO @puhui999:返回要带上 .00 哈.例如说 1.00
 /**
  * 将一个整数转换为分数保留两位小数
  * @param num
@@ -185,6 +184,31 @@ export const formatToFraction = (num: number | string | undefined): number => {
   return parseFloat((parsedNumber / 100).toFixed(2))
 }
 
+/**
+ * 将一个数转换为 1.00 这样
+ * 数据呈现的时候使用
+ *
+ * @param num 整数
+ */
+export const floatToFixed2 = (num: number | string | undefined): string => {
+  let str = '0.00'
+  if (typeof num === 'undefined') {
+    return str
+  }
+  const f = formatToFraction(num)
+  const decimalPart = f.toString().split('.')[1]
+  const len = decimalPart ? decimalPart.length : 0
+  switch (len) {
+    case 0:
+      str = f.toString() + '.00'
+      break
+    case 1:
+      str = f.toString() + '0'
+      break
+  }
+  return str
+}
+
 /**
  * 将一个分数转换为整数
  * @param num

+ 1 - 1
src/views/Login/components/LoginForm.vue

@@ -21,8 +21,8 @@
             v-model="loginData.loginForm.tenantName"
             :placeholder="t('login.tenantNamePlaceholder')"
             :prefix-icon="iconHouse"
-            type="primary"
             link
+            type="primary"
           />
         </el-form-item>
       </el-col>

+ 2 - 2
src/views/infra/codegen/PreviewCode.vue

@@ -45,9 +45,9 @@
             <el-button class="float-right" text type="primary" @click="copy(item.code)">
               {{ t('common.copy') }}
             </el-button>
-            <div>
+            <el-scrollbar height="600px">
               <pre><code v-dompurify-html="highlightedCode(item)" class="hljs"></code></pre>
-            </div>
+            </el-scrollbar>
           </el-tab-pane>
         </el-tabs>
       </el-card>

+ 192 - 0
src/views/mall/product/comment/CommentForm.vue

@@ -0,0 +1,192 @@
+<template>
+  <Dialog title="添加虚拟评论" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="商品" prop="spuId">
+        <div @click="handleSelectSpu" class="w-60px h-60px">
+          <div v-if="spuData && spuData.picUrl">
+            <el-image :src="spuData.picUrl" />
+          </div>
+          <div v-else class="select-box">
+            <Icon icon="ep:plus" />
+          </div>
+        </div>
+      </el-form-item>
+      <el-form-item label="商品规格" prop="skuId" v-if="formData.spuId">
+        <div @click="handleSelectSku" class="w-60px h-60px">
+          <div v-if="skuData && skuData.picUrl">
+            <el-image :src="skuData.picUrl" />
+          </div>
+          <div v-else class="select-box">
+            <Icon icon="ep:plus" />
+          </div>
+        </div>
+      </el-form-item>
+      <el-form-item label="用户头像" prop="userAvatar">
+        <UploadImg v-model="formData.userAvatar" height="60px" width="60px" />
+      </el-form-item>
+      <el-form-item label="用户名称" prop="userNickname">
+        <el-input v-model="formData.userNickname" placeholder="请输入用户名称" />
+      </el-form-item>
+      <el-form-item label="评论内容" prop="content">
+        <el-input type="textarea" v-model="formData.content" />
+      </el-form-item>
+      <el-form-item label="描述星级" prop="descriptionScores">
+        <el-rate v-model="formData.descriptionScores" />
+      </el-form-item>
+      <el-form-item label="服务星级" prop="benefitScores">
+        <el-rate v-model="formData.benefitScores" />
+      </el-form-item>
+      <el-form-item label="评论图片" prop="picUrls">
+        <UploadImgs v-model="formData.picUrls" :limit="9" height="60px" width="60px" />
+      </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>
+  <SpuTableSelect ref="spuTableSelectRef" @change="handleSpuChange" />
+  <SkuTableSelect ref="skuTableSelectRef" @change="handleSkuChange" :spu-id="spuData.id" />
+</template>
+<script setup lang="ts">
+import * as CommentApi from '@/api/mall/product/comment'
+import SpuTableSelect from '@/views/mall/product/spu/components/SpuTableSelect.vue'
+import * as ProductSpuApi from '@/api/mall/product/spu'
+import SkuTableSelect from '@/views/mall/product/spu/components/SkuTableSelect.vue'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  userId: undefined,
+  userNickname: undefined,
+  userAvatar: undefined,
+  spuId: undefined,
+  spuName: undefined,
+  skuId: undefined,
+  descriptionScores: 5,
+  benefitScores: 5,
+  content: undefined,
+  picUrls: []
+})
+const formRules = reactive({
+  spuId: [{ required: true, message: '商品不能为空', trigger: 'blur' }],
+  skuId: [{ required: true, message: '规格不能为空', trigger: 'blur' }],
+  userAvatar: [{ required: true, message: '用户头像不能为空', trigger: 'blur' }],
+  userNickname: [{ required: true, message: '用户名称不能为空', trigger: 'blur' }],
+  content: [{ required: true, message: '评论内容不能为空', trigger: 'blur' }],
+  descriptionScores: [{ required: true, message: '描述星级不能为空', trigger: 'blur' }],
+  benefitScores: [{ required: true, message: '服务星级不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+const spuData = ref<ProductSpuApi.Spu>({})
+const skuData = ref({
+  id: -1,
+  name: '',
+  picUrl: ''
+})
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await CommentApi.getComment(id)
+    } 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
+  // 提交请求
+  formLoading.value = true
+  try {
+    //处理评论图片
+    const picUrls = formData.value.picUrls.map((item) => {
+      return item?.url ? item.url : item
+    })
+    const data = { ...formData.value, picUrls }
+    if (formType.value === 'create') {
+      await CommentApi.createComment(data)
+      message.success(t('common.createSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    userId: undefined,
+    userNickname: undefined,
+    userAvatar: undefined,
+    spuId: undefined,
+    skuId: undefined,
+    descriptionScores: 5,
+    benefitScores: 5,
+    content: undefined,
+    picUrls: []
+  }
+  formRef.value?.resetFields()
+}
+
+/** SPU 表格选择 */
+const spuTableSelectRef = ref()
+const handleSelectSpu = () => {
+  spuTableSelectRef.value.open()
+}
+const handleSpuChange = (spu: ProductSpuApi.Spu) => {
+  spuData.value = spu
+  formData.value.spuId = spu.id
+}
+
+/** SKU 表格选择 */
+const skuTableSelectRef = ref()
+const handleSelectSku = () => {
+  skuTableSelectRef.value.open()
+}
+const handleSkuChange = (sku: ProductSpuApi.Sku) => {
+  skuData.value = sku
+  formData.value.skuId = sku.id
+}
+</script>
+<style>
+.select-box {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border: 1px dashed var(--el-border-color-darker);
+  border-radius: 8px;
+  width: 100%;
+  height: 100%;
+}
+</style>

+ 76 - 0
src/views/mall/product/comment/ReplyForm.vue

@@ -0,0 +1,76 @@
+<template>
+  <Dialog title="回复" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="回复内容" prop="replyContent">
+        <el-input type="textarea" v-model="formData.replyContent" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitReplyForm" type="primary" :disabled="formLoading">确 定 </el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script setup lang="ts">
+import * as CommentApi from '@/api/mall/product/comment'
+import { ElInput } from 'element-plus'
+
+defineOptions({ name: 'ProductComment' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formData = ref({
+  id: undefined,
+  replyContent: undefined
+})
+const formRules = reactive({
+  replyContent: [{ required: true, message: '回复内容不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (id?: number) => {
+  resetForm()
+  formData.value.id = id
+  dialogVisible.value = true
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitReplyForm = async () => {
+  // 校验表单
+  const valid = await formRef?.value?.validate()
+  if (!valid) return
+  // 提交请求
+  formLoading.value = true
+  try {
+    await CommentApi.replyComment(formData.value)
+    message.success(t('common.createSuccess'))
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    replyContent: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 243 - 0
src/views/mall/product/comment/index.vue

@@ -0,0 +1,243 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="回复状态" prop="replyStatus">
+        <el-select v-model="queryParams.replyStatus">
+          <el-option label="已回复" :value="true" />
+          <el-option label="未回复" :value="false" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="商品名称" prop="spuName">
+        <el-input v-model="queryParams.spuName" placeholder="请输入商品名称" />
+      </el-form-item>
+      <el-form-item label="用户名称" prop="userNickname">
+        <el-input v-model="queryParams.userNickname" placeholder="请输入用户名称" />
+      </el-form-item>
+      <el-form-item label="订单编号" prop="orderId">
+        <el-input v-model="queryParams.orderId" placeholder="请输入订单编号" />
+      </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="['product:comment:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" />
+          添加虚拟评论
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <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">
+        <template #default="scope">
+          <div class="flex row items-center gap-x-4px">
+            <el-image
+              v-if="scope.row.skuPicUrl"
+              :src="scope.row.skuPicUrl"
+              :preview-src-list="[scope.row.skuPicUrl]"
+              class="w-40px h-40px shrink-0"
+              preview-teleported
+            />
+            <div>{{ scope.row.spuName }}</div>
+            <el-tag
+              v-for="property in scope.row.skuProperties"
+              :key="property.propertyId"
+              class="mr-10px"
+            >
+              {{ property.propertyName }}: {{ property.valueName }}
+            </el-tag>
+          </div>
+        </template>
+      </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">
+        <template #default="scope">
+          <p>{{ scope.row.content }}</p>
+          <div class="flex justify-center gap-x-4px">
+            <el-image
+              v-for="(picUrl, index) in scope.row.picUrls"
+              :key="index"
+              :src="picUrl"
+              :preview-src-list="scope.row.picUrls"
+              :initial-index="index"
+              class="w-40px h-40px"
+              preview-teleported
+            />
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="回复内容"
+        align="center"
+        prop="replyContent"
+        min-width="100"
+        show-overflow-tooltip
+      />
+      <el-table-column
+        label="评论时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="170"
+      />
+      <el-table-column label="状态" align="center" width="65px">
+        <template #default="scope">
+          <el-switch
+            v-model="scope.row.visible"
+            :active-value="true"
+            :inactive-value="false"
+            v-hasPermi="['product:comment:update']"
+            @change="handleVisibleChange(scope.row)"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" min-width="60px" fixed="right">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="handleReply(scope.row.id)"
+            v-hasPermi="['product:comment:update']"
+          >
+            回复
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <CommentForm ref="formRef" @success="getList" />
+  <!-- 回复表单弹窗 -->
+  <ReplyForm ref="replyFormRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { dateFormatter } from '@/utils/formatTime'
+import * as CommentApi from '@/api/mall/product/comment'
+import CommentForm from './CommentForm.vue'
+import ReplyForm from './ReplyForm.vue'
+
+defineOptions({ name: 'ProductComment' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  replyStatus: null,
+  spuName: null,
+  userNickname: null,
+  orderId: null,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await CommentApi.getCommentPage(queryParams)
+    // visible 如果为 null,会导致刷新的时候触发 e-switch 的 change 事件
+    data.list.forEach((item) => {
+      if (!item.visible) {
+        item.visible = false
+      }
+    })
+    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 openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 回复按钮操作 **/
+const replyFormRef = ref()
+const handleReply = (id: number) => {
+  replyFormRef.value.open(id)
+}
+
+/** 显示/隐藏 **/
+const handleVisibleChange = async (row: CommentApi.CommentVO) => {
+  if (loading.value) {
+    return
+  }
+  let changedValue = row.visible
+  try {
+    await message.confirm(changedValue ? '是否显示评论?' : '是否隐藏评论?')
+    await CommentApi.updateCommentVisible({ id: row.id, visible: changedValue })
+    await getList()
+  } catch {
+    row.visible = !changedValue
+  }
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

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

@@ -73,9 +73,7 @@
           >
             编辑
           </el-button>
-          <el-button link type="primary">
-            <router-link :to="'/property/value/' + scope.row.id">属性值</router-link>
-          </el-button>
+          <el-button link type="primary" @click="goValueList(scope.row.id)">属性值</el-button>
           <el-button
             v-hasPermi="['product:property:delete']"
             link
@@ -104,6 +102,8 @@ import { dateFormatter } from '@/utils/formatTime'
 import * as PropertyApi from '@/api/mall/product/property'
 import PropertyForm from './PropertyForm.vue'
 
+const { push } = useRouter()
+
 defineOptions({ name: 'ProductProperty' })
 
 const message = useMessage() // 消息弹窗
@@ -163,6 +163,11 @@ const handleDelete = async (id: number) => {
   } catch {}
 }
 
+/** 跳转商品属性列表 */
+const goValueList = (id: number) => {
+  push({ path: '/product/property/value/' + id })
+}
+
 /** 初始化 **/
 onMounted(() => {
   getList()

+ 22 - 12
src/views/mall/product/spu/addForm.vue

@@ -42,7 +42,9 @@ import { useTagsViewStore } from '@/store/modules/tagsView'
 import { BasicInfoForm, DescriptionForm, OtherSettingsForm } from './components'
 // 业务api
 import * as ProductSpuApi from '@/api/mall/product/spu'
-import { convertToInteger, formatToFraction } from '@/utils'
+import { convertToInteger, floatToFixed2, formatToFraction } from '@/utils'
+
+// TODO @芋艿:后续稍微调整下;
 
 defineOptions({ name: 'ProductSpuForm' })
 
@@ -107,12 +109,20 @@ const getDetail = async () => {
     try {
       const res = (await ProductSpuApi.getSpu(id)) as ProductSpuApi.Spu
       res.skus?.forEach((item) => {
-        // 回显价格分转元
-        item.price = formatToFraction(item.price)
-        item.marketPrice = formatToFraction(item.marketPrice)
-        item.costPrice = formatToFraction(item.costPrice)
-        item.subCommissionFirstPrice = formatToFraction(item.subCommissionFirstPrice)
-        item.subCommissionSecondPrice = formatToFraction(item.subCommissionSecondPrice)
+        if (isDetail.value === true) {
+          item.price = floatToFixed2(item.price)
+          item.marketPrice = floatToFixed2(item.marketPrice)
+          item.costPrice = floatToFixed2(item.costPrice)
+          item.subCommissionFirstPrice = floatToFixed2(item.subCommissionFirstPrice)
+          item.subCommissionSecondPrice = floatToFixed2(item.subCommissionSecondPrice)
+        } else {
+          // 回显价格分转元
+          item.price = formatToFraction(item.price)
+          item.marketPrice = formatToFraction(item.marketPrice)
+          item.costPrice = formatToFraction(item.costPrice)
+          item.subCommissionFirstPrice = formatToFraction(item.subCommissionFirstPrice)
+          item.subCommissionSecondPrice = formatToFraction(item.subCommissionSecondPrice)
+        }
       })
       formData.value = res
     } finally {
@@ -132,19 +142,19 @@ const submitForm = async () => {
     await unref(descriptionRef)?.validate()
     await unref(otherSettingsRef)?.validate()
     // 深拷贝一份, 这样最终 server 端不满足,不需要恢复,
-    const deepCopyFormData = cloneDeep(unref(formData.value))
+    const deepCopyFormData = cloneDeep(unref(formData.value)) as ProductSpuApi.Spu
     // 兜底处理 sku 空数据
     formData.value.skus!.forEach((sku) => {
       // 因为是空数据这里判断一下商品条码是否为空就行
       if (sku.barCode === '') {
-        const index = deepCopyFormData.skus.findIndex(
+        const index = deepCopyFormData.skus!.findIndex(
           (item) => JSON.stringify(item.properties) === JSON.stringify(sku.properties)
         )
         // 删除这条 sku
-        deepCopyFormData.skus.splice(index, 1)
+        deepCopyFormData.skus!.splice(index, 1)
       }
     })
-    deepCopyFormData.skus.forEach((item) => {
+    deepCopyFormData.skus!.forEach((item) => {
       // 给sku name赋值
       item.name = deepCopyFormData.name
       // sku相关价格元转分
@@ -156,7 +166,7 @@ const submitForm = async () => {
     })
     // 处理轮播图列表
     const newSliderPicUrls: any[] = []
-    deepCopyFormData.sliderPicUrls.forEach((item: any) => {
+    deepCopyFormData.sliderPicUrls!.forEach((item: any) => {
       // 如果是前端选的图
       typeof item === 'object' ? newSliderPicUrls.push(item.url) : newSliderPicUrls.push(item)
     })

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

@@ -334,7 +334,7 @@ const validateSku = () => {
   for (const sku of formData.value!.skus!) {
     // 作为活动组件的校验
     if (props.isActivityComponent) {
-      for (const rule of props.ruleConfig) {
+      for (const rule of props?.ruleConfig) {
         const arg = getValue(sku, rule.name)
         if (!rule.rule(arg)) {
           validate = false // 只要有一个不通过则直接不通过
@@ -534,9 +534,10 @@ watch(
   }
 )
 const activitySkuListRef = ref<InstanceType<typeof ElTable>>()
-const clearSelection = () => {
-  activitySkuListRef.value.clearSelection()
+
+const getSkuTableRef = () => {
+  return activitySkuListRef.value
 }
 // 暴露出生成 sku 方法,给添加属性成功时调用
-defineExpose({ generateTableData, validateSku, clearSelection })
+defineExpose({ generateTableData, validateSku, getSkuTableRef })
 </script>

+ 94 - 0
src/views/mall/product/spu/components/SkuTableSelect.vue

@@ -0,0 +1,94 @@
+<template>
+  <Dialog v-model="dialogVisible" :appendToBody="true" title="选择规格" width="700">
+    <el-table v-loading="loading" :data="list" show-overflow-tooltip>
+      <el-table-column label="#" width="55">
+        <template #default="{ row }">
+          <el-radio :label="row.id" v-model="selectedSkuId" @change="handleSelected(row)"
+            >&nbsp;
+          </el-radio>
+        </template>
+      </el-table-column>
+      <el-table-column label="图片" min-width="80">
+        <template #default="{ row }">
+          <el-image
+            :src="row.picUrl"
+            class="w-30px h-30px"
+            :preview-src-list="[row.picUrl]"
+            preview-teleported
+          />
+        </template>
+      </el-table-column>
+      <el-table-column label="规格" align="center" min-width="80">
+        <template #default="{ row }">
+          {{ row.properties?.map((p) => p.valueName)?.join(' ') }}
+        </template>
+      </el-table-column>
+      <el-table-column align="center" label="销售价(元)" min-width="80">
+        <template #default="{ row }">
+          {{ row.price }}
+        </template>
+      </el-table-column>
+    </el-table>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { ElTable } from 'element-plus'
+import * as ProductSpuApi from '@/api/mall/product/spu'
+import { propTypes } from '@/utils/propTypes'
+
+defineOptions({ name: 'SkuTableSelect' })
+
+const props = defineProps({
+  spuId: propTypes.number.def(null)
+})
+
+const message = useMessage() // 消息弹窗
+const list = ref<any[]>([]) // 列表的数据
+const loading = ref(false) // 列表的加载中
+const dialogVisible = ref(false) // 弹窗的是否展示
+
+const selectedSkuId = ref() // 选中的商品 spuId
+
+/** 选中时触发 */
+const handleSelected = (row: ProductSpuApi.Sku) => {
+  emits('change', row)
+  // 关闭弹窗
+  dialogVisible.value = false
+  selectedSkuId.value = undefined
+}
+
+// 确认选择时的触发事件
+const emits = defineEmits<{
+  (e: 'change', spu: ProductSpuApi.Sku): void
+}>()
+
+/** 打开弹窗 */
+const open = () => {
+  dialogVisible.value = true
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 查询列表 */
+const getSpuDetail = async () => {
+  loading.value = true
+  try {
+    const spu = await ProductSpuApi.getSpu(props.spuId)
+    list.value = spu.skus
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 初始化 **/
+onMounted(async () => {})
+watch(
+  () => props.spuId,
+  () => {
+    if (!props.spuId) {
+      return
+    }
+    getSpuDetail()
+  }
+)
+</script>

+ 167 - 0
src/views/mall/product/spu/components/SpuTableSelect.vue

@@ -0,0 +1,167 @@
+<template>
+  <Dialog v-model="dialogVisible" :appendToBody="true" title="选择商品" width="70%">
+    <el-row :gutter="20" class="mb-10px">
+      <el-col :span="6">
+        <el-input
+          v-model="queryParams.name"
+          class="!w-240px"
+          clearable
+          placeholder="请输入商品名称"
+          @keyup.enter="handleQuery"
+        />
+      </el-col>
+      <el-col :span="6">
+        <el-tree-select
+          v-model="queryParams.categoryId"
+          :data="categoryTreeList"
+          :props="defaultProps"
+          check-strictly
+          class="w-1/1"
+          node-key="id"
+          placeholder="请选择商品分类"
+        />
+      </el-col>
+      <el-col :span="6">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+          end-placeholder="结束日期"
+          start-placeholder="开始日期"
+          type="daterange"
+          value-format="YYYY-MM-DD HH:mm:ss"
+        />
+      </el-col>
+      <el-col :span="6">
+        <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-col>
+    </el-row>
+    <el-table ref="spuListRef" v-loading="loading" :data="list" show-overflow-tooltip>
+      <el-table-column label="#" width="55">
+        <template #default="{ row }">
+          <el-radio :label="row.id" v-model="selectedSpuId" @change="handleSelected(row)"
+            >&nbsp;</el-radio
+          >
+        </template>
+      </el-table-column>
+      <el-table-column key="id" align="center" label="商品编号" prop="id" min-width="60" />
+      <el-table-column label="商品图" min-width="80">
+        <template #default="{ row }">
+          <el-image
+            :src="row.picUrl"
+            class="w-30px h-30px"
+            :preview-src-list="[row.picUrl]"
+            preview-teleported
+          />
+        </template>
+      </el-table-column>
+      <el-table-column label="商品名称" min-width="200" prop="name" />
+      <el-table-column label="商品分类" min-width="100" prop="categoryId">
+        <template #default="{ row }">
+          <span>{{ categoryList.find((c) => c.id === row.categoryId)?.name }}</span>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
+      @pagination="getList"
+    />
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { ElTable } from 'element-plus'
+import { defaultProps, handleTree } from '@/utils/tree'
+
+import * as ProductCategoryApi from '@/api/mall/product/category'
+import * as ProductSpuApi from '@/api/mall/product/spu'
+
+defineOptions({ name: 'SpuTableSelect' })
+
+const message = useMessage() // 消息弹窗
+const total = ref(0) // 列表的总页数
+const list = ref<any[]>([]) // 列表的数据
+const loading = ref(false) // 列表的加载中
+const dialogVisible = ref(false) // 弹窗的是否展示
+const queryParams = ref({
+  pageNo: 1,
+  pageSize: 10,
+  tabType: 0, // 默认获取上架的商品
+  name: '',
+  categoryId: null,
+  createTime: []
+}) // 查询参数
+const spuListRef = ref<InstanceType<typeof ElTable>>()
+
+const selectedSpuId = ref() // 选中的商品 spuId
+
+/** 选中时触发 */
+const handleSelected = (row: ProductSpuApi.Spu) => {
+  emits('change', row)
+  // 关闭弹窗
+  dialogVisible.value = false
+  selectedSpuId.value = undefined
+}
+
+// 确认选择时的触发事件
+const emits = defineEmits<{
+  (e: 'change', spu: ProductSpuApi.Spu): void
+}>()
+
+/** 打开弹窗 */
+const open = () => {
+  dialogVisible.value = true
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await ProductSpuApi.getSpuPage(queryParams.value)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryParams.value = {
+    pageNo: 1,
+    pageSize: 10,
+    tabType: 0, // 默认获取上架的商品
+    name: '',
+    categoryId: null,
+    createTime: []
+  }
+  getList()
+}
+
+const categoryList = ref() // 分类列表
+const categoryTreeList = ref() // 分类树
+
+/** 初始化 **/
+onMounted(async () => {
+  await getList()
+  // 获得分类树
+  categoryList.value = await ProductCategoryApi.getCategoryList({})
+  categoryTreeList.value = handleTree(categoryList.value, 'id', 'parentId')
+})
+</script>

+ 0 - 1
src/views/mall/product/spu/components/index.ts

@@ -7,7 +7,6 @@ import SkuList from './SkuList.vue'
 
 import { Spu } from '@/api/mall/product/spu'
 
-// TODO @puhui999:Properties 改成 Property 更合适?Property 在 Spu 中已存在避免冲突 PropertyAndValues
 interface PropertyAndValues {
   id: number
   name: string

+ 9 - 11
src/views/mall/product/spu/index.vue

@@ -80,7 +80,7 @@
     <el-table v-loading="loading" :data="list">
       <el-table-column type="expand" width="30">
         <template #default="{ row }">
-          <el-form class="demo-table-expand" label-position="left">
+          <el-form class="spu-table-expand" label-position="left">
             <el-row>
               <el-col :span="24">
                 <el-row>
@@ -91,12 +91,12 @@
                   </el-col>
                   <el-col :span="8">
                     <el-form-item label="市场价:">
-                      <span>{{ formatToFraction(row.marketPrice) }}</span>
+                      <span>{{ floatToFixed2(row.marketPrice) }}元</span>
                     </el-form-item>
                   </el-col>
                   <el-col :span="8">
                     <el-form-item label="成本价:">
-                      <span>{{ formatToFraction(row.costPrice) }}</span>
+                      <span>{{ floatToFixed2(row.costPrice) }}元</span>
                     </el-form-item>
                   </el-col>
                 </el-row>
@@ -130,9 +130,7 @@
       </el-table-column>
       <el-table-column :show-overflow-tooltip="true" label="商品名称" min-width="300" prop="name" />
       <el-table-column align="center" label="商品售价" min-width="90" prop="price">
-        <template #default="{ row }">
-          {{ formatToFraction(row.price) }}
-        </template>
+        <template #default="{ row }"> {{ floatToFixed2(row.price) }}元</template>
       </el-table-column>
       <el-table-column align="center" label="销量" min-width="90" prop="salesCount" />
       <el-table-column align="center" label="库存" min-width="90" prop="stock" />
@@ -229,7 +227,7 @@ import { createImageViewer } from '@/components/ImageViewer'
 import { dateFormatter } from '@/utils/formatTime'
 import { checkSelectedNode, defaultProps, handleTree, treeToString } from '@/utils/tree'
 import { ProductSpuStatusEnum } from '@/utils/constants'
-import { formatToFraction } from '@/utils'
+import { floatToFixed2 } from '@/utils'
 import download from '@/utils/download'
 import * as ProductSpuApi from '@/api/mall/product/spu'
 import * as ProductCategoryApi from '@/api/mall/product/category'
@@ -392,7 +390,7 @@ const resetQuery = () => {
 const openForm = (id?: number) => {
   // 修改
   if (typeof id === 'number') {
-    push('/product/spu/edit/' + id)
+    push({ name: 'ProductSpuEdit', params: { spuId: id } })
     return
   }
   // 新增
@@ -402,8 +400,8 @@ const openForm = (id?: number) => {
 /**
  * 查看商品详情
  */
-const openDetail = (id?: number) => {
-  push('/product/spu/detail/' + id)
+const openDetail = (id: number) => {
+  push({ name: 'ProductSpuDetail', params: { spuId: id } })
 }
 
 /** 导出按钮操作 */
@@ -458,7 +456,7 @@ onMounted(async () => {
 })
 </script>
 <style lang="scss" scoped>
-.demo-table-expand {
+.spu-table-expand {
   padding-left: 42px;
 
   :deep(.el-form-item__label) {

+ 23 - 10
src/views/mall/promotion/bargain/activity/BargainActivityForm.vue

@@ -51,7 +51,7 @@
       <el-button @click="dialogVisible = false">取 消</el-button>
     </template>
   </Dialog>
-  <SpuSelect ref="spuSelectRef" :isSelectSku="true" @confirm="selectSpu" />
+  <SpuSelect ref="spuSelectRef" :isSelectSku="true" :radio="true" @confirm="selectSpu" />
 </template>
 <script lang="ts" setup>
 import * as BargainActivityApi from '@/api/mall/promotion/bargain/bargainActivity'
@@ -83,17 +83,17 @@ const ruleConfig: RuleConfig[] = [
   {
     name: 'productConfig.bargainFirstPrice',
     rule: (arg) => arg > 0,
-    message: '商品砍价起始价格不能小于0 !!!'
+    message: '商品砍价起始价格不能小于 0 !!!'
   },
   {
     name: 'productConfig.bargainPrice',
-    rule: (arg) => arg > 0,
-    message: '商品砍价底价不能小于0 !!!'
+    rule: (arg) => arg >= 0,
+    message: '商品砍价底价不能小于 0 !!!'
   },
   {
     name: 'productConfig.stock',
-    rule: (arg) => arg > 1,
-    message: '商品活动库存不能小于1 !!!'
+    rule: (arg) => arg >= 1,
+    message: '商品活动库存不能小于 1 !!!'
   }
 ]
 const selectSpu = (spuId: number, skuIds: number[]) => {
@@ -164,7 +164,20 @@ const open = async (type: string, id?: number) => {
       // 用户每次砍价金额分转元, 分转元
       data.randomMinPrice = formatToFraction(data.randomMinPrice)
       data.randomMaxPrice = formatToFraction(data.randomMaxPrice)
-      await getSpuDetails(data.spuId!, data.products?.map((sku) => sku.skuId), data.products)
+      // 对齐活动商品处理结构
+      await getSpuDetails(
+        data.spuId!,
+        [data.skuId],
+        [
+          {
+            spuId: data.spuId!,
+            skuId: data.skuId,
+            bargainFirstPrice: data.bargainFirstPrice, // 砍价起始价格,单位分
+            bargainPrice: data.bargainPrice, // 砍价底价
+            stock: data.stock // 活动库存
+          }
+        ]
+      )
       formRef.value.setValues(data)
     } finally {
       formLoading.value = false
@@ -201,12 +214,12 @@ const submitForm = async () => {
     // 用户每次砍价金额分转元, 元转分
     data.randomMinPrice = convertToInteger(data.randomMinPrice)
     data.randomMaxPrice = convertToInteger(data.randomMaxPrice)
-    data.products = products
+    const formData = { ...data, ...products[0] }
     if (formType.value === 'create') {
-      await BargainActivityApi.createBargainActivity(data)
+      await BargainActivityApi.createBargainActivity(formData)
       message.success(t('common.createSuccess'))
     } else {
-      await BargainActivityApi.updateBargainActivity(data)
+      await BargainActivityApi.updateBargainActivity(formData)
       message.success(t('common.updateSuccess'))
     }
     dialogVisible.value = false

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

@@ -1,5 +1,5 @@
 <template>
-
+  <div></div>
 </template>
-<script lang="ts" setup>
-</script>
+
+<script lang="ts" name="CombinationRecord" setup></script>

+ 26 - 7
src/views/mall/promotion/components/SpuSelect.vue

@@ -127,7 +127,8 @@ defineOptions({ name: 'PromotionSpuSelect' })
 const props = defineProps({
   // 默认不需要(不需要的情况下只返回 spu,需要的情况下返回 选中的 spu 和 sku 列表)
   // 其它活动需要选择商品和商品属性导入此组件即可,需添加组件属性 :isSelectSku='true'
-  isSelectSku: propTypes.bool.def(false) // 是否需要选择 sku 属性
+  isSelectSku: propTypes.bool.def(false), // 是否需要选择 sku 属性
+  radio: propTypes.bool.def(false) // 是否单选 sku
 })
 
 const message = useMessage() // 消息弹窗
@@ -146,7 +147,7 @@ const queryParams = ref({
 }) // 查询参数
 const propertyList = ref<PropertyAndValues[]>([]) // 商品属性列表
 const spuListRef = ref<InstanceType<typeof ElTable>>()
-const skuListRef = ref() // 商品属性选择 Ref
+const skuListRef = ref<InstanceType<typeof SkuList>>() // 商品属性选择 Ref
 const spuData = ref<ProductSpuApi.Spu>() // 商品详情
 const isExpand = ref(false) // 控制 SKU 列表显示
 const expandRowKeys = ref<number[]>() // 控制展开行需要设置 row-key 属性才能使用,该属性为展开行的 keys 数组。
@@ -155,12 +156,30 @@ const expandRowKeys = ref<number[]>() // 控制展开行需要设置 row-key 属
 const selectedSpuId = ref<number>(0) // 选中的商品 spuId
 const selectedSkuIds = ref<number[]>([]) // 选中的商品 skuIds
 const selectSku = (val: ProductSpuApi.Sku[]) => {
+  const skuTable = skuListRef.value?.getSkuTableRef()
   if (selectedSpuId.value === 0) {
     message.warning('请先选择商品再选择相应的规格!!!')
-    skuListRef.value.clearSelection()
+    skuTable?.clearSelection()
     return
   }
-  selectedSkuIds.value = val.map((sku) => sku.id!)
+  if (val.length === 0) {
+    selectedSkuIds.value = []
+    return
+  }
+  if (props.radio) {
+    // 只选择一个
+    selectedSkuIds.value = [val.map((sku) => sku.id!)[0]]
+    // 如果大于1个
+    if (val.length > 1) {
+      // 清空选择
+      skuTable?.clearSelection()
+      // 变更为最后一次选择的
+      skuTable?.toggleRowSelection(val.pop(), true)
+      return
+    }
+  } else {
+    selectedSkuIds.value = val.map((sku) => sku.id!)
+  }
 }
 const selectSpu = (val: ProductSpuApi.Spu[]) => {
   if (val.length === 0) {
@@ -176,9 +195,9 @@ const selectSpu = (val: ProductSpuApi.Spu[]) => {
   // 如果大于1个
   if (val.length > 1) {
     // 清空选择
-    spuListRef.value.clearSelection()
+    spuListRef.value?.clearSelection()
     // 变更为最后一次选择的
-    spuListRef.value.toggleRowSelection(val.pop(), true)
+    spuListRef.value?.toggleRowSelection(val.pop(), true)
     return
   }
   expandChange(val[0], val)
@@ -194,7 +213,7 @@ const expandChange = async (row: ProductSpuApi.Spu, expandedRows?: ProductSpuApi
       expandRowKeys.value = [selectedSpuId.value]
       return
     }
-    // 如果展开 skuList 则选择此对应的 spu 不需要重新获取渲染 skuList
+    // 如果展开 skuList 则选择此对应的 spu 不需要重新获取渲染 skuList
     if (isExpand.value && spuData.value?.id === row.id) {
       return
     }

+ 0 - 12
src/views/mall/promotion/seckill/activity/SeckillActivityForm.vue

@@ -193,16 +193,4 @@ const resetForm = async () => {
   await nextTick()
   formRef.value.getElFormRef().resetFields()
 }
-// TODO @puhui999:下面的 css 名字,是不是可以改下;demo-table-expand
 </script>
-<style lang="scss" scoped>
-.demo-table-expand {
-  padding-left: 42px;
-
-  :deep(.el-form-item__label) {
-    width: 82px;
-    font-weight: bold;
-    color: #99a9bf;
-  }
-}
-</style>

+ 31 - 49
src/views/mall/trade/delivery/pickUpStore/index.vue

@@ -1,27 +1,27 @@
 <template>
   <!-- 搜索工作栏 -->
   <ContentWrap>
-    <el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true">
+    <el-form ref="queryFormRef" :inline="true" :model="queryParams" class="-mb-15px">
       <el-form-item label="门店手机" prop="phone">
         <el-input
           v-model="queryParams.phone"
-          placeholder="请输门店手机"
+          class="!w-240px"
           clearable
+          placeholder="请输门店手机"
           @keyup.enter="handleQuery"
-          class="!w-240px"
         />
       </el-form-item>
       <el-form-item label="门店名称" prop="name">
         <el-input
           v-model="queryParams.name"
-          placeholder="请输门店名称"
+          class="!w-240px"
           clearable
+          placeholder="请输门店名称"
           @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-select v-model="queryParams.status" class="!w-240px" clearable placeholder="门店状态">
           <el-option
             v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
             :key="dict.value"
@@ -33,32 +33,30 @@
       <el-form-item label="创建时间" prop="createTime">
         <el-date-picker
           v-model="queryParams.createTime"
-          value-format="YYYY-MM-DD HH:mm:ss"
-          type="datetimerange"
-          start-placeholder="开始日期"
-          end-placeholder="结束日期"
           class="!w-240px"
+          end-placeholder="结束日期"
+          start-placeholder="开始日期"
+          type="datetimerange"
+          value-format="YYYY-MM-DD HH:mm:ss"
         />
       </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="['trade:delivery:pick-up-store:create']"
-        >
-          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        <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-button
-          type="success"
+          v-hasPermi="['trade:delivery:pick-up-store:create']"
           plain
-          @click="handleExport"
-          :loading="exportLoading"
-          v-hasPermi="['trade:delivery:pick-up-store:export']"
+          type="primary"
+          @click="openForm('create')"
         >
-          <Icon icon="ep:download" class="mr-5px" /> 导出
+          <Icon class="mr-5px" icon="ep:plus" />
+          新增
         </el-button>
       </el-form-item>
     </el-form>
@@ -75,34 +73,34 @@
       </el-table-column>
       <el-table-column label="门店名称" prop="name" />
       <el-table-column label="门店手机" prop="phone" />
-      <el-table-column label="门店详细地址" align="center" prop="detailAddress" />
-      <el-table-column label="开启状态" align="center" prop="status">
+      <el-table-column align="center" label="门店详细地址" prop="detailAddress" />
+      <el-table-column align="center" label="开启状态" prop="status">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
         </template>
       </el-table-column>
       <el-table-column
-        label="创建时间"
+        :formatter="dateFormatter"
         align="center"
+        label="创建时间"
         prop="createTime"
         width="180"
-        :formatter="dateFormatter"
       />
-      <el-table-column label="操作" align="center">
+      <el-table-column align="center" label="操作">
         <template #default="scope">
           <el-button
+            v-hasPermi="['trade:delivery:pick-up-store:update']"
             link
             type="primary"
             @click="openForm('update', scope.row.id)"
-            v-hasPermi="['trade:delivery:pick-up-store:update']"
           >
             编辑
           </el-button>
           <el-button
+            v-hasPermi="['trade:delivery:pick-up-store:delete']"
             link
             type="danger"
             @click="handleDelete(scope.row.id)"
-            v-hasPermi="['trade:delivery:pick-up-store:delete']"
           >
             删除
           </el-button>
@@ -113,18 +111,17 @@
   <!-- 表单弹窗:添加/修改 -->
   <DeliveryPickUpStoreForm ref="formRef" @success="getList" />
 </template>
-<script setup lang="ts" name="DeliveryPickUpStore">
+<script lang="ts" name="DeliveryPickUpStore" setup>
 import * as DeliveryPickUpStoreApi from '@/api/mall/trade/delivery/pickUpStore'
 import DeliveryPickUpStoreForm from './PickUpStoreForm.vue'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
-import download from '@/utils/download'
+
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 
 const total = ref(0) // 列表的总页数
 const loading = ref(true) // 列表的加载中
-const exportLoading = ref(false) // 导出的加载中
 const list = ref<any[]>([]) // 列表的数据
 const queryParams = reactive({
   pageNo: 1,
@@ -179,21 +176,6 @@ const resetQuery = () => {
   handleQuery()
 }
 
-/** 导出按钮操作 */
-const handleExport = async () => {
-  try {
-    // 导出的二次确认
-    await message.exportConfirm()
-    // 发起导出
-    exportLoading.value = true
-    const data = await DeliveryPickUpStoreApi.exportDeliveryPickUpStoreApi(queryParams)
-    download.excel(data, '自提门店.xls')
-  } catch {
-  } finally {
-    exportLoading.value = false
-  }
-}
-
 /** 初始化 **/
 onMounted(() => {
   getList()

+ 100 - 0
src/views/mall/trade/order/components/OrderDeliveryForm.vue

@@ -0,0 +1,100 @@
+<template>
+  <Dialog v-model="dialogVisible" title="订单发货" width="25%">
+    <el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="80px">
+      <el-form-item label="发货方式">
+        <el-radio-group v-model="expressType">
+          <el-radio border label="express">快递物流</el-radio>
+          <el-radio border label="none">无需发货</el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <template v-if="expressType === 'express'">
+        <el-form-item label="物流公司">
+          <el-select v-model="formData.logisticsId" placeholder="请选择" style="width: 100%">
+            <el-option
+              v-for="item in deliveryExpressList"
+              :key="item.id"
+              :label="item.name"
+              :value="item.id"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="物流单号">
+          <el-input v-model="formData.logisticsNo" />
+        </el-form-item>
+      </template>
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+import * as DeliveryExpressApi from '@/api/mall/trade/delivery/express'
+import * as TradeOrderApi from '@/api/mall/trade/order'
+import { copyValueToTarget } from '@/utils'
+
+// TODO @puhui999:要不 Form 保持和别的模块一样,和 index.vue 放在一个目录下,不作为 components;components 更多要支持给多个模块使用哈。
+defineOptions({ name: 'OrderDeliveryForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const expressType = ref('express') // 如果值是 express,则是快递;none 则是无;未来做同城配送;
+const formData = ref<TradeOrderApi.DeliveryVO>({
+  id: 0, // 订单编号
+  logisticsId: null, // 物流公司编号
+  logisticsNo: '' // 物流编号
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (row: TradeOrderApi.OrderVO) => {
+  resetForm()
+  // 设置数据
+  copyValueToTarget(formData.value, row)
+  if (row.logisticsId === null || row.logisticsId === 0) {
+    expressType.value = 'none'
+  }
+  dialogVisible.value = true
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = unref(formData)
+    if (expressType.value === 'none') {
+      // 无需发货的情况
+      data.logisticsId = 0
+      data.logisticsNo = ''
+    }
+    await TradeOrderApi.delivery(data)
+    message.success(t('common.updateSuccess'))
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success', true)
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: 0, // 订单编号
+    logisticsId: null, // 物流公司编号
+    logisticsNo: '' // 物流编号
+  }
+  formRef.value?.resetFields()
+}
+const deliveryExpressList = ref([])
+onMounted(async () => {
+  deliveryExpressList.value = await DeliveryExpressApi.getSimpleDeliveryExpressList()
+})
+</script>

+ 98 - 0
src/views/mall/trade/order/components/OrderUpdateAddressForm.vue

@@ -0,0 +1,98 @@
+<template>
+  <Dialog v-model="dialogVisible" title="修改订单收货地址" width="35%">
+    <el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="120px">
+      <el-form-item label="收件人">
+        <el-input v-model="formData.receiverName" placeholder="请输入收件人名称" />
+      </el-form-item>
+      <el-form-item label="手机号">
+        <el-input v-model="formData.receiverMobile" placeholder="请输入收件人手机号" />
+      </el-form-item>
+      <el-form-item label="所在地">
+        <el-tree-select
+          v-model="formData.receiverAreaId"
+          :data="areaList"
+          :props="defaultProps"
+          :render-after-expand="true"
+        />
+      </el-form-item>
+      <el-form-item label="详细地址">
+        <el-input
+          v-model="formData.receiverDetailAddress"
+          :rows="3"
+          placeholder="请输入收件人详细地址"
+          type="textarea"
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+import * as TradeOrderApi from '@/api/mall/trade/order'
+import { getAreaTree } from '@/api/system/area'
+import { copyValueToTarget } from '@/utils'
+import { defaultProps } from '@/utils/tree'
+
+defineOptions({ name: 'OrderUpdateAddressForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formData = ref({
+  id: 0, // 订单编号
+  receiverName: '', // 收件人名称
+  receiverMobile: '', // 收件人手机
+  receiverAreaId: null, //收件人地区编号
+  receiverDetailAddress: '' //收件人详细地址
+})
+const areaList = ref([]) // 地区列表
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (row: TradeOrderApi.OrderVO) => {
+  resetForm()
+  // 设置数据
+  copyValueToTarget(formData.value, row)
+  dialogVisible.value = true
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = unref(formData)
+    await TradeOrderApi.updateAddress(data)
+    message.success(t('common.updateSuccess'))
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success', true)
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: 0, // 订单编号
+    receiverName: '', // 收件人名称
+    receiverMobile: '', // 收件人手机
+    receiverAreaId: null, //收件人地区编号
+    receiverDetailAddress: '' //收件人详细地址
+  }
+  formRef.value?.resetFields()
+}
+
+onMounted(async () => {
+  // 获得地区列表
+  areaList.value = await getAreaTree()
+})
+</script>

+ 92 - 0
src/views/mall/trade/order/components/OrderUpdatePriceForm.vue

@@ -0,0 +1,92 @@
+<template>
+  <Dialog v-model="dialogVisible" title="订单调价" width="25%">
+    <el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="100px">
+      <el-form-item label="应付金额(总)">
+        <el-input v-model="formData.payPrice" disabled />
+      </el-form-item>
+      <el-form-item label="订单调价">
+        <el-input-number v-model="formData.adjustPrice" :precision="2" :step="0.1" class="w-100%" />
+        <el-tag class="mt-10px" type="warning">订单调价。 正数,加价;负数,减价</el-tag>
+      </el-form-item>
+      <el-form-item label="调价后">
+        <el-input v-model="formData.newPayPrice" disabled />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+import * as TradeOrderApi from '@/api/mall/trade/order'
+import { convertToInteger, floatToFixed2, formatToFraction } from '@/utils'
+import { cloneDeep } from 'lodash-es'
+
+defineOptions({ name: 'OrderUpdatePriceForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formData = ref({
+  id: 0, // 订单编号
+  adjustPrice: 0, // 订单调价
+  payPrice: '', // 应付金额(总)
+  newPayPrice: '' // 调价后应付金额(总)
+})
+watch(
+  () => formData.value.adjustPrice,
+  (data: number) => {
+    const num = formData.value.payPrice!.replace('元', '')
+    // @ts-ignore
+    formData.value.newPayPrice = (num * 1 + data).toFixed(2) + '元'
+  }
+)
+
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (row: TradeOrderApi.OrderVO) => {
+  resetForm()
+  formData.value.id = row.id!
+  // 设置数据
+  formData.value.adjustPrice = formatToFraction(row.adjustPrice!)
+  formData.value.payPrice = floatToFixed2(row.payPrice!) + '元'
+  formData.value.newPayPrice = formData.value.payPrice
+  dialogVisible.value = true
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = cloneDeep(unref(formData))
+    data.adjustPrice = convertToInteger(data.adjustPrice)
+    delete data.payPrice
+    delete data.newPayPrice
+    await TradeOrderApi.updatePrice(data)
+    message.success(t('common.updateSuccess'))
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success', true)
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: 0, // 订单编号
+    adjustPrice: 0, // 订单调价
+    payPrice: '', // 应付金额(总)
+    newPayPrice: '' // 调价后应付金额(总)
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 71 - 0
src/views/mall/trade/order/components/OrderUpdateRemarkForm.vue

@@ -0,0 +1,71 @@
+<template>
+  <Dialog v-model="dialogVisible" title="商家备注" width="45%">
+    <el-form ref="formRef" v-loading="formLoading" :model="formData" label-width="80px">
+      <el-form-item label="备注">
+        <el-input
+          v-model="formData.remark"
+          :rows="3"
+          placeholder="请输入订单备注"
+          type="textarea"
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+import * as TradeOrderApi from '@/api/mall/trade/order'
+
+defineOptions({ name: 'OrderUpdateRemarkForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formData = ref({
+  id: 0, // 订单编号
+  remark: '' // 订单备注
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (row: TradeOrderApi.OrderVO) => {
+  resetForm()
+  // 设置数据
+  formData.value.id = row.id
+  formData.value.remark = row.remark
+  dialogVisible.value = true
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = unref(formData)
+    console.log(data)
+    await TradeOrderApi.updateRemark(data)
+    message.success(t('common.updateSuccess'))
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success', true)
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: 0, // 订单编号
+    remark: '' // 订单备注
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 358 - 365
src/views/mall/trade/order/tradeOrderDetail.vue → src/views/mall/trade/order/detail/index.vue

@@ -1,365 +1,358 @@
-<template>
-  <ContentWrap>
-    <!-- 订单信息 -->
-    <el-descriptions title="订单信息">
-      <el-descriptions-item label="订单号: ">{{ order.no }}</el-descriptions-item>
-      <el-descriptions-item label="配送方式: ">物流配送</el-descriptions-item>
-      <!-- TODO 芋艿:待实现 -->
-      <el-descriptions-item label="营销活动: ">物流配送</el-descriptions-item>
-      <!-- TODO 芋艿:待实现 -->
-      <el-descriptions-item label="订单类型: ">
-        <dict-tag :type="DICT_TYPE.TRADE_ORDER_TYPE" :value="order.type" />
-      </el-descriptions-item>
-      <el-descriptions-item label="收货人: ">{{ order.receiverName }}</el-descriptions-item>
-      <el-descriptions-item label="买家留言: ">{{ order.userRemark }}</el-descriptions-item>
-      <el-descriptions-item label="订单来源: ">
-        <dict-tag :type="DICT_TYPE.TERMINAL" :value="order.terminal" />
-      </el-descriptions-item>
-      <el-descriptions-item label="联系电话: ">{{ order.receiverMobile }}</el-descriptions-item>
-      <el-descriptions-item label="商家备注: ">{{ order.remark }}</el-descriptions-item>
-      <el-descriptions-item label="支付单号: ">{{ order.payOrderId }}</el-descriptions-item>
-      <el-descriptions-item label="付款方式: ">
-        <dict-tag :type="DICT_TYPE.PAY_CHANNEL_CODE_TYPE" :value="order.payChannelCode" />
-      </el-descriptions-item>
-      <!-- <el-descriptions-item label="买家: ">{{ order.user.nickname }}</el-descriptions-item> -->
-      <!-- TODO 芋艿:待实现:跳转会员 -->
-      <el-descriptions-item label="收货地址: ">
-        {{ order.receiverAreaName }} {{ order.receiverDetailAddress }}
-        <el-link
-          v-clipboard:copy="order.receiverAreaName + ' ' + order.receiverDetailAddress"
-          v-clipboard:success="clipboardSuccess"
-          icon="ep:document-copy"
-          type="primary"
-        />
-      </el-descriptions-item>
-    </el-descriptions>
-
-    <!-- 订单状态 -->
-    <el-descriptions title="订单状态" :column="1">
-      <el-descriptions-item label="订单状态: ">
-        <!-- TODO xiaobai:status 一定有值哈,不用判断 -->
-        <dict-tag
-          v-if="order.status !== ''"
-          :type="DICT_TYPE.TRADE_ORDER_STATUS"
-          :value="order.status"
-        />
-      </el-descriptions-item>
-      <el-descriptions-item label-class-name="no-colon">
-        <el-button type="primary" size="small">调整价格</el-button>
-        <!-- TODO 芋艿:待实现 -->
-        <el-button type="primary" size="small">备注</el-button>
-        <!-- TODO 芋艿:待实现 -->
-        <el-button type="primary" size="small">发货</el-button>
-        <!-- TODO 芋艿:待实现 -->
-        <el-button type="primary" size="small">关闭订单</el-button>
-        <!-- TODO 芋艿:待实现 -->
-        <el-button type="primary" size="small">修改地址</el-button>
-        <!-- TODO 芋艿:待实现 -->
-        <el-button type="primary" size="small">打印电子面单</el-button>
-        <!-- TODO 芋艿:待实现 -->
-        <el-button type="primary" size="small">打印发货单</el-button>
-        <!-- TODO 芋艿:待实现 -->
-        <el-button type="primary" size="small">确认收货</el-button>
-        <!-- TODO 芋艿:待实现 -->
-      </el-descriptions-item>
-      <el-descriptions-item>
-        <template #label><span style="color: red">提醒: </span></template>
-        买家付款成功后,货款将直接进入您的商户号(微信、支付宝)<br />
-        请及时关注你发出的包裹状态,确保可以配送至买家手中 <br />
-        如果买家表示没收到货或货物有问题,请及时联系买家处理,友好协商
-      </el-descriptions-item>
-    </el-descriptions>
-
-    <!-- 物流信息 TODO -->
-
-    <!-- 商品信息 -->
-    <el-descriptions title="商品信息">
-      <el-descriptions-item labelClassName="no-colon">
-        <el-row :gutter="20">
-          <el-col :span="15">
-            <el-table :data="order.items" border>
-              <el-table-column prop="spuName" label="商品" width="auto">
-                <template #default="{ row }">
-                  {{ row.spuName }}
-                  <el-tag
-                    size="medium"
-                    v-for="property in row.properties"
-                    :key="property.propertyId"
-                  >
-                    {{ property.propertyName }}: {{ property.valueName }}</el-tag
-                  >
-                </template>
-              </el-table-column>
-              <el-table-column prop="price" label="商品原价(元)" width="150">
-                <template #default="{ row }"> ¥{{ (row.price / 100.0).toFixed(2) }} </template>
-              </el-table-column>
-              <el-table-column prop="count" label="数量" width="100" />
-              <el-table-column prop="payPrice" label="合计(元)" width="150">
-                <template #default="{ row }"> ¥{{ (row.payPrice / 100.0).toFixed(2) }} </template>
-              </el-table-column>
-              <el-table-column prop="afterSaleStatus" label="售后状态" width="auto">
-                <template #default="{ row }">
-                  <dict-tag
-                    :type="DICT_TYPE.TRADE_ORDER_ITEM_AFTER_SALE_STATUS"
-                    :value="row.afterSaleStatus"
-                  />
-                </template>
-              </el-table-column>
-            </el-table>
-          </el-col>
-          <el-col :span="10" />
-        </el-row>
-      </el-descriptions-item>
-      <!-- 占位 -->
-      <!-- <el-descriptions-item v-for="item in 5" label-class-name="no-colon" :key="item" /> -->
-    </el-descriptions>
-    <el-descriptions column="6">
-      <el-descriptions-item label="商品总额: ">
-        <!-- TODO xiaobai: 是不是 (item.payPrice / 100.0).toFixed(2) -->
-        ¥{{ parseFloat((order.totalPrice / 100.0) as unknown as string).toFixed(2) }}
-      </el-descriptions-item>
-      <el-descriptions-item label="运费金额: ">
-        ¥{{ parseFloat((order.deliveryPrice / 100.0) as unknown as string).toFixed(2) }}
-      </el-descriptions-item>
-      <el-descriptions-item label="订单调价: ">
-        ¥{{
-          parseFloat((order.adjustPrice / 100.0) as unknown as string).toFixed(2)
-        }}</el-descriptions-item
-      >
-
-      <el-descriptions-item>
-        <template #label><span style="color: red">商品优惠: </span></template>
-        <!-- 没理解TODO  order.totalPrice - order.totalPrice -->
-        ¥{{
-          parseFloat(((order.totalPrice - order.totalPrice) / 100.0) as unknown as string).toFixed(
-            2
-          )
-        }}
-      </el-descriptions-item>
-      <el-descriptions-item>
-        <template #label><span style="color: red">订单优惠: </span></template>
-        ¥{{ parseFloat((order.discountPrice / 100.0) as unknown as string).toFixed(2) }}
-      </el-descriptions-item>
-      <el-descriptions-item>
-        <template #label><span style="color: red">积分抵扣: </span></template>
-        ¥{{ parseFloat((order.pointPrice / 100.0) as unknown as string).toFixed(2) }}
-      </el-descriptions-item>
-
-      <el-descriptions-item v-for="item in 5" label-class-name="no-colon" :key="item" />
-      <!-- 占位 -->
-      <el-descriptions-item label="应付金额: ">
-        ¥{{ (order.payPrice / 100.0).toFixed(2) }}
-      </el-descriptions-item>
-    </el-descriptions>
-
-    <!-- TODO 芋艿:需要改改 -->
-    <div v-for="group in detailGroups" :key="group.title">
-      <el-descriptions v-bind="group.groupProps" :title="group.title">
-        <!-- 订单操作日志 -->
-        <el-descriptions-item v-if="group.key === 'orderLog'" labelClassName="no-colon">
-          <el-timeline>
-            <el-timeline-item
-              v-for="activity in detailInfo[group.key]"
-              :key="activity.timestamp"
-              :timestamp="activity.timestamp"
-            >
-              {{ activity.content }}
-            </el-timeline-item>
-          </el-timeline>
-        </el-descriptions-item>
-
-        <!-- 物流信息 -->
-        <!-- TODO @xiaobai:改成一个包裹哈;目前只允许发货一次 -->
-        <el-descriptions-item v-if="group.key === 'expressInfo'" labelClassName="no-colon">
-          <!-- 循环包裹物流信息 -->
-          <div v-show="(pkgInfo = detailInfo[group.key]) !== null" style="border: 1px dashed">
-            <!-- 包裹详情 -->
-            <el-descriptions class="m-5">
-              <el-descriptions-item
-                v-for="(pkgChild, pkgCIdx) in group.children"
-                v-bind="pkgChild.childProps"
-                :key="`pkgChild_${pkgCIdx}`"
-                :label="pkgChild.label"
-              >
-                <!-- 包裹商品列表 -->
-                <template v-if="pkgChild.valueKey === 'goodsList' && pkgInfo[pkgChild.valueKey]">
-                  <div
-                    v-for="(goodInfo, goodInfoIdx) in pkgInfo[pkgChild.valueKey]"
-                    :key="`goodInfo_${goodInfoIdx}`"
-                    style="display: flex"
-                  >
-                    <el-image
-                      style="width: 100px; height: 100px; flex: none"
-                      :src="goodInfo.imgUrl"
-                    />
-                    <el-descriptions :column="1">
-                      <el-descriptions-item labelClassName="no-colon">{{
-                        goodInfo.name
-                      }}</el-descriptions-item>
-                      <el-descriptions-item label="数量">{{ goodInfo.count }}</el-descriptions-item>
-                    </el-descriptions>
-                  </div>
-                </template>
-
-                <!-- 包裹物流详情 -->
-                <template v-else-if="pkgChild.valueKey === 'wlxq'">
-                  <el-row :gutter="10">
-                    <el-col :span="6" :offset="1">
-                      <el-timeline>
-                        <el-timeline-item
-                          v-for="(activity, index) in pkgInfo[pkgChild.valueKey]"
-                          :key="index"
-                          :timestamp="activity.timestamp"
-                        >
-                          {{ activity.content }}
-                        </el-timeline-item>
-                      </el-timeline>
-                    </el-col>
-                  </el-row>
-                </template>
-                <template v-else>
-                  {{ pkgInfo[pkgChild.valueKey] }}
-                </template>
-              </el-descriptions-item>
-            </el-descriptions>
-          </div>
-        </el-descriptions-item>
-      </el-descriptions>
-    </div>
-  </ContentWrap>
-</template>
-<script lang="ts" name="TradeOrderDetail" setup>
-// TODO @xiaobai:在 order 下创建一个 order/detail,然后改名为 index.vue
-import { DICT_TYPE } from '@/utils/dict'
-import * as TradeOrderApi from '@/api/mall/trade/order'
-const message = useMessage() // 消息弹窗
-
-const { query } = useRoute()
-const queryParams = reactive({
-  id: query.id
-})
-const dialogVisible = ref(false)
-const loading = ref(false)
-const order = ref<any>({
-  items: [],
-  user: {}
-}) // 详情数据
-
-const detailGroups = ref([
-  {
-    title: '物流信息',
-    key: 'expressInfo',
-    children: [
-      { label: '发货时间: ', valueKey: 'fhsj' },
-      { label: '物流公司: ', valueKey: 'wlgs' },
-      { label: '运单号: ', valueKey: 'ydh' },
-      { label: '物流状态: ', valueKey: 'wlzt', childProps: { span: 3 } },
-      { label: '物流详情: ', valueKey: 'wlxq' }
-    ]
-  },
-  {
-    title: '订单操作日志',
-    key: 'orderLog'
-  }
-])
-
-const detailInfo = ref({
-  expressInfo:
-    // 物流信息
-    {
-      label: '包裹1',
-      name: 'bg1',
-      fhsj: '2022-11-03 16:50:45',
-      wlgs: '极兔',
-      ydh: '2132123',
-      wlzt: '不支持此快递公司',
-      wlxq: [
-        {
-          content: '正在派送途中,请您准备签收(派件人:王涛,电话:13854563814)',
-          timestamp: '2018-04-15 15:00:16'
-        },
-        {
-          content: '快件到达 【烟台龙口东江村委营业点】',
-          timestamp: '2018-04-13 14:54:19'
-        },
-        {
-          content: '快件已发车',
-          timestamp: '2018-04-11 12:55:52'
-        },
-        {
-          content: '快件已发车',
-          timestamp: '2018-04-11 12:55:52'
-        },
-        {
-          content: '快件已发车',
-          timestamp: '2018-04-11 12:55:52'
-        }
-      ]
-    },
-  orderLog: [
-    // 订单操作日志
-    {
-      content: '买家【乌鸦】关闭了订单',
-      timestamp: '2018-04-15 15:00:16'
-    },
-    {
-      content: '买家【乌鸦】下单了',
-      timestamp: '2018-04-15 15:00:16'
-    }
-  ],
-  goodsInfo: [] // 商品详情tableData
-})
-// 暂考虑一次性加载详情页面所有数据 TODO
-const getlist = async () => {
-  dialogVisible.value = true
-  loading.value = true
-  try {
-    const res = await TradeOrderApi.getOrderDetail(queryParams.id as unknown as number)
-    order.value = res
-    console.log(order)
-  } catch {
-    message.error('获取详情数据失败')
-  } finally {
-    loading.value = false
-  }
-}
-onMounted(async () => {
-  await getlist()
-})
-const clipboardSuccess = () => {
-  message.success('复制成功')
-}
-</script>
-<style lang="scss" scoped>
-:deep(.el-descriptions) {
-  &:not(:nth-child(1)) {
-    margin-top: 20px;
-  }
-
-  .el-descriptions__title {
-    display: flex;
-    align-items: center;
-
-    &::before {
-      display: inline-block;
-      width: 3px;
-      height: 20px;
-      margin-right: 10px;
-      background-color: #409eff;
-      content: '';
-    }
-  }
-
-  .el-descriptions-item__container {
-    margin: 0 10px;
-
-    .no-colon {
-      margin: 0;
-
-      &::after {
-        content: '';
-      }
-    }
-  }
-}
-</style>
+<template>
+  <ContentWrap>
+    <!-- 订单信息 -->
+    <el-descriptions title="订单信息">
+      <el-descriptions-item label="订单号: ">{{ orderInfo.no }}</el-descriptions-item>
+      <el-descriptions-item label="配送方式: ">
+        <dict-tag :type="DICT_TYPE.TRADE_DELIVERY_TYPE" :value="orderInfo.deliveryType" />
+      </el-descriptions-item>
+      <!-- TODO 营销活动待实现     -->
+      <el-descriptions-item label="营销活动: ">秒杀活动</el-descriptions-item>
+      <el-descriptions-item label="订单类型: ">
+        <dict-tag :type="DICT_TYPE.TRADE_ORDER_TYPE" :value="orderInfo.type" />
+      </el-descriptions-item>
+      <el-descriptions-item label="收货人: ">{{ orderInfo.receiverName }}</el-descriptions-item>
+      <el-descriptions-item label="买家留言: ">{{ orderInfo.userRemark }}</el-descriptions-item>
+      <el-descriptions-item label="订单来源: ">
+        <dict-tag :type="DICT_TYPE.TERMINAL" :value="orderInfo.terminal" />
+      </el-descriptions-item>
+      <el-descriptions-item label="联系电话: ">{{ orderInfo.receiverMobile }}</el-descriptions-item>
+      <el-descriptions-item label="商家备注: ">{{ orderInfo.remark }}</el-descriptions-item>
+      <el-descriptions-item label="支付单号: ">{{ orderInfo.payOrderId }}</el-descriptions-item>
+      <el-descriptions-item label="付款方式: ">
+        <dict-tag :type="DICT_TYPE.PAY_CHANNEL_CODE" :value="orderInfo.payChannelCode" />
+      </el-descriptions-item>
+      <!-- <el-descriptions-item label="买家: ">{{ orderInfo.user.nickname }}</el-descriptions-item> -->
+      <!-- TODO 芋艿:待实现:跳转会员 -->
+      <el-descriptions-item label="收货地址: ">
+        {{ orderInfo.receiverAreaName }} {{ orderInfo.receiverDetailAddress }}
+        <el-link
+          v-clipboard:copy="orderInfo.receiverAreaName + ' ' + orderInfo.receiverDetailAddress"
+          v-clipboard:success="clipboardSuccess"
+          icon="ep:document-copy"
+          type="primary"
+        />
+      </el-descriptions-item>
+    </el-descriptions>
+
+    <!-- 订单状态 -->
+    <el-descriptions :column="1" title="订单状态">
+      <el-descriptions-item label="订单状态: ">
+        <dict-tag :type="DICT_TYPE.TRADE_ORDER_STATUS" :value="orderInfo.status" />
+      </el-descriptions-item>
+      <el-descriptions-item label-class-name="no-colon">
+        <el-button type="primary" @click="openForm('updatePrice')">调整价格</el-button>
+        <el-button type="primary" @click="openForm('remark')">备注</el-button>
+        <el-button type="primary" @click="openForm('delivery')">发货</el-button>
+        <el-button type="primary" @click="openForm('updateAddress')">修改地址</el-button>
+      </el-descriptions-item>
+      <el-descriptions-item>
+        <template #label><span style="color: red">提醒: </span></template>
+        买家付款成功后,货款将直接进入您的商户号(微信、支付宝)<br />
+        请及时关注你发出的包裹状态,确保可以配送至买家手中 <br />
+        如果买家表示没收到货或货物有问题,请及时联系买家处理,友好协商
+      </el-descriptions-item>
+    </el-descriptions>
+
+    <!-- 商品信息 -->
+    <el-descriptions title="商品信息">
+      <el-descriptions-item labelClassName="no-colon">
+        <el-row :gutter="20">
+          <el-col :span="15">
+            <el-table :data="orderInfo.items" border>
+              <el-table-column label="商品" prop="spuName" width="auto">
+                <template #default="{ row }">
+                  {{ row.spuName }}
+                  <el-tag v-for="property in row.properties" :key="property.propertyId">
+                    {{ property.propertyName }}: {{ property.valueName }}
+                  </el-tag>
+                </template>
+              </el-table-column>
+              <el-table-column label="商品原价" prop="price" width="150">
+                <template #default="{ row }">{{ floatToFixed2(row.price) }}元</template>
+              </el-table-column>
+              <el-table-column label="数量" prop="count" width="100" />
+              <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>
+              </el-table-column>
+            </el-table>
+          </el-col>
+          <el-col :span="10" />
+        </el-row>
+      </el-descriptions-item>
+    </el-descriptions>
+    <el-descriptions :column="6">
+      <el-descriptions-item label="商品总额: ">
+        {{ floatToFixed2(orderInfo.totalPrice) }}元
+      </el-descriptions-item>
+      <el-descriptions-item label="运费金额: ">
+        {{ floatToFixed2(orderInfo.deliveryPrice) }}元
+      </el-descriptions-item>
+      <el-descriptions-item label="订单调价: ">
+        {{ floatToFixed2(orderInfo.updatePrice) }}元
+      </el-descriptions-item>
+
+      <el-descriptions-item>
+        <template #label><span style="color: red">商品优惠: </span></template>
+        {{ floatToFixed2(orderInfo.couponPrice) }}元
+      </el-descriptions-item>
+      <el-descriptions-item>
+        <template #label><span style="color: red">订单优惠: </span></template>
+        {{ floatToFixed2(orderInfo.discountPrice) }}元
+      </el-descriptions-item>
+      <el-descriptions-item>
+        <template #label><span style="color: red">积分抵扣: </span></template>
+        {{ floatToFixed2(orderInfo.pointPrice) }}元
+      </el-descriptions-item>
+
+      <el-descriptions-item v-for="item in 5" :key="item" label-class-name="no-colon" />
+      <!-- 占位 -->
+      <el-descriptions-item label="应付金额: ">
+        {{ floatToFixed2(orderInfo.payPrice) }}元
+      </el-descriptions-item>
+    </el-descriptions>
+
+    <!-- TODO 芋艿:需要改改 -->
+    <div v-for="group in detailGroups" :key="group.title">
+      <el-descriptions :title="group.title" v-bind="group.groupProps">
+        <!-- 订单操作日志 -->
+        <el-descriptions-item v-if="group.key === 'orderLog'" labelClassName="no-colon">
+          <el-timeline>
+            <el-timeline-item
+              v-for="activity in detailInfo[group.key]"
+              :key="activity.timestamp"
+              :timestamp="activity.timestamp"
+            >
+              {{ activity.content }}
+            </el-timeline-item>
+          </el-timeline>
+        </el-descriptions-item>
+
+        <!-- 物流信息 TODO 等物流接口搞定重构一下 -->
+        <!-- TODO @xiaobai:改成一个包裹哈;目前只允许发货一次 -->
+        <el-descriptions-item v-if="group.key === 'expressInfo'" labelClassName="no-colon">
+          <!-- 循环包裹物流信息 -->
+          <div v-show="(pkgInfo = detailInfo[group.key]) !== null" style="border: 1px dashed">
+            <!-- 包裹详情 -->
+            <el-descriptions class="m-5">
+              <el-descriptions-item
+                v-for="(pkgChild, pkgCIdx) in group.children"
+                :key="`pkgChild_${pkgCIdx}`"
+                :label="pkgChild.label"
+                v-bind="pkgChild.childProps"
+              >
+                <!-- 包裹商品列表 -->
+                <template v-if="pkgChild.valueKey === 'goodsList' && pkgInfo[pkgChild.valueKey]">
+                  <div
+                    v-for="(goodInfo, goodInfoIdx) in pkgInfo[pkgChild.valueKey]"
+                    :key="`goodInfo_${goodInfoIdx}`"
+                    style="display: flex"
+                  >
+                    <el-image
+                      :src="goodInfo.imgUrl"
+                      style="width: 100px; height: 100px; flex: none"
+                    />
+                    <el-descriptions :column="1">
+                      <el-descriptions-item labelClassName="no-colon"
+                        >{{ goodInfo.name }}
+                      </el-descriptions-item>
+                      <el-descriptions-item label="数量"
+                        >{{ goodInfo.count }}
+                      </el-descriptions-item>
+                    </el-descriptions>
+                  </div>
+                </template>
+
+                <!-- 包裹物流详情 -->
+                <template v-else-if="pkgChild.valueKey === 'wlxq'">
+                  <el-row :gutter="10">
+                    <el-col :offset="1" :span="6">
+                      <el-timeline>
+                        <el-timeline-item
+                          v-for="(activity, index) in pkgInfo[pkgChild.valueKey]"
+                          :key="index"
+                          :timestamp="activity.timestamp"
+                        >
+                          {{ activity.content }}
+                        </el-timeline-item>
+                      </el-timeline>
+                    </el-col>
+                  </el-row>
+                </template>
+                <template v-else>
+                  {{ pkgInfo[pkgChild.valueKey] }}
+                </template>
+              </el-descriptions-item>
+            </el-descriptions>
+          </div>
+        </el-descriptions-item>
+      </el-descriptions>
+    </div>
+  </ContentWrap>
+
+  <!-- 各种操作的弹窗 -->
+  <OrderDeliveryForm ref="deliveryFormRef" @success="getDetail" />
+  <OrderUpdateRemarkForm ref="updateRemarkForm" @success="getDetail" />
+  <OrderUpdateAddressForm ref="updateAddressFormRef" @success="getDetail" />
+  <OrderUpdatePriceForm ref="updatePriceFormRef" @success="getDetail" />
+</template>
+<script lang="ts" setup>
+import * as TradeOrderApi from '@/api/mall/trade/order'
+import { floatToFixed2 } from '@/utils'
+import { DICT_TYPE } from '@/utils/dict'
+import OrderUpdateRemarkForm from '@/views/mall/trade/order/components/OrderUpdateRemarkForm.vue'
+import OrderDeliveryForm from '@/views/mall/trade/order/components/OrderDeliveryForm.vue'
+import OrderUpdateAddressForm from '@/views/mall/trade/order/components/OrderUpdateAddressForm.vue'
+import OrderUpdatePriceForm from '@/views/mall/trade/order/components/OrderUpdatePriceForm.vue'
+
+defineOptions({ name: 'TradeOrderDetailForm' })
+
+const message = useMessage() // 消息弹窗
+const { params } = useRoute() // 查询参数
+const orderInfo = ref<TradeOrderApi.OrderVO>({})
+
+// TODO @puhui999:这个改成直接读属性,不用按照这种写法;后续再改
+const detailGroups = ref([
+  {
+    title: '物流信息',
+    key: 'expressInfo',
+    children: [
+      { label: '发货时间: ', valueKey: 'fhsj' },
+      { label: '物流公司: ', valueKey: 'wlgs' },
+      { label: '运单号: ', valueKey: 'ydh' },
+      { label: '物流状态: ', valueKey: 'wlzt', childProps: { span: 3 } },
+      { label: '物流详情: ', valueKey: 'wlxq' }
+    ]
+  },
+  {
+    title: '订单操作日志',
+    key: 'orderLog'
+  }
+])
+
+// TODO @puhui999:从后台读数据哈。后续再改
+const detailInfo = ref({
+  // 物流信息
+  expressInfo: {
+    label: '包裹1',
+    name: 'bg1',
+    fhsj: '2022-11-03 16:50:45',
+    wlgs: '极兔',
+    ydh: '2132123',
+    wlzt: '不支持此快递公司',
+    wlxq: [
+      {
+        content: '正在派送途中,请您准备签收(派件人:王涛,电话:13854563814)',
+        timestamp: '2018-04-15 15:00:16'
+      },
+      {
+        content: '快件到达 【烟台龙口东江村委营业点】',
+        timestamp: '2018-04-13 14:54:19'
+      },
+      {
+        content: '快件已发车',
+        timestamp: '2018-04-11 12:55:52'
+      },
+      {
+        content: '快件已发车',
+        timestamp: '2018-04-11 12:55:52'
+      },
+      {
+        content: '快件已发车',
+        timestamp: '2018-04-11 12:55:52'
+      }
+    ]
+  },
+  orderLog: [
+    // 订单操作日志
+    {
+      content: '买家【乌鸦】关闭了订单',
+      timestamp: '2018-04-15 15:00:16'
+    },
+    {
+      content: '买家【乌鸦】下单了',
+      timestamp: '2018-04-15 15:00:16'
+    }
+  ],
+  goodsInfo: [] // 商品详情tableData
+})
+
+const deliveryFormRef = ref() // 发货表单 Ref
+const updateRemarkForm = ref() // 订单备注表单 Ref
+const updateAddressFormRef = ref() // 收货地址表单 Ref
+const updatePriceFormRef = ref() // 订单调价表单 Ref
+const openForm = (type: string) => {
+  switch (type) {
+    case 'remark':
+      updateRemarkForm.value?.open(orderInfo.value)
+      break
+    case 'delivery':
+      deliveryFormRef.value?.open(orderInfo.value)
+      break
+    case 'updateAddress':
+      updateAddressFormRef.value?.open(orderInfo.value)
+      break
+    case 'updatePrice':
+      updatePriceFormRef.value?.open(orderInfo.value)
+      break
+  }
+}
+
+/** 获得详情 */
+const getDetail = async () => {
+  const id = params.orderId as unknown as number
+  if (id) {
+    const res = (await TradeOrderApi.getOrder(id)) as TradeOrderApi.OrderVO
+    orderInfo.value = res
+  }
+}
+
+onMounted(async () => {
+  await getDetail()
+})
+
+const clipboardSuccess = () => {
+  message.success('复制成功')
+}
+</script>
+<style lang="scss" scoped>
+:deep(.el-descriptions) {
+  &:not(:nth-child(1)) {
+    margin-top: 20px;
+  }
+
+  .el-descriptions__title {
+    display: flex;
+    align-items: center;
+
+    &::before {
+      display: inline-block;
+      width: 3px;
+      height: 20px;
+      margin-right: 10px;
+      background-color: #409eff;
+      content: '';
+    }
+  }
+
+  .el-descriptions-item__container {
+    margin: 0 10px;
+
+    .no-colon {
+      margin: 0;
+
+      &::after {
+        content: '';
+      }
+    }
+  }
+}
+</style>

+ 475 - 572
src/views/mall/trade/order/index.vue

@@ -1,572 +1,475 @@
-<template>
-  <!-- 搜索 -->
-  <ContentWrap>
-    <el-form
-      ref="queryFormRef"
-      :model="queryParams"
-      class="-mb-15px"
-      label-width="68px"
-      :inline="true"
-    >
-      <el-form-item label="订单状态" prop="status">
-        <el-select class="!w-280px" v-model="queryParams.status" clearable placeholder="全部">
-          <el-option
-            v-for="dict in getStrDictOptions(DICT_TYPE.TRADE_ORDER_STATUS)"
-            :key="dict.value as string"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="支付方式" prop="payChannelCode">
-        <el-select
-          v-model="queryParams.payChannelCode"
-          class="!w-280px"
-          clearable
-          placeholder="全部"
-        >
-          <el-option
-            v-for="dict in getStrDictOptions(DICT_TYPE.PAY_CHANNEL_CODE_TYPE)"
-            :key="dict.value as string"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="创建时间" prop="createTime">
-        <el-date-picker
-          v-model="queryParams.createTime"
-          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
-          class="!w-280px"
-          start-placeholder="自定义时间"
-          end-placeholder="自定义时间"
-          type="daterange"
-          value-format="YYYY-MM-DD HH:mm:ss"
-        />
-      </el-form-item>
-      <el-form-item label="订单来源" prop="terminal">
-        <el-select class="!w-280px" v-model="queryParams.terminal" clearable placeholder="全部">
-          <el-option
-            v-for="dict in getStrDictOptions(DICT_TYPE.TERMINAL)"
-            :key="dict.value as string"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-      <el-form-item label="订单类型" prop="type">
-        <el-select class="!w-280px" v-model="queryParams.type" clearable placeholder="全部">
-          <el-option
-            v-for="dict in getStrDictOptions(DICT_TYPE.TRADE_ORDER_TYPE)"
-            :key="dict.value as string"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
-      </el-form-item>
-
-      <el-form-item label="订单搜索">
-        <el-input
-          v-show="true"
-          class="!w-280px"
-          v-model="queryType.v"
-          clearable
-          placeholder="请输入"
-        >
-          <template #prepend>
-            <el-select style="width: 110px" v-model="queryType.k" clearable placeholder="全部">
-              <el-option
-                v-for="dict in searchList"
-                :key="dict.value"
-                :label="dict.label"
-                :value="dict.value"
-              />
-            </el-select>
-          </template>
-        </el-input>
-      </el-form-item>
-      <el-form-item>
-        <el-button @click="handleQuery" v-hasPermi="['trade:order:query']">
-          <Icon class="mr-5px" icon="ep:search" />
-          搜索
-        </el-button>
-        <el-button @click="resetQuery" v-hasPermi="['trade:order:query']">
-          <Icon class="mr-5px" icon="ep:refresh" />
-          重置
-        </el-button>
-        <el-button type="success" plain @click="handleExport" :loading="exportLoading">
-          <!--           v-hasPermi="['trade:order:export']"        -->
-          <Icon icon="ep:download" class="mr-5px" /> 导出TODO
-        </el-button>
-      </el-form-item>
-    </el-form>
-  </ContentWrap>
-  <!-- 表格 -->
-  <ContentWrap>
-    <!-- 表单 -->
-    <el-table v-loading="loading" :data="list">
-      <el-table-column type="expand" fixed="left">
-        <template #default="scope">
-          <el-descriptions class="mx-40">
-            <el-descriptions-item label="商品原价(总): ">{{
-              '¥ ' +
-              parseFloat((scope.row.originalPrice / 100) as unknown as string).toFixed(2) +
-              ' 元'
-            }}</el-descriptions-item>
-            <el-descriptions-item label="下单时间: ">
-              {{ formatDate(scope.row.createTime) }}</el-descriptions-item
-            >
-            <el-descriptions-item label="推广人: ">TODO</el-descriptions-item>
-            <el-descriptions-item label="用户备注: ">{{
-              scope.row.userRemark
-            }}</el-descriptions-item>
-            <el-descriptions-item label="商家备注: ">{{ scope.row.remark }}</el-descriptions-item>
-          </el-descriptions>
-        </template>
-      </el-table-column>
-      <el-table-column width="100" fixed="left">
-        <template #header>
-          <el-dropdown icon="eq:search" @command="handleDropType">
-            <el-button link type="primary">全选({{ orderSelect.selectTotal }}) </el-button>
-
-            <template #dropdown>
-              <el-dropdown-menu>
-                <el-dropdown-item command="1">当前页</el-dropdown-item>
-                <el-dropdown-item command="2">所有页</el-dropdown-item>
-              </el-dropdown-menu>
-            </template>
-          </el-dropdown>
-        </template>
-        <template #default="scope">
-          <el-checkbox v-model="scope.row.itemSelect" @change="handcheckclick(scope.row)" />
-        </template>
-      </el-table-column>
-
-      <el-table-column label="订单号" align="center" min-width="110">
-        <template #default="scope">
-          <el-button link type="primary" @click="showOrderDetail(scope.row)">{{
-            scope.row.no
-          }}</el-button>
-        </template>
-      </el-table-column>
-      <el-table-column label="订单类型" align="center" min-width="100">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.TRADE_ORDER_TYPE" :value="scope.row.type" />
-        </template>
-      </el-table-column>
-      <el-table-column label="用户信息" align="center" min-width="100">
-        <template #default="scope">
-          <el-button link type="primary" @click="goUserDetail(scope.row)"
-            >{{ scope.row.userId }}{{ '[' + scope.row.user.nickname + ']' }}</el-button
-          >
-        </template>
-      </el-table-column>
-      <el-table-column
-        :formatter="dateFormatter"
-        align="center"
-        label="创建时间"
-        prop="createTime"
-        min-width="180"
-      />
-      <el-table-column label="订单来源" align="center" min-width="100">
-        <template #default="scope">
-          <dict-tag
-            v-if="scope.row.terminal"
-            :type="DICT_TYPE.TERMINAL"
-            :value="scope.row.terminal"
-          />
-          <span v-else>{{ scope.terminal }}</span>
-        </template>
-      </el-table-column>
-
-      <el-table-column label="商品信息" align="left" min-width="200" prop="items">
-        <template #default="scope">
-          <el-popover
-            ref="popover"
-            placement="bottom"
-            :title="'订单:' + scope.row.no"
-            :width="400"
-            trigger="hover"
-          >
-            <template #reference>
-              <div>
-                <div v-for="item in scope.row.items" :key="item">
-                  <el-image
-                    style="width: 36px; height: 36px"
-                    :src="item.picUrl"
-                    :preview-src-list="[item.picUrl]"
-                    fit="cover"
-                    @click="imagePreview(item.picUrl)"
-                  />
-                  <span class="m-2">{{ item.spuName }}</span>
-                </div>
-              </div>
-            </template>
-            <div v-for="item in scope.row.items" :key="item">
-              <div>
-                <p>{{ item.spuName }}</p>
-                <!-- TODO xiaobai: 是不是 (item.payPrice / 100.0).toFixed(2) -->
-                <p>{{
-                  '¥ ' +
-                  parseFloat((item.payPrice / 100) as unknown as string).toFixed(2) +
-                  '元 x ' +
-                  item.count
-                }}</p>
-              </div>
-            </div>
-          </el-popover>
-        </template>
-      </el-table-column>
-      <el-table-column label="实际支付(元)" align="center" prop="payPrice" min-width="100">
-        <template #default="scope">
-          {{ '¥ ' + parseFloat((scope.row.payPrice / 100) as unknown as string).toFixed(2) }}
-        </template>
-      </el-table-column>
-      <el-table-column
-        :formatter="dateFormatter"
-        align="center"
-        label="支付时间"
-        prop="payTime"
-        min-width="180"
-      />
-      <el-table-column label="支付类型" align="center" min-width="100" prop="payChannelCode">
-        <template #default="scope">
-          <dict-tag
-            v-if="scope.row.payChannelCode"
-            :type="DICT_TYPE.PAY_CHANNEL_CODE_TYPE"
-            :value="scope.row.payChannelCode"
-          />
-        </template>
-      </el-table-column>
-      <el-table-column label="订单状态" align="center" prop="status" min-width="100">
-        <template #default="scope">
-          <dict-tag
-            v-if="scope.row.status !== ''"
-            :type="DICT_TYPE.TRADE_ORDER_STATUS"
-            :value="scope.row.status"
-          />
-          <span v-else>{{ scope.status }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="操作" align="center" fixed="right" min-width="150">
-        <template #default="scope">
-          <!-- <el-button v-if="scope.row.status == '0'" link type="primary" @click="sendXX(scope.row)"
-            >待支付</el-button> -->
-          <el-button v-if="scope.row.status == '10'" link type="primary" @click="sendXX(scope.row)"
-            >发货</el-button
-          >
-          <el-button link type="primary" @click="showOrderDetail(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>
-  <el-image-viewer
-    v-if="imgViewVisible"
-    :url-list="imageViewerList"
-    @close="imgViewVisible = false"
-  />
-</template>
-<script setup lang="ts" name="OrderList">
-import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
-import * as TradeOrderApi from '@/api/mall/trade/order'
-import {
-  TradeOrderPageReqVO,
-  SelectType,
-  TradeOrderPageItemRespVO
-} from '@/api/mall/trade/order/type/orderType'
-import { dateFormatter, formatDate } from '@/utils/formatTime'
-import download from '@/utils/download'
-
-const message = useMessage()
-const { push } = useRouter()
-const imgViewVisible = ref(false) // 商品图预览
-const imageViewerList = ref<string[]>([]) // 商品图预览列表
-const queryFormRef = ref()
-const loading = ref(false)
-const exportLoading = ref(false)
-const total = ref(0) // 总记录数
-const list = ref<Array<TradeOrderPageItemRespVO | any>>([]) //表数据
-
-// 选中状态选中处理
-const orderSelect: SelectType = reactive({
-  queryParams: {} as TradeOrderPageReqVO,
-  selectTotal: 0,
-  selectAllFlag: false,
-  selectData: new Map<number, Set<string>>(),
-  unSelectList: new Set<string>()
-})
-
-//表单搜索
-const queryParams: TradeOrderPageReqVO = reactive({
-  pageNo: 1, //首页
-  pageSize: 10 //页面大小
-})
-
-const queryType = reactive({ k: '', v: '' }) // 订单搜索类型kv
-
-/*
- * 订单搜索
- * 商品名称  商品件数 全部  需要后端支持TODO
- */
-const searchList = ref([
-  { value: 'no', label: '订单号' },
-  { value: 'userId', label: '用户UID' },
-  { value: 'userNickname', label: '用户昵称' },
-  { value: 'userMobile', label: '用户电话' },
-  { value: 'spuName', label: '商品名称TODO' },
-  { value: 'itemCount', label: '商品件数TODO' }
-])
-
-/**
-
- 当前页/? 如果pageNo存在,则将但前数据全部按照单个选中模式取消 ,不存在,则新增全页 增加 Map.pageNo Map.roderNoList
- 单个选中  如果pagelist存在,订单号选中状态取反,并对总数按选中状态加减。如果pagelist不存在,订单号选中状态取反,并对总数按选中状态加减,增加 Map.pageNo,
- 如果当前Map.pageNo 所对应list 为空 ,清除pageNo
- * @param command ===1 当前页 选中 ===2 所有页面选中
- */
-const handleDropType = (command: string) => {
-  let i = 0
-  //当前页按钮
-  if (command === '1') {
-    //如果该页面有选中数据 则选中事件触发时 取消该页面
-    if (orderSelect.selectData && orderSelect.selectData.has(queryParams.pageNo)) {
-      for (i = 0; i < list.value.length; i++) {
-        if (orderSelect.selectData.get(queryParams.pageNo)!.has(list.value[i].id)) {
-          //选中数量减少
-          orderSelect.selectTotal -= 1
-          //考虑全选中,针对某一页面选中当前页时 会将所有数据中去掉该页面, 需要登记到 orderSelect.unSelectList
-          unSelectListRecord(list.value[i].id, 'add')
-        }
-        list.value[i]['itemSelect'] = false
-      }
-      orderSelect.selectData.delete(queryParams.pageNo) //移除该页面
-    } else {
-      //当前页选中状态中 默认全选中
-      orderSelect.selectData.set(queryParams.pageNo, new Set<string>())
-      for (i = 0; i < list.value.length; i++) {
-        list.value[i]['itemSelect'] = true
-        orderSelect.selectData.get(queryParams.pageNo)!.add(list.value[i].id)
-        //选中数量增加
-        orderSelect.selectTotal += 1
-        //对于登记过取消状态中的数据排除
-        unSelectListRecord(list.value[i].id, 'del')
-      }
-    }
-  }
-  //所有页按钮
-  if (command === '2') {
-    orderSelect.selectAllFlag = !orderSelect.selectAllFlag
-
-    if (orderSelect.selectAllFlag) {
-      //打勾勾 //全选
-      orderSelect.selectData?.set(queryParams.pageNo, new Set<string>())
-      for (i = 0; i < list.value.length; i++) {
-        list.value[i]['itemSelect'] = true
-        orderSelect.selectData?.get(queryParams.pageNo)?.add(list.value[i].id) //id是主键不重复
-      }
-      orderSelect.selectTotal = total.value
-    } else {
-      //取消勾勾
-      for (i; i < list.value.length; i++) {
-        list.value[i]['itemSelect'] = false
-      }
-      initSelect() //重置之前选中的类容清空
-    }
-  }
-}
-
-//对全选状态中的 单选或者当前页面单选时登记取消的数据
-const unSelectListRecord = (id: string, op: string) => {
-  if (!orderSelect.selectAllFlag) {
-    return
-  }
-  if (op == 'add') {
-    orderSelect.unSelectList.add(id)
-  } else {
-    orderSelect.unSelectList.delete(id)
-  }
-}
-/***复选框选中 */
-const handcheckclick = (row: any) => {
-  if (row.itemSelect) {
-    orderSelect.selectTotal += 1
-    if (!orderSelect.selectData.has(queryParams.pageNo)) {
-      orderSelect.selectData?.set(queryParams.pageNo, new Set<string>())
-    }
-    orderSelect.selectData?.get(queryParams.pageNo)?.add(row.id)
-    unSelectListRecord(row.id, 'del')
-  } else {
-    orderSelect.selectTotal -= 1
-    orderSelect.selectData.get(queryParams.pageNo)?.delete(row.id)
-    unSelectListRecord(row.id, 'add')
-  }
-}
-/**
- * 导出数据
- */
-
-const handleExport = async () => {
-  try {
-    // 导出的二次确认
-    await message.exportConfirm()
-    //增加查询条件 用于全选时后台查询数据
-    orderSelect.queryParams = queryParams
-
-    // 发起导出
-    exportLoading.value = true
-    //全选时 根据上送的条件查询所有数据,在排除unseleectList 数据,
-    //非全选时, 根据上送的selectData 直接查询数据 后台实现导出数据接口即可
-    console.log(orderSelect)
-    download.excel(orderSelect as any, '订单信息.xls') //?
-  } catch {
-  } finally {
-    exportLoading.value = false
-  }
-  //TODO
-  exportLoading.value = false
-}
-
-/** 搜索按钮操作 */
-const handleQuery = () => {
-  getList()
-}
-
-/** 重置按钮操作 */
-const resetQuery = () => {
-  queryFormRef.value.resetFields()
-  queryType.v = '' //重置
-  queryType.k = ''
-  //休眠0.1s 等待watch响应
-  setTimeout(() => {
-    initSelect() //重置对选中设置恢复初始状态
-    handleQuery()
-  }, 100)
-}
-
-/**选中状态初始化**/
-const initSelect = () => {
-  orderSelect.queryParams = {} as TradeOrderPageReqVO
-  orderSelect.selectTotal = 0
-  orderSelect.selectAllFlag = false
-  orderSelect.selectData?.clear()
-  orderSelect.unSelectList?.clear()
-}
-
-const getList = async () => {
-  loading.value = true
-  try {
-    const data = await TradeOrderApi.getOrderList(queryParams)
-    list.value = data.list
-    total.value = data.total
-    let i = 0
-    if (orderSelect.selectData && orderSelect.selectData.has(queryParams.pageNo)) {
-      //该页面已经加载过了。直接按照之前状态设置选中状态值
-      for (i = 0; i < list.value.length; i++) {
-        if (orderSelect.selectData.get(queryParams.pageNo)!.has(list.value[i].id)) {
-          list.value[i]['itemSelect'] = true //之前已经选取过了
-        } else {
-          list.value[i]['itemSelect'] = false
-        }
-      }
-    } else if (orderSelect.selectAllFlag) {
-      //全选状态中 首次加载页面 默认全部选中
-      orderSelect.selectData.set(queryParams.pageNo, new Set<string>())
-      for (i = 0; i < list.value.length; i++) {
-        list.value[i]['itemSelect'] = true
-        orderSelect.selectData.get(queryParams.pageNo)!.add(list.value[i].id)
-      }
-    } else {
-      //非全选状态中  首次加载默认非选中状态
-      for (i; i < list.value.length; i++) {
-        list.value[i]['itemSelect'] = false //设置状态为未选中状态
-      }
-    }
-  } finally {
-    loading.value = false
-  }
-}
-
-/**
- * 跳转订单详情
- */
-const showOrderDetail = (row: any) => {
-  push({ name: 'TradeOrderDetail', query: { id: row.id } })
-}
-
-/**
- * 跳转用户详情
- */
-const goUserDetail = (row: any) => {
-  console.log('TODO User Detail: ' + row.userId)
-}
-/**
- * 发货
- */
-const sendXX = (row: any) => {
-  console.log('TODO Send XX: ' + row.no)
-}
-
-/**
- * 商品图预览
- * @param imgUrl
- */
-const imagePreview = (imgUrl: string) => {
-  imageViewerList.value = [imgUrl]
-  imgViewVisible.value = true
-}
-
-//针对订单搜索类型和值进行调整 使用监听器
-watch(
-  () => [queryType.k, queryType.v],
-  ([newK, newV], [oldK]) => {
-    //重置oldK对应得value
-    if (oldK != newK) {
-      if (oldK == 'no' && queryParams.no != '') {
-        queryParams.no = ''
-      } else if (oldK == 'userId' && queryParams.userId != '') {
-        queryParams.userId = ''
-      } else if (oldK == 'userNickname' && queryParams.userNickname != '') {
-        queryParams.userNickname = ''
-      } else if (oldK == 'userMobile' && queryParams.userMobile !== '') {
-        queryParams.userMobile = ''
-      } else if (oldK == 'spuName' && queryParams.spuName !== '') {
-        queryParams.spuName = ''
-      } else if (oldK == 'itemCount' && queryParams.itemCount !== '') {
-        queryParams.itemCount = ''
-      } else if (oldK == '' && queryParams.all !== '') {
-        queryParams.all = ''
-      }
-    }
-    // 根据选中得k设置Value
-    if (newK == 'no') {
-      queryParams.no = newV
-    } else if (newK == 'userId') {
-      queryParams.userId = newV
-    } else if (newK == 'userNickname') {
-      queryParams.userNickname = newV
-    } else if (newK == 'userMobile') {
-      queryParams.userMobile = newV
-    } else if (newK == 'spuName') {
-      queryParams.spuName = newV
-    } else if (newK == 'itemCount') {
-      queryParams.itemCount = newV
-    } else if (newK == '') {
-      queryParams.all = newV
-    }
-  }
-)
-
-/** 初始化 **/
-onMounted(() => {
-  initSelect()
-  getList()
-})
-</script>
+<template>
+  <!-- 搜索 -->
+  <ContentWrap>
+    <el-form
+      ref="queryFormRef"
+      :inline="true"
+      :model="queryParams"
+      class="-mb-15px"
+      label-width="68px"
+    >
+      <el-form-item label="订单状态" prop="status">
+        <el-select v-model="queryParams.status" class="!w-280px" clearable placeholder="全部">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.TRADE_ORDER_STATUS)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="支付方式" prop="payChannelCode">
+        <el-select
+          v-model="queryParams.payChannelCode"
+          class="!w-280px"
+          clearable
+          placeholder="全部"
+        >
+          <el-option
+            v-for="dict in getStrDictOptions(DICT_TYPE.PAY_CHANNEL_CODE)"
+            :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"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-280px"
+          end-placeholder="自定义时间"
+          start-placeholder="自定义时间"
+          type="daterange"
+          value-format="YYYY-MM-DD HH:mm:ss"
+        />
+      </el-form-item>
+      <el-form-item label="订单来源" prop="terminal">
+        <el-select v-model="queryParams.terminal" class="!w-280px" clearable placeholder="全部">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.TERMINAL)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="订单类型" prop="type">
+        <el-select v-model="queryParams.type" class="!w-280px" clearable placeholder="全部">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.TRADE_ORDER_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <!-- TODO @puhui999:要不加个 deliveryType 筛选;配送方式;然后如果选了快递,就有【快递公司】筛选;如果选了自提,就有【自提门店】;然后把他们这 3 个,坐在一个 el-form-item 里;
+        目的是;有的时候,会筛选门店,然后做核销;这个时候,就需要筛选自提门店;
+       -->
+      <el-form-item label="快递公司" prop="type">
+        <el-select v-model="queryParams.logisticsId" class="!w-280px" clearable placeholder="全部">
+          <el-option
+            v-for="item in deliveryExpressList"
+            :key="item.id"
+            :label="item.name"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="自提门店" prop="type">
+        <el-select
+          v-model="queryParams.pickUpStoreId"
+          class="!w-280px"
+          clearable
+          multiple
+          placeholder="全部"
+        >
+          <el-option
+            v-for="item in pickUpStoreList"
+            :key="item.id"
+            :label="item.name"
+            :value="item.id"
+          />
+        </el-select>
+      </el-form-item>
+      <!-- TODO 聚合搜索等售后结束后实现-->
+      <el-form-item label="聚合搜索">
+        <el-input
+          v-show="true"
+          v-model="queryType.v"
+          class="!w-280px"
+          clearable
+          placeholder="请输入"
+        >
+          <template #prepend>
+            <el-select v-model="queryType.k" class="!w-110px" clearable placeholder="全部">
+              <el-option
+                v-for="dict in searchList"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
+          </template>
+        </el-input>
+      </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="list">
+      <el-table-column class-name="order-table-col">
+        <template #header>
+          <!-- TODO @phui999:小屏幕下,会有偏移,后续看看 -->
+          <div class="flex items-center" style="width: 100%">
+            <div class="ml-100px mr-200px">商品信息</div>
+            <div class="mr-60px">单价(元)/数量</div>
+            <div class="mr-60px">售后状态</div>
+            <div class="mr-60px">实付金额(元)</div>
+            <div class="mr-60px">买家/收货人</div>
+            <div class="mr-60px">配送方式</div>
+            <div class="mr-60px">订单状态</div>
+            <div class="mr-60px">操作</div>
+          </div>
+        </template>
+        <template #default="scope">
+          <el-table
+            :data="scope.row.items"
+            :header-cell-style="headerStyle"
+            :span-method="spanMethod"
+            border
+            style="width: 100%"
+          >
+            <el-table-column min-width="300" prop="spuName">
+              <template #header>
+                <div
+                  class="flex items-center"
+                  style="height: 35px; background-color: #f7f7f7; width: 100%"
+                >
+                  <span class="mr-20px">订单号:{{ scope.row.no }} </span>
+                  <span class="mr-20px">下单时间:{{ formatDate(scope.row.createTime) }}</span>
+                  <span>订单来源:</span>
+                  <dict-tag
+                    :type="DICT_TYPE.TERMINAL"
+                    :value="scope.row.terminal"
+                    class="mr-20px"
+                  />
+                  <span>支付方式:</span>
+                  <dict-tag
+                    v-if="scope.row.payChannelCode"
+                    :type="DICT_TYPE.PAY_CHANNEL_CODE"
+                    :value="scope.row.payChannelCode"
+                    class="mr-20px"
+                  />
+                  <v-else class="mr-20px" v-else>未支付</v-else>
+                  <span class="mr-20px" v-if="scope.row.payTime">
+                    支付时间:{{ formatDate(scope.row.payTime) }}
+                  </span>
+                  <span>订单类型:</span>
+                  <dict-tag :type="DICT_TYPE.TRADE_ORDER_TYPE" :value="scope.row.type" />
+                </div>
+              </template>
+              <template #default="{ row }">
+                <div class="flex items-center">
+                  <el-image
+                    :src="row.picUrl"
+                    class="w-30px h-30px mr-10px"
+                    @click="imagePreview(row.picUrl)"
+                  />
+                  <span class="mr-10px">{{ row.spuName }}</span>
+                  <el-tag
+                    v-for="property in row.properties"
+                    :key="property.propertyId"
+                    class="mr-10px"
+                  >
+                    {{ property.propertyName }}: {{ property.valueName }}
+                  </el-tag>
+                </div>
+              </template>
+            </el-table-column>
+            <el-table-column label="商品原价*数量" prop="price" width="150">
+              <template #default="{ row }">
+                {{ floatToFixed2(row.price) }} 元 / {{ row.count }}
+              </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>
+            </el-table-column>
+            <el-table-column align="center" label="实际支付" min-width="120" prop="payPrice">
+              <template #default>
+                {{ floatToFixed2(scope.row.payPrice) + '元' }}
+              </template>
+            </el-table-column>
+            <el-table-column label="买家/收货人" min-width="160">
+              <template #default>
+                <!-- 快递发货  -->
+                <div v-if="scope.row.deliveryType === 1" class="flex flex-col">
+                  <span>买家:{{ scope.row.user.nickname }}</span>
+                  <span>
+                    收货人:{{ scope.row.receiverName }} {{ scope.row.receiverMobile }}
+                    {{ scope.row.receiverAreaName }} {{ scope.row.receiverDetailAddress }}
+                  </span>
+                </div>
+                <!-- 自提  -->
+                <div v-if="scope.row.deliveryType === 2" class="flex flex-col">
+                  <span>
+                    门店名称:
+                    {{ pickUpStoreList.find((p) => p.id === scope.row.pickUpStoreId)?.name }}
+                  </span>
+                  <span>
+                    门店手机:
+                    {{ pickUpStoreList.find((p) => p.id === scope.row.pickUpStoreId)?.phone }}
+                  </span>
+                  <span>
+                    自提门店:
+                    {{
+                      pickUpStoreList.find((p) => p.id === scope.row.pickUpStoreId)?.detailAddress
+                    }}
+                  </span>
+                </div>
+              </template>
+            </el-table-column>
+            <el-table-column align="center" label="配送方式" width="120">
+              <template #default>
+                <dict-tag :type="DICT_TYPE.TRADE_DELIVERY_TYPE" :value="scope.row.deliveryType" />
+              </template>
+            </el-table-column>
+            <el-table-column align="center" label="订单状态" width="120">
+              <template #default>
+                <dict-tag :type="DICT_TYPE.TRADE_ORDER_STATUS" :value="scope.row.status" />
+              </template>
+            </el-table-column>
+            <el-table-column align="center" fixed="right" label="操作" width="160">
+              <template #default>
+                <!-- TODO 权限后续补齐 -->
+                <div class="flex justify-center items-center">
+                  <el-button link type="primary" @click="openForm(scope.row.id)">
+                    <Icon icon="ep:notification" />
+                    详情
+                  </el-button>
+                  <el-dropdown @command="(command) => handleCommand(command, scope.row)">
+                    <el-button link type="primary">
+                      <Icon icon="ep:d-arrow-right" />
+                      更多
+                    </el-button>
+                    <template #dropdown>
+                      <el-dropdown-menu>
+                        <!-- 如果是【快递】,并且【未发货】,则展示【发货】按钮 -->
+                        <el-dropdown-item
+                          v-if="scope.row.deliveryType === 1 && scope.row.status === 10"
+                          command="delivery"
+                        >
+                          <Icon icon="ep:takeaway-box" />
+                          发货
+                        </el-dropdown-item>
+                        <el-dropdown-item command="remark">
+                          <Icon icon="ep:chat-line-square" /> 备注
+                        </el-dropdown-item>
+                      </el-dropdown-menu>
+                    </template>
+                  </el-dropdown>
+                </div>
+              </template>
+            </el-table-column>
+          </el-table>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      v-model:limit="queryParams.pageSize"
+      v-model:page="queryParams.pageNo"
+      :total="total"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 各种操作的弹窗 -->
+  <OrderDeliveryForm ref="deliveryFormRef" @success="getList" />
+  <OrderUpdateRemarkForm ref="updateRemarkForm" @success="getList" />
+</template>
+
+<script lang="ts" name="Order" setup>
+import type { FormInstance, TableColumnCtx } from 'element-plus'
+import OrderDeliveryForm from './components/OrderDeliveryForm.vue'
+import OrderUpdateRemarkForm from './components/OrderUpdateRemarkForm.vue'
+import * as TradeOrderApi from '@/api/mall/trade/order'
+import * as PickUpStoreApi from '@/api/mall/trade/delivery/pickUpStore'
+import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+import { floatToFixed2 } from '@/utils'
+import { createImageViewer } from '@/components/ImageViewer'
+import * as DeliveryExpressApi from '@/api/mall/trade/delivery/express'
+
+const { currentRoute, push } = useRouter() // 路由跳转
+
+const loading = ref(true) // 列表的加载中
+const total = ref(2) // 列表的总页数
+const list = ref<TradeOrderApi.OrderVO[]>([]) // 列表的数据
+const queryFormRef = ref<FormInstance>() // 搜索的表单
+// 表单搜索
+const queryParams = reactive({
+  pageNo: 1, //首页
+  pageSize: 10, //页面大小
+  no: '',
+  userId: '',
+  userNickname: '',
+  userMobile: '',
+  receiverName: '',
+  receiverMobile: '',
+  terminal: '',
+  type: null,
+  status: null,
+  payChannelCode: '',
+  createTime: [],
+  spuName: '',
+  itemCount: '',
+  pickUpStoreId: [],
+  logisticsId: null,
+  all: ''
+})
+const queryType = reactive({ k: '', v: '' }) // 订单搜索类型kv
+/**
+ * 订单聚合搜索
+ * 商品名称、商品件数、全部
+ *
+ * 需要后端支持 TODO
+ */
+const searchList = ref([
+  { value: 'no', label: '订单号' },
+  { value: 'userId', label: '用户UID' },
+  { value: 'userNickname', label: '用户昵称' },
+  { value: 'userMobile', label: '用户电话' }
+])
+
+const headerStyle = ({ row, columnIndex }: any) => {
+  // 表头第一行第一列占 8
+  if (columnIndex === 0) {
+    row[columnIndex].colSpan = 8
+  } else {
+    // 其余的不要
+    row[columnIndex].colSpan = 0
+    return {
+      display: 'none'
+    }
+  }
+}
+
+interface SpanMethodProps {
+  row: TradeOrderApi.OrderItemRespVO
+  column: TableColumnCtx<TradeOrderApi.OrderItemRespVO>
+  rowIndex: number
+  columnIndex: number
+}
+
+const spanMethod = ({ row, rowIndex, columnIndex }: SpanMethodProps) => {
+  const len = list.value.find(
+    (order) => order.items?.findIndex((item) => item.id === row.id) !== -1
+  )?.items?.length
+  // 要合并的列,从零开始
+  const colIndex = [3, 4, 5, 6]
+  if (colIndex.includes(columnIndex)) {
+    // 除了第一行其余的不要
+    if (rowIndex !== 0) {
+      return {
+        rowspan: 0,
+        colspan: 0
+      }
+    }
+    // 动态合并行
+    return {
+      rowspan: len,
+      colspan: 1
+    }
+  }
+}
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await TradeOrderApi.getOrderPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = async () => {
+  queryParams.pageNo = 1
+  await getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+
+/** 商品图预览 */
+const imagePreview = (imgUrl: string) => {
+  createImageViewer({
+    urlList: [imgUrl]
+  })
+}
+
+/** 查看订单详情 */
+const openForm = (id: number) => {
+  push({ name: 'TradeOrderDetailForm', params: { orderId: id } })
+}
+
+/** 操作分发 */
+const deliveryFormRef = ref()
+const updateRemarkForm = ref()
+const handleCommand = (command: string, row: TradeOrderApi.OrderVO) => {
+  switch (command) {
+    case 'remark':
+      updateRemarkForm.value?.open(row)
+      break
+    case 'delivery':
+      deliveryFormRef.value?.open(row)
+      break
+  }
+}
+
+// 监听路由变化更新列表,解决订单保存/更新后,列表不刷新的问题。
+watch(
+  () => currentRoute.value,
+  () => {
+    getList()
+  }
+)
+
+const pickUpStoreList = ref([]) // 自提门店精简列表
+const deliveryExpressList = ref([]) // 物流公司
+/** 初始化 **/
+onMounted(async () => {
+  await getList()
+  pickUpStoreList.value = await PickUpStoreApi.getListAllSimple()
+  deliveryExpressList.value = await DeliveryExpressApi.getSimpleDeliveryExpressList()
+})
+</script>
+<style lang="scss" scoped>
+:deep(.order-table-col > .cell) {
+  padding: 0;
+}
+</style>

+ 112 - 0
src/views/member/group/GroupForm.vue

@@ -0,0 +1,112 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible" width="600">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="名称" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入名称" />
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+        <el-radio-group v-model="formData.status">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="备注" prop="remark">
+        <el-input v-model="formData.remark" type="textarea" placeholder="请输入备注" />
+      </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 { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as GroupApi from '@/api/member/group'
+import { CommonStatusEnum } from '@/utils/constants'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  name: undefined,
+  remark: undefined,
+  status: CommonStatusEnum.ENABLE
+})
+const formRules = reactive({
+  name: [{ required: true, message: '名称不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await GroupApi.getGroup(id)
+    } 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
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as GroupApi.GroupVO
+    if (formType.value === 'create') {
+      await GroupApi.createGroup(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await GroupApi.updateGroup(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: undefined,
+    remark: undefined,
+    status: CommonStatusEnum.ENABLE
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 45 - 0
src/views/member/group/components/MemberGroupSelect.vue

@@ -0,0 +1,45 @@
+<template>
+  <el-select v-model="groupId" placeholder="请选择用户分组" clearable class="!w-240px">
+    <el-option
+      v-for="group in groupOptions"
+      :key="group.id"
+      :label="group.name"
+      :value="group.id"
+    />
+  </el-select>
+</template>
+<script lang="ts" setup>
+import * as GroupApi from '@/api/member/group'
+
+/** 会员分组选择框 **/
+defineOptions({ name: 'MemberGroupSelect' })
+
+const props = defineProps({
+  /** 下拉框选中值 **/
+  modelValue: {
+    type: Number,
+    default: undefined
+  }
+})
+const emit = defineEmits(['update:modelValue'])
+
+const groupId = computed({
+  get() {
+    return props.modelValue
+  },
+  set(value: any) {
+    emit('update:modelValue', value)
+  }
+})
+
+const groupOptions = ref<GroupApi.GroupVO[]>([])
+
+const getList = async () => {
+  groupOptions.value = await GroupApi.getSimpleGroupList()
+}
+
+/** 初始化 */
+onMounted(() => {
+  getList()
+})
+</script>

+ 174 - 0
src/views/member/group/index.vue

@@ -0,0 +1,174 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <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 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" @click="openForm('create')" v-hasPermi="['member:group:create']">
+          <Icon icon="ep:plus" 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="60" />
+      <el-table-column label="名称" align="center" prop="name" min-width="80" />
+      <el-table-column label="备注" align="center" prop="remark" min-width="100" />
+      <el-table-column label="状态" align="center" prop="status" min-width="70">
+        <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"
+        min-width="170"
+      />
+      <el-table-column label="操作" align="center" width="150px">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['member:group:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['member:group: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>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <GroupForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as GroupApi from '@/api/member/group'
+import GroupForm from './GroupForm.vue'
+
+/** 用户分组管理 **/
+defineOptions({ name: 'MemberGroup' })
+
+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,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await GroupApi.getGroupPage(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 openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await GroupApi.deleteGroup(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 175 - 0
src/views/member/level/LevelForm.vue

@@ -0,0 +1,175 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible" width="800">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="110px"
+      v-loading="formLoading"
+    >
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="等级名称" prop="name">
+            <el-input v-model="formData.name" placeholder="请输入等级名称" class="!w-240px" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="等级" prop="level">
+            <el-input-number
+              v-model="formData.level"
+              :min="0"
+              :precision="0"
+              placeholder="请输入等级"
+              class="!w-240px"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="升级经验" prop="experience">
+            <el-input-number
+              v-model="formData.experience"
+              :min="0"
+              :precision="0"
+              placeholder="请输入升级经验"
+              class="!w-240px"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="享受折扣(%)" prop="discountPercent">
+            <el-input-number
+              v-model="formData.discountPercent"
+              :min="0"
+              :max="100"
+              :precision="0"
+              placeholder="请输入享受折扣"
+              class="!w-240px"
+            />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row>
+        <el-col :span="12">
+          <el-form-item label="等级图标">
+            <UploadImg v-model="formData.icon" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="背景图">
+            <UploadImg v-model="formData.backgroundUrl" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-form-item label="状态" prop="status">
+        <el-radio-group v-model="formData.status">
+          <el-radio
+            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </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 { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as LevelApi from '@/api/member/level'
+import { CommonStatusEnum } from '@/utils/constants'
+
+/** 会员等级表单 **/
+defineOptions({ name: 'MemberLevelForm' })
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  name: undefined,
+  experience: undefined,
+  level: undefined,
+  discountPercent: undefined,
+  icon: undefined,
+  backgroundUrl: undefined,
+  status: CommonStatusEnum.ENABLE
+})
+const formRules = reactive({
+  name: [{ required: true, message: '等级名称不能为空', trigger: 'blur' }],
+  experience: [{ required: true, message: '升级经验不能为空', trigger: 'blur' }],
+  level: [{ required: true, message: '等级不能为空', trigger: 'blur' }],
+  discountPercent: [{ required: true, message: '享受折扣不能为空', trigger: 'blur' }],
+  status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await LevelApi.getLevel(id)
+    } 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
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as LevelApi.LevelVO
+    if (formType.value === 'create') {
+      await LevelApi.createLevel(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await LevelApi.updateLevel(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: undefined,
+    experience: undefined,
+    level: undefined,
+    discountPercent: undefined,
+    icon: undefined,
+    backgroundUrl: undefined,
+    status: CommonStatusEnum.ENABLE
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 45 - 0
src/views/member/level/components/MemberLevelSelect.vue

@@ -0,0 +1,45 @@
+<template>
+  <el-select v-model="levelId" placeholder="请选择用户等级" clearable class="!w-240px">
+    <el-option v-for="level in levelOptions" :key="level.id" :label="level.name" :value="level.id">
+      <span class="flex items-center gap-x-8px">
+        <el-avatar :src="level.icon" size="small" />
+        {{ level.name }}
+      </span>
+    </el-option>
+  </el-select>
+</template>
+<script lang="ts" setup>
+import * as LevelApi from '@/api/member/level'
+
+/** 会员等级选择框 **/
+defineOptions({ name: 'MemberLevelSelect' })
+
+const props = defineProps({
+  /** 下拉框选中值 **/
+  modelValue: {
+    type: Number,
+    default: undefined
+  }
+})
+const emit = defineEmits(['update:modelValue'])
+
+const levelId = computed({
+  get() {
+    return props.modelValue
+  },
+  set(value: any) {
+    emit('update:modelValue', value)
+  }
+})
+
+const levelOptions = ref<LevelApi.LevelVO[]>([])
+
+const getList = async () => {
+  levelOptions.value = await LevelApi.getSimpleLevelList()
+}
+
+/** 初始化 */
+onMounted(() => {
+  getList()
+})
+</script>

+ 169 - 0
src/views/member/level/index.vue

@@ -0,0 +1,169 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <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 type="primary" @click="openForm('create')" v-hasPermi="['member:level:create']">
+          <Icon icon="ep:plus" 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="60" />
+      <el-table-column label="等级图标" align="center" prop="icon" min-width="80">
+        <template #default="scope">
+          <el-image
+            :src="scope.row.icon"
+            class="w-30px h-30px"
+            :preview-src-list="[scope.row.icon]"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column label="等级背景图" align="center" prop="backgroundUrl" min-width="100">
+        <template #default="scope">
+          <el-image
+            :src="scope.row.backgroundUrl"
+            class="w-30px h-30px"
+            :preview-src-list="[scope.row.backgroundUrl]"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column label="等级名称" align="center" prop="name" min-width="100" />
+      <el-table-column label="等级" align="center" prop="level" min-width="60" />
+      <el-table-column label="升级经验" align="center" prop="experience" min-width="80" />
+      <el-table-column label="享受折扣(%)" align="center" prop="discountPercent" min-width="110" />
+      <el-table-column label="状态" align="center" prop="status" min-width="70">
+        <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"
+        min-width="170"
+      />
+      <el-table-column label="操作" align="center" min-width="110px" fixed="right">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['member:level:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['member:level:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <LevelForm ref="formRef" @success="getList" />
+</template>
+
+<script setup lang="ts">
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as LevelApi from '@/api/member/level'
+import LevelForm from './LevelForm.vue'
+
+/** 会员等级管理 **/
+defineOptions({ name: 'MemberLevel' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref([]) // 列表的数据
+const queryParams = reactive({
+  name: null,
+  status: null
+})
+const queryFormRef = ref() // 搜索的表单
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    list.value = await LevelApi.getLevelList(queryParams)
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await LevelApi.deleteLevel(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 1 - 2
src/views/member/tag/components/MemberTagSelect.vue

@@ -52,8 +52,7 @@ const tagIds = computed({
 const tags = ref<TagApi.TagVO[]>([])
 
 const getList = async () => {
-  const data = await TagApi.getMemberTagPage({})
-  tags.value = data.list
+  tags.value = await TagApi.getSimpleTagList()
 }
 
 /** 添加用户标签表单弹框 */

+ 101 - 0
src/views/member/user/UpdateLevelForm.vue

@@ -0,0 +1,101 @@
+<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" placeholder="请输入用户昵称" class="!w-240px" disabled />
+      </el-form-item>
+      <el-form-item label="用户昵称" prop="nickname">
+        <el-input
+          v-model="formData.nickname"
+          placeholder="请输入用户昵称"
+          class="!w-240px"
+          disabled
+        />
+      </el-form-item>
+      <el-form-item label="用户等级" prop="levelId">
+        <MemberLevelSelect v-model="formData.levelId" />
+      </el-form-item>
+      <el-form-item label="修改原因" prop="reason">
+        <el-input type="textarea" v-model="formData.reason" placeholder="请输入修改原因" />
+      </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'
+import MemberLevelSelect from '@/views/member/level/components/MemberLevelSelect.vue'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formData = ref({
+  id: undefined,
+  nickname: undefined,
+  levelId: undefined,
+  reason: undefined
+})
+const formRules = reactive({
+  reason: [{ 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)
+    } 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
+  // 提交请求
+  formLoading.value = true
+  try {
+    await UserApi.updateUserLevel(formData.value)
+
+    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()
+}
+</script>

+ 9 - 6
src/views/member/user/UserForm.vue

@@ -60,6 +60,9 @@
       <el-form-item label="用户标签" prop="tagIds">
         <MemberTagSelect v-model="formData.tagIds" show-add />
       </el-form-item>
+      <el-form-item label="用户分组" prop="groupId">
+        <MemberGroupSelect v-model="formData.groupId" />
+      </el-form-item>
       <el-form-item label="会员备注" prop="mark">
         <el-input type="textarea" v-model="formData.mark" placeholder="请输入会员备注" />
       </el-form-item>
@@ -76,6 +79,7 @@ import * as UserApi from '@/api/member/user'
 import * as AreaApi from '@/api/system/area'
 import { defaultProps } from '@/utils/tree'
 import MemberTagSelect from '@/views/member/tag/components/MemberTagSelect.vue'
+import MemberGroupSelect from '@/views/member/group/components/MemberGroupSelect.vue'
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
@@ -87,6 +91,7 @@ const formType = ref('') // 表单的类型:create - 新增;update - 修改
 const formData = ref({
   id: undefined,
   mobile: undefined,
+  password: undefined,
   status: undefined,
   nickname: undefined,
   avatar: undefined,
@@ -95,7 +100,8 @@ const formData = ref({
   areaId: undefined,
   birthday: undefined,
   mark: undefined,
-  tagIds: []
+  tagIds: [],
+  groupId: undefined
 })
 const formRules = reactive({
   mobile: [{ required: true, message: '手机号不能为空', trigger: 'blur' }],
@@ -158,9 +164,6 @@ const resetForm = () => {
     mobile: undefined,
     password: undefined,
     status: undefined,
-    registerIp: undefined,
-    loginIp: undefined,
-    loginDate: undefined,
     nickname: undefined,
     avatar: undefined,
     name: undefined,
@@ -168,8 +171,8 @@ const resetForm = () => {
     areaId: undefined,
     birthday: undefined,
     mark: undefined,
-    createTime: undefined,
-    tagIds: []
+    tagIds: [],
+    groupId: undefined
   }
   formRef.value?.resetFields()
 }

+ 0 - 43
src/views/member/user/components/account-info.vue

@@ -1,43 +0,0 @@
-<template>
-  <el-descriptions :column="2">
-    <el-descriptions-item>
-      <template #label>
-        <div class="cell-item"> 储值余额 </div>
-      </template>
-      {{ 0 }}
-    </el-descriptions-item>
-    <el-descriptions-item>
-      <template #label>
-        <div class="cell-item"> 现金余额 </div>
-      </template>
-      {{ 0 }}
-    </el-descriptions-item>
-    <el-descriptions-item>
-      <template #label>
-        <div class="cell-item"> 积分 </div>
-      </template>
-      {{ 0 }}
-    </el-descriptions-item>
-    <el-descriptions-item>
-      <template #label>
-        <div class="cell-item"> 成长值 </div>
-      </template>
-      {{ 0 }}
-    </el-descriptions-item>
-  </el-descriptions>
-</template>
-<script lang="ts" setup>
-import { defineComponent } from 'vue'
-
-defineComponent({
-  name: 'AccountInfo'
-})
-</script>
-<style scoped lang="scss">
-.cell-item {
-  display: inline;
-}
-.cell-item::after {
-  content: ':';
-}
-</style>

+ 0 - 13
src/views/member/user/components/address-list.vue

@@ -1,13 +0,0 @@
-<script lang="ts">
-import { defineComponent } from 'vue'
-
-export default defineComponent({
-  name: 'AddressList'
-})
-</script>
-
-<template>
-  <div>收货地址列表</div>
-</template>
-
-<style scoped lang="scss"></style>

+ 1 - 0
src/views/member/user/components/balance-list.vue

@@ -6,6 +6,7 @@ export default defineComponent({
 })
 </script>
 
+<!-- TODO @芋艿:未来实现,等周建的 -->
 <template>
   <div>余额列表</div>
 </template>

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

@@ -6,6 +6,7 @@ export default defineComponent({
 })
 </script>
 
+<!-- TODO @梦:可以读取 member_experience_log 表 -->
 <template>
   <div>成长值列表</div>
 </template>

+ 60 - 0
src/views/member/user/detail/UserAccountInfo.vue

@@ -0,0 +1,60 @@
+<template>
+  <el-descriptions :column="2">
+    <el-descriptions-item>
+      <template #label>
+        <descriptions-item-label label=" 等级 " icon="svg-icon:member_level" />
+      </template>
+      {{ user.levelName || '无' }}
+    </el-descriptions-item>
+    <el-descriptions-item>
+      <template #label>
+        <descriptions-item-label label=" 成长值 " icon="ep:suitcase" />
+      </template>
+      {{ user.experience || 0 }}
+    </el-descriptions-item>
+    <el-descriptions-item>
+      <template #label>
+        <descriptions-item-label label=" 当前积分 " icon="ep:coin" />
+      </template>
+      {{ user.point || 0 }}
+    </el-descriptions-item>
+    <el-descriptions-item>
+      <template #label>
+        <descriptions-item-label label=" 总积分 " icon="ep:coin" />
+      </template>
+      {{ user.totalPoint || 0 }}
+    </el-descriptions-item>
+    <!-- TODO 芋艿:后续接入余额、支付金额 -->
+    <el-descriptions-item>
+      <template #label>
+        <descriptions-item-label label=" 当前余额 " icon="svg-icon:member_balance" />
+      </template>
+      {{ 0 }}
+    </el-descriptions-item>
+    <el-descriptions-item>
+      <template #label>
+        <descriptions-item-label label=" 支出金额 " icon="svg-icon:member_expenditure_balance" />
+      </template>
+      {{ 0 }}
+    </el-descriptions-item>
+    <el-descriptions-item>
+      <template #label>
+        <descriptions-item-label label=" 充值金额 " icon="svg-icon:member_recharge_balance" />
+      </template>
+      {{ 0 }}
+    </el-descriptions-item>
+  </el-descriptions>
+</template>
+<script setup lang="ts">
+import { DescriptionsItemLabel } from '@/components/Descriptions'
+import * as UserApi from '@/api/member/user'
+const { user } = defineProps<{ user: UserApi.UserVO }>()
+</script>
+<style scoped lang="scss">
+.cell-item {
+  display: inline;
+}
+.cell-item::after {
+  content: ':';
+}
+</style>

+ 54 - 0
src/views/member/user/detail/UserAddressList.vue

@@ -0,0 +1,54 @@
+<template>
+  <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+    <el-table-column label="地址编号" align="center" prop="id" width="150px" />
+    <el-table-column label="收件人名称" align="center" prop="name" width="150px" />
+    <el-table-column label="手机号" align="center" prop="mobile" width="150px" />
+    <el-table-column label="地区编码" align="center" prop="areaId" width="150px" />
+    <el-table-column label="收件详细地址" align="center" prop="detailAddress" />
+    <el-table-column label="是否默认" align="center" prop="defaultStatus" width="150px">
+      <template #default="scope">
+        <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="Number(scope.row.defaultStatus)" />
+      </template>
+    </el-table-column>
+    <el-table-column
+      label="创建时间"
+      align="center"
+      prop="createTime"
+      :formatter="dateFormatter"
+      width="180px"
+    />
+  </el-table>
+</template>
+<script lang="ts" setup>
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as AddressApi from '@/api/member/address'
+
+const { userId }: { userId: number } = defineProps({
+  userId: {
+    type: Number,
+    required: true
+  }
+})
+
+const loading = ref(true) // 列表的加载中
+const total = ref(0) // 列表的总页数
+const list = ref([]) // 列表的数据
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    list.value = await AddressApi.getAddressList({ userId })
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>
+
+<style scoped lang="scss"></style>

+ 85 - 0
src/views/member/user/detail/UserBasicInfo.vue

@@ -0,0 +1,85 @@
+<template>
+  <el-card shadow="never">
+    <template #header>
+      <slot name="header"></slot>
+    </template>
+    <el-row>
+      <el-col :span="4">
+        <ElAvatar shape="square" :size="140" :src="user.avatar || undefined" />
+      </el-col>
+      <el-col :span="20">
+        <el-descriptions :column="2">
+          <el-descriptions-item>
+            <template #label>
+              <descriptions-item-label label="用户名" icon="ep:user" />
+            </template>
+            {{ user.name || '空' }}
+          </el-descriptions-item>
+          <el-descriptions-item>
+            <template #label>
+              <descriptions-item-label label="昵称" icon="ep:user" />
+            </template>
+            {{ user.nickname }}
+          </el-descriptions-item>
+          <el-descriptions-item label="手机号">
+            <template #label>
+              <descriptions-item-label label="手机号" icon="ep:phone" />
+            </template>
+            {{ user.mobile }}
+          </el-descriptions-item>
+          <el-descriptions-item>
+            <template #label>
+              <descriptions-item-label label="性别" icon="fa:mars-double" />
+            </template>
+            <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="user.sex" />
+          </el-descriptions-item>
+          <el-descriptions-item>
+            <template #label>
+              <descriptions-item-label label="所在地" icon="ep:location" />
+            </template>
+            {{ user.areaName }}
+          </el-descriptions-item>
+          <el-descriptions-item>
+            <template #label>
+              <descriptions-item-label label="注册 IP" icon="ep:position" />
+            </template>
+            {{ user.registerIp }}
+          </el-descriptions-item>
+          <el-descriptions-item>
+            <template #label>
+              <descriptions-item-label label="生日" icon="fa:birthday-cake" />
+            </template>
+            {{ user.birthday ? formatDate(user.birthday) : '空' }}
+          </el-descriptions-item>
+          <el-descriptions-item>
+            <template #label>
+              <descriptions-item-label label="注册时间" icon="ep:calendar" />
+            </template>
+            {{ user.createTime ? formatDate(user.createTime) : '空' }}
+          </el-descriptions-item>
+          <el-descriptions-item>
+            <template #label>
+              <descriptions-item-label label="最后登录时间" icon="ep:calendar" />
+            </template>
+            {{ user.loginDate ? formatDate(user.loginDate) : '空' }}
+          </el-descriptions-item>
+        </el-descriptions>
+      </el-col>
+    </el-row>
+  </el-card>
+</template>
+<script setup lang="ts">
+import { DICT_TYPE } from '@/utils/dict'
+import { formatDate } from '@/utils/formatTime'
+import * as UserApi from '@/api/member/user'
+import { DescriptionsItemLabel } from '@/components/Descriptions/index'
+
+const { user } = defineProps<{ user: UserApi.UserVO }>()
+</script>
+<style scoped lang="scss">
+.card-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+</style>

+ 8 - 9
src/views/member/user/components/point-list.vue → src/views/member/user/detail/UserPointList.vue

@@ -67,7 +67,6 @@
         :formatter="dateFormatter"
         width="180"
       />
-      <el-table-column label="用户" align="center" prop="nickname" width="200" />
       <el-table-column label="获得积分" align="center" prop="point" width="100">
         <template #default="scope">
           <el-tag v-if="scope.row.point > 0" class="ml-2" type="success" effect="dark">
@@ -99,20 +98,18 @@
 <script lang="ts" setup>
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
-import * as RecordApi from '@/api/member/point/record'
-import { RecordQueryVO } from '@/api/member/point/record'
+import * as RecordApi from '@/api//member/point/record'
 
-defineOptions({ name: 'PointList' })
 const loading = ref(true) // 列表的加载中
 const total = ref(0) // 列表的总页数
 const list = ref([]) // 列表的数据
-const queryParams = reactive<RecordQueryVO>({
+const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
   bizType: undefined,
   title: null,
   createDate: [],
-  userId: null
+  userId: NaN
 })
 const queryFormRef = ref() // 搜索的表单
 
@@ -139,15 +136,17 @@ const resetQuery = () => {
   queryFormRef.value.resetFields()
   handleQuery()
 }
-const { memberId } = defineProps({
-  memberId: {
+
+const { userId } = defineProps({
+  userId: {
     type: Number,
     required: true
   }
 })
+
 /** 初始化 **/
 onMounted(() => {
-  queryParams.userId = memberId
+  queryParams.userId = userId
   getList()
 })
 </script>

+ 7 - 8
src/views/member/user/components/sign-list.vue → src/views/member/user/detail/UserSignList.vue

@@ -48,7 +48,6 @@
   <ContentWrap>
     <el-table v-loading="loading" :data="list">
       <el-table-column label="编号" align="center" prop="id" />
-      <el-table-column label="签到用户" align="center" prop="nickname" />
       <el-table-column
         label="签到天数"
         align="center"
@@ -83,16 +82,14 @@
 <script lang="ts" setup>
 import { dateFormatter } from '@/utils/formatTime'
 import * as SignInRecordApi from '@/api/member/signin/record'
-import { SignInRecordQueryVO } from '@/api/member/signin/record'
-
-defineOptions({ name: 'SignList' })
 
 const loading = ref(true) // 列表的加载中
 const total = ref(0) // 列表的总页数
 const list = ref([]) // 列表的数据
-const queryParams = reactive<SignInRecordQueryVO>({
+const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
+  userId: NaN,
   nickname: null,
   day: null,
   createTime: []
@@ -122,15 +119,17 @@ const resetQuery = () => {
   queryFormRef.value.resetFields()
   handleQuery()
 }
-const { memberId } = defineProps({
-  memberId: {
+
+const { userId } = defineProps({
+  userId: {
     type: Number,
     required: true
   }
 })
+
 /** 初始化 **/
 onMounted(() => {
-  queryParams.userId = memberId
+  queryParams.userId = userId
   getList()
 })
 </script>

+ 51 - 152
src/views/member/user/detail/index.vue

@@ -1,200 +1,105 @@
 <template>
   <div v-loading="loading">
-    <el-row :gutter="10" class="detail-info-warp">
+    <el-row :gutter="10">
+      <!-- 左上角:基本信息 -->
       <el-col :span="14" class="detail-info-item">
-        <el-card shadow="never">
+        <UserBasicInfo :user="user">
           <template #header>
             <div class="card-header">
               <CardTitle title="基本信息" />
-              <el-button
-                v-if="userInfo.id"
-                type="primary"
-                text
-                @click="openForm('update', userInfo.id)"
-                >编辑</el-button
-              >
+              <el-button type="primary" size="small" text @click="openForm('update')">
+                编辑
+              </el-button>
             </div>
           </template>
-          <el-row>
-            <el-col :span="4">
-              <ElAvatar shape="square" :size="140" :src="userInfo.avatar || undefined" />
-            </el-col>
-            <el-col :span="20">
-              <el-descriptions :column="2">
-                <el-descriptions-item>
-                  <template #label>
-                    <div class="cell-item">
-                      <Icon icon="ep:user" />
-                      用户名
-                    </div>
-                  </template>
-                  {{ userInfo.name || '空' }}
-                </el-descriptions-item>
-                <el-descriptions-item>
-                  <template #label>
-                    <div class="cell-item">
-                      <Icon icon="ep:user" />
-                      昵称
-                    </div>
-                  </template>
-                  {{ userInfo.nickname }}</el-descriptions-item
-                >
-                <el-descriptions-item label="手机号">
-                  <template #label>
-                    <div class="cell-item">
-                      <Icon icon="ep:phone" />
-                      手机号
-                    </div>
-                  </template>
-                  {{ userInfo.mobile }}</el-descriptions-item
-                >
-                <el-descriptions-item>
-                  <template #label>
-                    <div class="cell-item">
-                      <Icon icon="fa:mars-double" />
-                      性别
-                    </div>
-                  </template>
-                  <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="userInfo.sex" />
-                </el-descriptions-item>
-                <el-descriptions-item>
-                  <template #label>
-                    <div class="cell-item">
-                      <Icon icon="ep:location" />
-                      所在地
-                    </div>
-                  </template>
-                  {{ userInfo.areaId }}
-                </el-descriptions-item>
-                <el-descriptions-item>
-                  <template #label>
-                    <div class="cell-item">
-                      <Icon icon="ep:position" />
-                      注册IP
-                    </div>
-                  </template>
-                  {{ userInfo.registerIp }}
-                </el-descriptions-item>
-                <el-descriptions-item>
-                  <template #label>
-                    <div class="cell-item">
-                      <Icon icon="fa:birthday-cake" />
-                      生日
-                    </div>
-                  </template>
-                  {{ userInfo.birthday ? formatDate(userInfo.birthday) : '空' }}
-                </el-descriptions-item>
-                <el-descriptions-item>
-                  <template #label>
-                    <div class="cell-item">
-                      <Icon icon="ep:calendar" />
-                      注册时间
-                    </div>
-                  </template>
-                  {{ userInfo.createTime ? formatDate(userInfo.createTime) : '空' }}
-                </el-descriptions-item>
-                <el-descriptions-item>
-                  <template #label>
-                    <div class="cell-item">
-                      <Icon icon="ep:calendar" />
-                      最后登录时间
-                    </div>
-                  </template>
-                  {{ userInfo.loginDate ? formatDate(userInfo.loginDate) : '空' }}
-                </el-descriptions-item>
-              </el-descriptions>
-            </el-col>
-          </el-row>
-        </el-card>
+        </UserBasicInfo>
       </el-col>
+      <!-- 右上角:账户信息 -->
       <el-col :span="10" class="detail-info-item">
         <el-card shadow="never">
           <template #header>
-            <CardTitle title="账户信息(WIP)" />
+            <CardTitle title="账户信息" />
           </template>
-          <AccountInfo />
+          <UserAccountInfo :user="user" />
         </el-card>
       </el-col>
+      <!-- 下边:账户明细 -->
+      <!-- TODO 芋艿:【订单管理】【售后管理】【收藏记录】【优惠劵】 -->
       <el-card header="账户明细" style="width: 100%; margin-top: 20px" shadow="never">
         <template #header>
           <CardTitle title="账户明细" />
         </template>
-        <el-tabs v-model="activeName" class="demo-tabs">
+        <el-tabs v-model="activeName">
           <el-tab-pane label="积分" name="point">
-            <PointList v-if="userInfo.id" :member-id="userInfo.id" />
+            <UserPointList :user-id="id" />
           </el-tab-pane>
-          <el-tab-pane label="签到" name="sign">
-            <SignList v-if="userInfo.id" :member-id="userInfo.id" />
+          <el-tab-pane label="签到" name="sign" lazy>
+            <UserSignList :user-id="id" />
           </el-tab-pane>
           <el-tab-pane label="成长值" name="third">成长值(WIP)</el-tab-pane>
           <el-tab-pane label="余额" name="fourth">余额(WIP)</el-tab-pane>
+          <el-tab-pane label="收货地址" name="address" lazy>
+            <UserAddressList :user-id="id" />
+          </el-tab-pane>
+          <el-tab-pane label="订单管理" name="fourth">订单管理(WIP)</el-tab-pane>
+          <el-tab-pane label="售后管理" name="fourth">售后管理(WIP)</el-tab-pane>
+          <el-tab-pane label="收藏记录" name="fourth">收藏记录(WIP)</el-tab-pane>
+          <el-tab-pane label="优惠劵" name="fourth">优惠劵(WIP)</el-tab-pane>
         </el-tabs>
       </el-card>
     </el-row>
   </div>
+
   <!-- 表单弹窗:添加/修改 -->
-  <UserForm ref="formRef" @success="getUserData(userInfo.id)" />
+  <UserForm ref="formRef" @success="getUserData(id)" />
 </template>
 <script setup lang="ts">
-import { ref } from 'vue'
-import PointList from '@/views/member/user/components/point-list.vue'
-import SignList from '@/views/member/user/components/sign-list.vue'
-import CardTitle from '@/views/member/user/components/card-title.vue'
-import { ElMessage } from 'element-plus'
-import { getUser, UserBaseInfoVO } from '@/api/member/user'
-import { formatDate } from '@/utils/formatTime'
-import { DICT_TYPE } from '@/utils/dict'
+import * as UserApi from '@/api/member/user'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import UserBasicInfo from './UserBasicInfo.vue'
 import UserForm from '@/views/member/user/UserForm.vue'
-import AccountInfo from '@/views/member/user/components/account-info.vue'
+import UserAccountInfo from './UserAccountInfo.vue'
+import UserAddressList from './UserAddressList.vue'
+import UserPointList from './UserPointList.vue'
+import UserSignList from './UserSignList.vue'
+import { CardTitle } from '@/components/Card/index'
+
 defineOptions({ name: 'MemberDetail' })
 
-const activeName = ref('point')
-const loading = ref(true)
+const activeName = ref('point') // 账户明细 选中的 tabs
+const loading = ref(true) // 加载中
+let user = ref<UserApi.UserVO>({})
+
 /** 添加/修改操作 */
 const formRef = ref()
-const openForm = (type: string, id?: number) => {
+const openForm = (type: string) => {
   formRef.value.open(type, id)
 }
-let userInfo = ref<UserBaseInfoVO>({
-  areaId: undefined,
-  avatar: undefined,
-  birthday: undefined,
-  createTime: undefined,
-  id: undefined,
-  loginDate: undefined,
-  loginIp: '',
-  mark: '',
-  mobile: '',
-  name: '',
-  nickname: '',
-  password: null,
-  registerIp: undefined,
-  sex: 0,
-  status: 0
-})
 
-const getUserData = async (id: any) => {
+/** 获得用户 */
+const getUserData = async (id: number) => {
   loading.value = true
   try {
-    // userInfo.value = Object.assign(userInfo, await getUser(parseInt(id as string)))
-    userInfo.value = await getUser(parseInt(id as string))
+    user.value = await UserApi.getUser(id)
   } finally {
     loading.value = false
   }
 }
+
+/** 初始化 */
+const { currentRoute } = useRouter() // 路由
+const { delView } = useTagsViewStore() // 视图操作
 const route = useRoute()
-let router = useRouter()
-const { member_id } = route.query
+const id = Number(route.params.id)
 onMounted(() => {
-  if (!member_id) {
-    ElMessage.warning('会员id 未携带!')
-    router.back()
+  if (!id) {
+    ElMessage.warning('参数错误,会员编号不能为空!')
+    delView(unref(currentRoute))
     return
   }
-  getUserData(member_id)
+  getUserData(id)
 })
 </script>
-
 <style scoped lang="css">
 .detail-info-item:first-child {
   padding-left: 0 !important;
@@ -208,10 +113,4 @@ onMounted(() => {
   justify-content: space-between;
   align-items: center;
 }
-.cell-item {
-  display: inline;
-}
-.cell-item::after {
-  content: ':';
-}
 </style>

+ 36 - 16
src/views/member/user/index.vue

@@ -51,6 +51,12 @@
       <el-form-item label="用户标签" prop="tagIds">
         <MemberTagSelect v-model="queryParams.tagIds" />
       </el-form-item>
+      <el-form-item label="用户等级" prop="levelId">
+        <MemberLevelSelect v-model="queryParams.levelId" />
+      </el-form-item>
+      <el-form-item label="用户分组" prop="groupId">
+        <MemberGroupSelect v-model="queryParams.groupId" />
+      </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>
@@ -69,9 +75,8 @@
       </el-table-column>
       <el-table-column label="手机号" align="center" prop="mobile" width="120px" />
       <el-table-column label="昵称" align="center" prop="nickname" width="80px" />
-      <!-- TODO 芋艿:待接入 -->
-      <el-table-column label="等级" align="center" width="100px" />
-      <el-table-column label="分组" align="center" width="100px" />
+      <el-table-column label="等级" align="center" prop="levelName" width="100px" />
+      <el-table-column label="分组" align="center" prop="groupName" width="100px" />
       <el-table-column
         label="用户标签"
         align="center"
@@ -79,10 +84,12 @@
         :show-overflow-tooltip="false"
       >
         <template #default="scope">
-          <el-tag v-for="(tagName, index) in scope.row.tagNames" :key="index">{{ tagName }}</el-tag>
+          <el-tag v-for="(tagName, index) in scope.row.tagNames" :key="index" class="mr-5px">
+            {{ tagName }}
+          </el-tag>
         </template>
       </el-table-column>
-      <el-table-column label="积分" align="center" width="100px" />
+      <el-table-column label="积分" align="center" prop="point" width="100px" />
       <el-table-column label="状态" align="center" prop="status" width="100px">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
@@ -102,8 +109,9 @@
         :formatter="dateFormatter"
         width="180px"
       />
-      <el-table-column label="操作" align="center">
+      <el-table-column label="操作" align="center" width="180px" fixed="right">
         <template #default="scope">
+          <el-button link type="primary" @click="openDetail(scope.row.id)">详情</el-button>
           <el-button
             link
             type="primary"
@@ -112,6 +120,15 @@
           >
             编辑
           </el-button>
+          <!-- todo 放到更多菜单中 -->
+          <el-button
+            link
+            type="primary"
+            @click="updateLevelFormRef.open(scope.row.id)"
+            v-hasPermi="['member:user:update-level']"
+          >
+            修改等级
+          </el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -126,19 +143,20 @@
 
   <!-- 表单弹窗:添加/修改 -->
   <UserForm ref="formRef" @success="getList" />
+  <!-- 修改用户等级弹窗 -->
+  <UpdateLevelForm ref="updateLevelFormRef" @success="getList" />
 </template>
 <script setup lang="ts">
 import { dateFormatter } from '@/utils/formatTime'
 import * as UserApi from '@/api/member/user'
-import UserForm from './UserForm.vue'
 import { DICT_TYPE } from '@/utils/dict'
 import MemberTagSelect from '@/views/member/tag/components/MemberTagSelect.vue'
+import MemberLevelSelect from '@/views/member/level/components/MemberLevelSelect.vue'
+import MemberGroupSelect from '@/views/member/group/components/MemberGroupSelect.vue'
+import UpdateLevelForm from '@/views/member/user/UpdateLevelForm.vue'
 
 defineOptions({ name: 'MemberUser' })
 
-const message = useMessage() // 消息弹窗
-const { t } = useI18n() // 国际化
-
 const loading = ref(true) // 列表的加载中
 const total = ref(0) // 列表的总页数
 const list = ref([]) // 列表的数据
@@ -149,10 +167,12 @@ const queryParams = reactive({
   mobile: null,
   loginDate: [],
   createTime: [],
-  tagIds: []
+  tagIds: [],
+  levelId: null,
+  groupId: null
 })
 const queryFormRef = ref() // 搜索的表单
-const exportLoading = ref(false) // 导出的加载中
+const updateLevelFormRef = ref() // 修改会员等级表单
 
 /** 查询列表 */
 const getList = async () => {
@@ -178,10 +198,10 @@ const resetQuery = () => {
   handleQuery()
 }
 
-/** 添加/修改操作 */
-const formRef = ref()
-const openForm = (type: string, id?: number) => {
-  formRef.value.open(type, id)
+/** 打开会员详情 */
+const { push } = useRouter()
+const openDetail = (id: number) => {
+  push({ name: 'MemberUserDetail', params: { id } })
 }
 
 /** 初始化 **/

+ 1 - 1
src/views/pay/order/index.vue

@@ -29,7 +29,7 @@
           class="!w-240px"
         >
           <el-option
-            v-for="dict in getStrDictOptions(DICT_TYPE.PAY_CHANNEL_CODE_TYPE)"
+            v-for="dict in getStrDictOptions(DICT_TYPE.PAY_CHANNEL_CODE)"
             :key="dict.value"
             :label="dict.label"
             :value="dict.value"

部分文件因文件數量過多而無法顯示