Pārlūkot izejas kodu

Merge remote-tracking branch 'yudao/dev' into dev-crm

# Conflicts:
#	src/router/modules/remaining.ts
puhui999 1 gadu atpakaļ
vecāks
revīzija
2a55d88a44
58 mainītis faili ar 1157 papildinājumiem un 1269 dzēšanām
  1. 3 3
      .env.dev
  2. 0 2
      package.json
  3. 5 0
      src/api/crm/product/index.ts
  4. 0 4
      src/api/mall/product/category.ts
  5. 0 10
      src/api/mall/product/property.ts
  6. 3 8
      src/api/mall/product/spu.ts
  7. 1 1
      src/api/mall/promotion/diy/page.ts
  8. 1 1
      src/api/mall/promotion/diy/template.ts
  9. 16 12
      src/api/mall/promotion/reward/rewardActivity.ts
  10. 2 2
      src/components/DiyEditor/components/ComponentContainerProperty.vue
  11. 4 10
      src/components/DiyEditor/components/mobile/CouponCard/component.tsx
  12. 0 1
      src/components/DiyEditor/components/mobile/CouponCard/config.ts
  13. 0 1
      src/components/DiyEditor/components/mobile/MagicCube/config.ts
  14. 0 1
      src/components/DiyEditor/components/mobile/ProductCard/config.ts
  15. 0 1
      src/components/DiyEditor/components/mobile/ProductList/config.ts
  16. 2 3
      src/components/DiyEditor/components/mobile/PromotionArticle/index.vue
  17. 0 2
      src/components/DiyEditor/components/mobile/PromotionCombination/config.ts
  18. 0 1
      src/components/DiyEditor/components/mobile/SearchBar/config.ts
  19. 1 2
      src/components/DiyEditor/components/mobile/VideoPlayer/config.ts
  20. 2 2
      src/components/DiyEditor/components/mobile/VideoPlayer/property.vue
  21. 3 4
      src/components/DiyEditor/util.ts
  22. 2 2
      src/components/UploadFile/src/UploadImg.vue
  23. 1 1
      src/components/UploadFile/src/UploadImgs.vue
  24. 13 4
      src/router/modules/remaining.ts
  25. 0 1
      src/utils/dict.ts
  26. 5 5
      src/utils/index.ts
  27. 0 71
      src/views/crm/product/ProductDetail.vue
  28. 3 3
      src/views/crm/product/ProductForm.vue
  29. 55 0
      src/views/crm/product/detail/ProductDetailsHeader.vue
  30. 45 0
      src/views/crm/product/detail/ProductDetailsInfo.vue
  31. 62 0
      src/views/crm/product/detail/index.vue
  32. 30 27
      src/views/crm/product/index.vue
  33. 0 6
      src/views/mall/product/category/CategoryForm.vue
  34. 1 1
      src/views/mall/product/category/index.vue
  35. 2 2
      src/views/mall/product/property/value/index.vue
  36. 40 10
      src/views/mall/product/spu/components/SkuList.vue
  37. 0 66
      src/views/mall/product/spu/form/ActivityOrdersSort.vue
  38. 0 375
      src/views/mall/product/spu/form/BasicInfoForm.vue
  39. 96 0
      src/views/mall/product/spu/form/DeliveryForm.vue
  40. 19 47
      src/views/mall/product/spu/form/DescriptionForm.vue
  41. 146 0
      src/views/mall/product/spu/form/InfoForm.vue
  42. 91 0
      src/views/mall/product/spu/form/OtherForm.vue
  43. 0 209
      src/views/mall/product/spu/form/OtherSettingsForm.vue
  44. 19 13
      src/views/mall/product/spu/form/ProductAttributes.vue
  45. 12 17
      src/views/mall/product/spu/form/ProductPropertyAddForm.vue
  46. 187 0
      src/views/mall/product/spu/form/SkuForm.vue
  47. 42 26
      src/views/mall/product/spu/form/index.vue
  48. 0 101
      src/views/mall/product/spu/form/spu.data.ts
  49. 70 64
      src/views/mall/product/spu/index.vue
  50. 8 8
      src/views/mall/promotion/diy/page/DiyPageForm.vue
  51. 1 1
      src/views/mall/promotion/diy/page/decorate.vue
  52. 3 3
      src/views/mall/promotion/diy/page/index.vue
  53. 8 8
      src/views/mall/promotion/diy/template/DiyTemplateForm.vue
  54. 1 1
      src/views/mall/promotion/diy/template/decorate.vue
  55. 3 3
      src/views/mall/promotion/diy/template/index.vue
  56. 148 113
      src/views/mall/promotion/rewardActivity/RewardForm.vue
  57. 1 9
      src/views/mall/promotion/rewardActivity/index.vue
  58. 0 1
      src/views/mall/trade/delivery/express/ExpressForm.vue

+ 3 - 3
.env.dev

@@ -4,8 +4,8 @@ NODE_ENV=development
 VITE_DEV=true
 
 # 请求路径
-# VITE_BASE_URL='http://api-dashboard.yudao.iocoder.cn'
-VITE_BASE_URL='http://dofast.demo.huizhizao.vip:20001'
+VITE_BASE_URL='http://api-dashboard.yudao.iocoder.cn'
+# VITE_BASE_URL='http://dofast.demo.huizhizao.vip:20001'
 
 # 上传路径
 VITE_UPLOAD_URL='http://api-dashboard.yudao.iocoder.cn/admin-api/infra/file/upload'
@@ -35,4 +35,4 @@ VITE_OUT_DIR=dist
 VITE_MALL_H5_DOMAIN='http://mall.yudao.iocoder.cn'
 
 # 验证码的开关
-VITE_APP_CAPTCHA_ENABLE=true
+VITE_APP_CAPTCHA_ENABLE=false

+ 0 - 2
package.json

@@ -57,7 +57,6 @@
     "pinia": "^2.1.7",
     "qrcode": "^1.5.3",
     "qs": "^6.11.2",
-    "sortablejs": "^1.15.0",
     "steady-xml": "^0.1.0",
     "url": "^0.11.3",
     "video.js": "^7.21.5",
@@ -81,7 +80,6 @@
     "@types/nprogress": "^0.2.3",
     "@types/qrcode": "^1.5.5",
     "@types/qs": "^6.9.10",
-    "@types/sortablejs": "^1.15.5",
     "@typescript-eslint/eslint-plugin": "^6.11.0",
     "@typescript-eslint/parser": "^6.11.0",
     "@unocss/transformer-variant-group": "^0.57.4",

+ 5 - 0
src/api/crm/product/index.ts

@@ -41,3 +41,8 @@ export const deleteProduct = async (id: number) => {
 export const exportProduct = async (params) => {
   return await request.download({ url: `/crm/product/export-excel`, params })
 }
+
+// 查询产品操作日志
+export const getOperateLogPage = async (params: any) => {
+  return await request.get({ url: '/crm/product/operate-log-page', params })
+}

+ 0 - 4
src/api/mall/product/category.ts

@@ -20,10 +20,6 @@ export interface CategoryVO {
    * 移动端分类图
    */
   picUrl: string
-  /**
-   * PC 端分类图
-   */
-  bigPicUrl?: string
   /**
    * 分类排序
    */

+ 0 - 10
src/api/mall/product/property.ts

@@ -65,16 +65,6 @@ export const getPropertyPage = (params: PageParam) => {
   return request.get({ url: '/product/property/page', params })
 }
 
-// 获得属性项列表
-export const getPropertyList = (params: any) => {
-  return request.get({ url: '/product/property/list', params })
-}
-
-// 获得属性项列表
-export const getPropertyListAndValue = (data: any) => {
-  return request.post({ url: '/product/property/get-value-list', data })
-}
-
 // ------------------------ 属性值 -------------------
 
 // 获得属性值分页

+ 3 - 8
src/api/mall/product/spu.ts

@@ -33,14 +33,15 @@ export interface GiveCouponTemplate {
 export interface Spu {
   id?: number
   name?: string // 商品名称
-  categoryId?: number | undefined // 商品分类
+  categoryId?: number // 商品分类
   keyword?: string // 关键字
   unit?: number | undefined // 单位
   picUrl?: string // 商品封面图
   sliderPicUrls?: string[] // 商品轮播图
   introduction?: string // 商品简介
+  deliveryTypes?: number[] // 配送方式
   deliveryTemplateId?: number | undefined // 运费模版
-  brandId?: number | undefined // 商品品牌编号
+  brandId?: number // 商品品牌编号
   specType?: boolean // 商品规格
   subCommissionType?: boolean // 分销类型
   skus?: Sku[] // sku数组
@@ -48,11 +49,6 @@ export interface Spu {
   sort?: number // 商品排序
   giveIntegral?: number // 赠送积分
   virtualSalesCount?: number // 虚拟销量
-  recommendHot?: boolean // 是否热卖
-  recommendBenefit?: boolean // 是否优惠
-  recommendBest?: boolean // 是否精品
-  recommendNew?: boolean // 是否新品
-  recommendGood?: boolean // 是否优品
   price?: number // 商品价格
   salesCount?: number // 商品销量
   marketPrice?: number // 市场价
@@ -60,7 +56,6 @@ export interface Spu {
   stock?: number // 商品库存
   createTime?: Date // 商品创建时间
   status?: number // 商品状态
-  activityOrders: number[] // 活动排序
 }
 
 // 获得 Spu 列表

+ 1 - 1
src/api/mall/promotion/diy/page.ts

@@ -5,7 +5,7 @@ export interface DiyPageVO {
   templateId?: number
   name: string
   remark: string
-  previewImageUrls: string[]
+  previewPicUrls: string[]
   property: string
 }
 

+ 1 - 1
src/api/mall/promotion/diy/template.ts

@@ -7,7 +7,7 @@ export interface DiyTemplateVO {
   used: boolean
   usedTime?: Date
   remark: string
-  previewImageUrls: string[]
+  previewPicUrls: string[]
   property: string
 }
 

+ 16 - 12
src/api/mall/promotion/reward/rewardActivity.ts

@@ -1,17 +1,18 @@
 import request from '@/config/axios'
 
 export interface DiscountActivityVO {
-  id?:number,
+  id?: number
   name?: string
-  startTime?:Date
-  endTime?:Date
-  remark?:string
-  conditionType?:number
-  productScope?:number
-  productSpuIds?:number[]
-  rules?:DiscountProductVO[]
+  startTime?: Date
+  endTime?: Date
+  remark?: string
+  conditionType?: number
+  productScope?: number
+  productSpuIds?: number[]
+  rules?: DiscountProductVO[]
 }
-//优惠规则
+
+// 优惠规则
 export interface DiscountProductVO {
   limit: number
   discountPrice: number
@@ -21,23 +22,26 @@ export interface DiscountProductVO {
   couponCounts: number[]
 }
 
-
 // 新增满减送活动
 export const createRewardActivity = async (data: DiscountActivityVO) => {
   return await request.post({ url: '/promotion/reward-activity/create', data })
 }
+
 // 更新满减送活动
 export const updateRewardActivity = async (data: DiscountActivityVO) => {
   return await request.put({ url: '/promotion/reward-activity/update', data })
 }
+
 // 查询满减送活动列表
 export const getRewardActivityPage = async (params) => {
   return await request.get({ url: '/promotion/reward-activity/page', params })
 }
+
 // 查询满减送活动详情
-export const getReward = async (id:number) => {
-  return await request.get({ url: '/promotion/reward-activity/get?id='+id,  })
+export const getReward = async (id: number) => {
+  return await request.get({ url: '/promotion/reward-activity/get?id=' + id })
 }
+
 // 删除限时折扣活动
 export const deleteRewardActivity = async (id: number) => {
   return await request.delete({ url: '/promotion/reward-activity/delete?id=' + id })

+ 2 - 2
src/components/DiyEditor/components/ComponentContainerProperty.vue

@@ -23,7 +23,7 @@
               <template #tip>建议宽度 750px</template>
             </UploadImg>
           </el-form-item>
-          <el-tree :data="treeData" :expand-on-click-node="false">
+          <el-tree :data="treeData" :expand-on-click-node="false" default-expand-all>
             <template #default="{ node, data }">
               <el-form-item
                 :label="data.label"
@@ -43,7 +43,7 @@
               </el-form-item>
             </template>
           </el-tree>
-          <slot name="style" :formData="formData"></slot>
+          <slot name="style" :style="formData"></slot>
         </el-form>
       </el-card>
     </el-tab-pane>

+ 4 - 10
src/components/DiyEditor/components/mobile/CouponCard/component.tsx

@@ -2,15 +2,13 @@ import * as CouponTemplateApi from '@/api/mall/promotion/coupon/couponTemplate'
 import { CouponTemplateValidityTypeEnum, PromotionDiscountTypeEnum } from '@/utils/constants'
 import { floatToFixed2 } from '@/utils'
 import { formatDate } from '@/utils/formatTime'
+import { object } from 'vue-types'
 
 // 优惠值
-// TODO @疯狂:idea 有告警
 export const CouponDiscount = defineComponent({
   name: 'CouponDiscount',
   props: {
-    coupon: {
-      type: CouponTemplateApi.CouponTemplateVO
-    }
+    coupon: object<CouponTemplateApi.CouponTemplateVO>()
   },
   setup(props) {
     const coupon = props.coupon as CouponTemplateApi.CouponTemplateVO
@@ -35,9 +33,7 @@ export const CouponDiscount = defineComponent({
 export const CouponDiscountDesc = defineComponent({
   name: 'CouponDiscountDesc',
   props: {
-    coupon: {
-      type: CouponTemplateApi.CouponTemplateVO
-    }
+    coupon: object<CouponTemplateApi.CouponTemplateVO>()
   },
   setup(props) {
     const coupon = props.coupon as CouponTemplateApi.CouponTemplateVO
@@ -61,9 +57,7 @@ export const CouponDiscountDesc = defineComponent({
 export const CouponValidTerm = defineComponent({
   name: 'CouponValidTerm',
   props: {
-    coupon: {
-      type: CouponTemplateApi.CouponTemplateVO
-    }
+    coupon: object<CouponTemplateApi.CouponTemplateVO>()
   },
   setup(props) {
     const coupon = props.coupon as CouponTemplateApi.CouponTemplateVO

+ 0 - 1
src/components/DiyEditor/components/mobile/CouponCard/config.ts

@@ -24,7 +24,6 @@ export interface CouponCardProperty {
 }
 
 // 定义组件
-// TODO @疯狂:idea 有告警
 export const component = {
   id: 'CouponCard',
   name: '优惠券',

+ 0 - 1
src/components/DiyEditor/components/mobile/MagicCube/config.ts

@@ -31,7 +31,6 @@ export interface MagicCubeItemProperty {
 }
 
 // 定义组件
-// TODO @疯狂:有 idea 爆红告警
 export const component = {
   id: 'MagicCube',
   name: '广告魔方',

+ 0 - 1
src/components/DiyEditor/components/mobile/ProductCard/config.ts

@@ -59,7 +59,6 @@ export interface ProductCardFieldProperty {
 }
 
 // 定义组件
-// TODO @疯狂:idea 有告警
 export const component = {
   id: 'ProductCard',
   name: '商品卡片',

+ 0 - 1
src/components/DiyEditor/components/mobile/ProductList/config.ts

@@ -38,7 +38,6 @@ export interface ProductListFieldProperty {
 }
 
 // 定义组件
-// TODO @疯狂:idea 有告警
 export const component = {
   id: 'ProductList',
   name: '商品栏',

+ 2 - 3
src/components/DiyEditor/components/mobile/PromotionArticle/index.vue

@@ -1,17 +1,16 @@
 <template>
-  <div class="min-h-30px" v-html="article.content"></div>
+  <div class="min-h-30px" v-html="article?.content"></div>
 </template>
 <script setup lang="ts">
 import { PromotionArticleProperty } from './config'
 import * as ArticleApi from '@/api/mall/promotion/article/index'
 
 /** 营销文章 */
-// TODO @疯狂:idea 有告警
 defineOptions({ name: 'PromotionArticle' })
 // 定义属性
 const props = defineProps<{ property: PromotionArticleProperty }>()
 // 商品列表
-const article = ref<ArticleApi.ArticleVO[]>({})
+const article = ref<ArticleApi.ArticleVO>()
 watch(
   () => props.property.id,
   async () => {

+ 0 - 2
src/components/DiyEditor/components/mobile/PromotionCombination/config.ts

@@ -39,13 +39,11 @@ export interface PromotionCombinationFieldProperty {
 }
 
 // 定义组件
-// TODO @疯狂:idea 有告警
 export const component = {
   id: 'PromotionCombination',
   name: '拼团',
   icon: 'mdi:account-group',
   property: {
-    activityId: undefined,
     layoutType: 'oneCol',
     fields: {
       name: { show: true, color: '#000' },

+ 0 - 1
src/components/DiyEditor/components/mobile/SearchBar/config.ts

@@ -17,7 +17,6 @@ export interface SearchProperty {
 export type PlaceholderPosition = 'left' | 'center'
 
 // 定义组件
-// TODO @疯狂:idea 这里爆红,可以卡看咋优化下哇:is missing the following properties from type DiyComponent<SearchProperty>: uid, position
 export const component = {
   id: 'SearchBar',
   name: '搜索框',

+ 1 - 2
src/components/DiyEditor/components/mobile/VideoPlayer/config.ts

@@ -19,7 +19,6 @@ export interface VideoPlayerStyle extends ComponentStyle {
 }
 
 // 定义组件
-// TODO @疯狂:idea 有告警
 export const component = {
   id: 'VideoPlayer',
   name: '视频播放',
@@ -33,6 +32,6 @@ export const component = {
       bgColor: '#fff',
       marginBottom: 8,
       height: 300
-    } as ComponentStyle
+    } as VideoPlayerStyle
   }
 } as DiyComponent<VideoPlayerProperty>

+ 2 - 2
src/components/DiyEditor/components/mobile/VideoPlayer/property.vue

@@ -1,9 +1,9 @@
 <template>
   <ComponentContainerProperty v-model="formData.style">
-    <template #style="{ formData }">
+    <template #style>
       <el-form-item label="高度" prop="height">
         <el-slider
-          v-model="formData.height"
+          v-model="formData.style.height"
           :max="500"
           :min="100"
           show-input

+ 3 - 4
src/components/DiyEditor/util.ts

@@ -6,7 +6,7 @@ import { TabBarProperty } from '@/components/DiyEditor/components/mobile/TabBar/
 // 页面装修组件
 export interface DiyComponent<T> {
   // 用于区分同一种组件的不同实例
-  uid: number
+  uid?: number
   // 组件唯一标识
   id: string
   // 组件名称
@@ -21,7 +21,7 @@ export interface DiyComponent<T> {
    空:同center
    fixed: 由组件自己决定位置,如弹窗位于手机中心、浮动按钮一般位于手机右下角
   */
-  position: 'top' | 'bottom' | 'center' | '' | 'fixed'
+  position?: 'top' | 'bottom' | 'center' | '' | 'fixed'
   // 组件属性
   property: T
 }
@@ -103,8 +103,7 @@ export function usePropertyForm<T>(modelValue: T, emit: Function): { formData: R
     }
   )
 
-  // TODO @疯狂:这个 idea 爆红,看看怎么可以解决哈
-  return { formData }
+  return { formData } as { formData: Ref<T> }
 }
 
 // 页面组件库

+ 2 - 2
src/components/UploadFile/src/UploadImg.vue

@@ -16,7 +16,7 @@
       <template v-if="modelValue">
         <img :src="modelValue" class="upload-image" />
         <div class="upload-handle" @click.stop>
-          <div class="handle-icon" @click="editImg">
+          <div class="handle-icon" @click="editImg" v-if="!disabled">
             <Icon icon="ep:edit" />
             <span v-if="showBtnText">{{ t('action.edit') }}</span>
           </div>
@@ -24,7 +24,7 @@
             <Icon icon="ep:zoom-in" />
             <span v-if="showBtnText">{{ t('action.detail') }}</span>
           </div>
-          <div v-if="showDelete" class="handle-icon" @click="deleteImg">
+          <div v-if="showDelete && !disabled" class="handle-icon" @click="deleteImg">
             <Icon icon="ep:delete" />
             <span v-if="showBtnText">{{ t('action.del') }}</span>
           </div>

+ 1 - 1
src/components/UploadFile/src/UploadImgs.vue

@@ -28,7 +28,7 @@
             <Icon icon="ep:zoom-in" />
             <span>查看</span>
           </div>
-          <div class="handle-icon" @click="handleRemove(file)">
+          <div class="handle-icon" @click="handleRemove(file)" v-if="!disabled">
             <Icon icon="ep:delete" />
             <span>删除</span>
           </div>

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

@@ -473,8 +473,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
           title: '模板装修',
           noCache: true,
           hidden: true,
-          // TODO @疯狂:建议 menu 那的 /mall/promotion/diy-template/diy-template 改成 /mall/promotion/diy/template
-          activeMenu: '/mall/promotion/diy-template/diy-template'
+          activeMenu: '/mall/promotion/diy/template'
         },
         component: () => import('@/views/mall/promotion/diy/template/decorate.vue')
       },
@@ -485,8 +484,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
           title: '页面装修',
           noCache: true,
           hidden: true,
-          // TODO @疯狂:建议 menu 那的 /mall/promotion/diy-template/diy-page 改成 /mall/promotion/diy/page
-          activeMenu: '/mall/promotion/diy-template/diy-page'
+          activeMenu: '/mall/promotion/diy/page'
         },
         component: () => import('@/views/mall/promotion/diy/page/decorate.vue')
       }
@@ -519,6 +517,17 @@ const remainingRouter: AppRouteRecordRaw[] = [
           activeMenu: '/crm/contact'
         },
         component: () => import('@/views/crm/contact/detail/index.vue')
+      },
+      {
+        path: 'product/detail/:id',
+        name: 'CrmProductDetail',
+        meta: {
+          title: '产品详情',
+          noCache: true,
+          hidden: true,
+          activeMenu: '/crm/product'
+        },
+        component: () => import('@/views/crm/product/detail/index.vue')
       }
     ]
   }

+ 0 - 1
src/utils/dict.ts

@@ -103,7 +103,6 @@ export const getDictLabel = (dictType: string, value: any): string => {
 export enum DICT_TYPE {
   USER_TYPE = 'user_type',
   COMMON_STATUS = 'common_status',
-  SYSTEM_TENANT_PACKAGE_ID = 'system_tenant_package_id',
   TERMINAL = 'terminal', // 终端
 
   // ========== SYSTEM 模块 ==========

+ 5 - 5
src/utils/index.ts

@@ -177,7 +177,7 @@ export const fileSizeFormatter = (row, column, cellValue) => {
  * @param target 目标对象
  * @param source 源对象
  */
-export const copyValueToTarget = (target, source) => {
+export const copyValueToTarget = (target: any, source: any) => {
   const newObj = Object.assign({}, target, source)
   // 删除多余属性
   Object.keys(newObj).forEach((key) => {
@@ -194,10 +194,10 @@ export const copyValueToTarget = (target, source) => {
  * 将一个整数转换为分数保留两位小数
  * @param num
  */
-export const formatToFraction = (num: number | string | undefined): number => {
-  if (typeof num === 'undefined') return 0
+export const formatToFraction = (num: number | string | undefined): string => {
+  if (typeof num === 'undefined') return '0.00'
   const parsedNumber = typeof num === 'string' ? parseFloat(num) : num
-  return parseFloat((parsedNumber / 100).toFixed(2))
+  return (parsedNumber / 100.0).toFixed(2)
 }
 
 /**
@@ -249,7 +249,7 @@ export const yuanToFen = (amount: string | number): number => {
 /**
  * 分转元
  */
-export const fenToYuan = (price: string | number): number => {
+export const fenToYuan = (price: string | number): string => {
   return formatToFraction(price)
 }
 

+ 0 - 71
src/views/crm/product/ProductDetail.vue

@@ -1,71 +0,0 @@
-<template>
-  <Dialog v-model="dialogVisible" :max-height="500" :scroll="true" title="产品详情">
-    <el-descriptions :column="1" border>
-      <el-descriptions-item label="产品名称">
-        {{ detailData.name }}
-      </el-descriptions-item>
-      <el-descriptions-item label="创建时间">
-        {{ formatDate(detailData.createTime) }}
-      </el-descriptions-item>
-      <el-descriptions-item label="状态">
-        <dict-tag :type="DICT_TYPE.CRM_PRODUCT_STATUS" :value="detailData.status" />
-      </el-descriptions-item>
-      <el-descriptions-item label="产品分类">
-        {{ productCategoryList?.find((c) => c.id === detailData.categoryId)?.name }}
-      </el-descriptions-item>
-      <el-descriptions-item label="产品编码">
-        {{ detailData.no }}
-      </el-descriptions-item>
-      <el-descriptions-item label="产品描述">
-        {{ detailData.description }}
-      </el-descriptions-item>
-      <el-descriptions-item label="负责人">
-        {{ detailData.ownerUserId }}
-      </el-descriptions-item>
-      <el-descriptions-item label="单位">
-        <dict-tag :type="DICT_TYPE.PRODUCT_UNIT" :value="detailData.unit" />
-      </el-descriptions-item>
-      <el-descriptions-item label="价格">
-        {{ fenToYuan(detailData.price) }}元
-      </el-descriptions-item>
-    </el-descriptions>
-  </Dialog>
-</template>
-<script setup lang="ts">
-// TODO 芋艿:统一改成,独立 tab
-import { DICT_TYPE } from '@/utils/dict'
-import * as ProductCategoryApi from '@/api/crm/product/productCategory'
-import * as ProductApi from '@/api/crm/product'
-import { formatDate } from '@/utils/formatTime'
-import { fenToYuan } from '@/utils'
-import { getSimpleUserList, UserVO } from '@/api/system/user'
-
-defineOptions({ name: 'CrmProductDetail' })
-
-const { t } = useI18n() // 国际化
-
-const dialogVisible = ref(false) // 弹窗的是否展示
-const detailLoading = ref(false) // 表单的加载中
-const detailData = ref() // 详情数据
-
-/** 打开弹窗 */
-const open = async (data: ProductApi.ProductVO) => {
-  dialogVisible.value = true
-  // 设置数据
-  detailLoading.value = true
-  try {
-    detailData.value = data
-  } finally {
-    detailLoading.value = false
-  }
-}
-defineExpose({ open }) // 提供 open 方法,用于打开弹窗
-
-const productCategoryList = ref([]) // 产品分类树
-const userList = ref<UserVO[]>([]) // 系统用户
-
-onMounted(async () => {
-  productCategoryList.value = await ProductCategoryApi.getProductCategoryList({})
-  userList.value = await getSimpleUserList()
-})
-</script>

+ 3 - 3
src/views/crm/product/ProductForm.vue

@@ -62,13 +62,13 @@
         </el-col>
         <el-col :span="12">
           <el-form-item label="价格" prop="price">
-            <el-input
-              type="number"
+            <el-input-number
               v-model="formData.price"
               placeholder="请输入价格"
               :min="0"
               :precision="2"
               :step="0.1"
+              class="w-full!"
             />
           </el-form-item>
         </el-col>
@@ -149,7 +149,7 @@ const open = async (type: string, id?: number) => {
     formLoading.value = true
     try {
       formData.value = await ProductApi.getProduct(id)
-      formData.value.price = fenToYuan(formData.value.price)
+      formData.value.price = Number(fenToYuan(formData.value.price))
     } finally {
       formLoading.value = false
     }

+ 55 - 0
src/views/crm/product/detail/ProductDetailsHeader.vue

@@ -0,0 +1,55 @@
+<template>
+  <div>
+    <div class="flex items-start justify-between">
+      <div>
+        <el-col>
+          <el-row>
+            <span class="text-xl font-bold">{{ product.name }}</span>
+          </el-row>
+        </el-col>
+      </div>
+      <div>
+        <!-- 右上:按钮 -->
+        <el-button @click="openForm('update', product.id)" v-hasPermi="['crm:product:update']">
+          编辑
+        </el-button>
+      </div>
+    </div>
+  </div>
+  <ContentWrap class="mt-10px">
+    <el-descriptions :column="5" direction="vertical">
+      <el-descriptions-item label="产品类别">
+        {{ productCategoryList?.find((c) => c.id === product.categoryId)?.name }}
+      </el-descriptions-item>
+      <el-descriptions-item label="产品单位">
+        <dict-tag :type="DICT_TYPE.PRODUCT_UNIT" :value="product.unit" />
+      </el-descriptions-item>
+      <el-descriptions-item label="产品价格">{{ fenToYuan(product.price) }}元</el-descriptions-item>
+      <el-descriptions-item label="产品编码">{{ product.no }}</el-descriptions-item>
+    </el-descriptions>
+  </ContentWrap>
+  <!-- 表单弹窗:添加/修改 -->
+  <ProductForm ref="formRef" @success="emit('refresh')" />
+</template>
+<script setup lang="ts">
+import ProductForm from '@/views/crm/product/ProductForm.vue'
+import { DICT_TYPE } from '@/utils/dict'
+import { fenToYuan } from '@/utils'
+import * as ProductApi from '@/api/crm/product'
+import * as ProductCategoryApi from '@/api/crm/product/productCategory'
+
+// 操作修改
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+const { product } = defineProps<{ product: ProductApi.ProductVO }>()
+const emit = defineEmits(['refresh']) // 定义 success 事件,用于操作成功后的回调
+
+/** 初始化 */
+const productCategoryList = ref([]) // 产品分类树
+
+onMounted(async () => {
+  productCategoryList.value = await ProductCategoryApi.getProductCategoryList({})
+})
+</script>

+ 45 - 0
src/views/crm/product/detail/ProductDetailsInfo.vue

@@ -0,0 +1,45 @@
+<template>
+  <ContentWrap>
+    <el-collapse v-model="activeNames">
+      <el-collapse-item name="basicInfo">
+        <template #title>
+          <span class="text-base font-bold">基本信息</span>
+        </template>
+        <el-descriptions :column="4">
+          <el-descriptions-item label="产品名称">{{ product.name }}</el-descriptions-item>
+          <el-descriptions-item label="产品编码">{{ product.no }}</el-descriptions-item>
+          <el-descriptions-item label="价格">{{ fenToYuan(product.price) }}元</el-descriptions-item>
+          <el-descriptions-item label="产品描述">{{ product.description }}</el-descriptions-item>
+          <el-descriptions-item label="产品类型">
+            {{ productCategoryList?.find((c) => c.id === product.categoryId)?.name }}
+          </el-descriptions-item>
+          <el-descriptions-item label="是否上下架">
+            <dict-tag :type="DICT_TYPE.CRM_PRODUCT_STATUS" :value="product.status"/>
+          </el-descriptions-item>
+          <el-descriptions-item label="单位">
+            <dict-tag :type="DICT_TYPE.PRODUCT_UNIT" :value="product.unit"/>
+          </el-descriptions-item>
+        </el-descriptions>
+      </el-collapse-item>
+    </el-collapse>
+  </ContentWrap>
+</template>
+<script setup lang="ts">
+import {DICT_TYPE} from '@/utils/dict'
+import * as ProductApi from '@/api/crm/product'
+import {fenToYuan} from '@/utils'
+import * as ProductCategoryApi from '@/api/crm/product/productCategory'
+
+const {product} = defineProps<{
+  product: ProductApi.ProductVO
+}>()
+
+// 展示的折叠面板
+const activeNames = ref(['basicInfo'])
+
+/** 初始化 */
+const productCategoryList = ref([]) // 产品分类树
+onMounted(async () => {
+  productCategoryList.value = await ProductCategoryApi.getProductCategoryList({})
+})
+</script>

+ 62 - 0
src/views/crm/product/detail/index.vue

@@ -0,0 +1,62 @@
+<template>
+  <ProductDetailsHeader :product="product" :loading="loading" @refresh="getProductData(id)" />
+  <el-col>
+    <el-tabs>
+      <el-tab-pane label="详细资料">
+        <ProductDetailsInfo :product="product" />
+      </el-tab-pane>
+      <el-tab-pane label="操作日志">
+        <OperateLogV2 :log-list="logList" />
+      </el-tab-pane>
+    </el-tabs>
+  </el-col>
+</template>
+<script setup lang="ts">
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import { OperateLogV2VO } from '@/api/system/operatelog'
+import * as ProductApi from '@/api/crm/product'
+import ProductDetailsHeader from '@/views/crm/product/detail/ProductDetailsHeader.vue'
+import ProductDetailsInfo from '@/views/crm/product/detail/ProductDetailsInfo.vue'
+
+defineOptions({ name: 'CrmProductDetail' })
+
+const route = useRoute()
+const id = Number(route.params.id) // 编号
+const loading = ref(true) // 加载中
+const product = ref<ProductApi.ProductVO>({} as ProductApi.ProductVO) // 详情
+
+/** 获取详情 */
+const getProductData = async (id: number) => {
+  loading.value = true
+  try {
+    product.value = await ProductApi.getProduct(id)
+    await getOperateLog(id)
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 获取操作日志 */
+const logList = ref<OperateLogV2VO[]>([]) // 操作日志列表
+const getOperateLog = async (productId: number) => {
+  if (!productId) {
+    return
+  }
+  const data = await ProductApi.getOperateLogPage({
+    bizId: productId
+  })
+  logList.value = data.list
+}
+
+/** 初始化 */
+const { delView } = useTagsViewStore() // 视图操作
+const { currentRoute } = useRouter() // 路由
+onMounted(async () => {
+  if (!id) {
+    ElMessage.warning('参数错误,产品不能为空!')
+    delView(unref(currentRoute))
+    return
+  }
+  await getProductData(id)
+})
+</script>

+ 30 - 27
src/views/crm/product/index.vue

@@ -28,8 +28,8 @@
         </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 @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="['crm:product:create']">
           <Icon icon="ep:plus" class="mr-5px" /> 新增
         </el-button>
@@ -40,7 +40,8 @@
           :loading="exportLoading"
           v-hasPermi="['crm:product:export']"
         >
-          <Icon icon="ep:download" class="mr-5px" /> 导出
+          <Icon icon="ep:download" class="mr-5px" />
+          导出
         </el-button>
       </el-form-item>
     </el-form>
@@ -49,8 +50,14 @@
   <!-- 列表 -->
   <ContentWrap>
     <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
-      <el-table-column label="产品名称" align="center" prop="name" />
-      <el-table-column label="产品类型" align="center" prop="categoryName" />
+      <el-table-column label="产品名称" align="center" prop="name" width="160">
+        <template #default="scope">
+          <el-link :underline="false" type="primary" @click="openDetail(scope.row.id)">
+            {{ scope.row.name }}
+          </el-link>
+        </template>
+      </el-table-column>
+      <el-table-column label="产品类型" align="center" prop="categoryName" width="160" />
       <el-table-column label="产品单位" align="center" prop="unit">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.PRODUCT_UNIT" :value="scope.row.unit" />
@@ -62,14 +69,15 @@
         align="center"
         prop="price"
         :formatter="fenToYuanFormat"
+        width="100"
       />
-      <el-table-column label="产品描述" align="center" prop="description" />
-      <el-table-column label="是否架" align="center" prop="status">
+      <el-table-column label="产品描述" align="center" prop="description" width="150" />
+      <el-table-column label="上架状态" align="center" prop="status" width="120">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.CRM_PRODUCT_STATUS" :value="scope.row.status" />
         </template>
       </el-table-column>
-      <el-table-column label="负责人" align="center" prop="ownerUserName" />
+      <el-table-column label="负责人" align="center" prop="ownerUserName" width="120" />
       <el-table-column
         label="更新时间"
         align="center"
@@ -77,7 +85,7 @@
         :formatter="dateFormatter"
         width="180px"
       />
-      <el-table-column label="创建" align="center" prop="creatorName" />
+      <el-table-column label="创建" align="center" prop="creatorName" width="120" />
       <el-table-column
         label="创建时间"
         align="center"
@@ -85,16 +93,8 @@
         :formatter="dateFormatter"
         width="180px"
       />
-      <el-table-column label="操作" align="center" width="160">
+      <el-table-column label="操作" align="center" fixed="right" width="160">
         <template #default="scope">
-          <el-button
-            v-hasPermi="['crm:product:query']"
-            link
-            type="primary"
-            @click="openDetail(scope.row)"
-          >
-            详情
-          </el-button>
           <el-button
             link
             type="primary"
@@ -125,8 +125,6 @@
 
   <!-- 表单弹窗:添加/修改 -->
   <ProductForm ref="formRef" @success="getList" />
-  <!-- 表单弹窗:详情 -->
-  <ProductDetail ref="detailRef" @success="getList" />
 </template>
 
 <script setup lang="ts">
@@ -135,7 +133,6 @@ import { dateFormatter } from '@/utils/formatTime'
 import download from '@/utils/download'
 import * as ProductApi from '@/api/crm/product'
 import ProductForm from './ProductForm.vue'
-import ProductDetail from './ProductDetail.vue'
 import { fenToYuanFormat } from '@/utils/formatter'
 
 defineOptions({ name: 'CrmProduct' })
@@ -184,10 +181,11 @@ const formRef = ref()
 const openForm = (type: string, id?: number) => {
   formRef.value.open(type, id)
 }
-/** 详情操作 */
-const detailRef = ref()
-const openDetail = (data: ProductApi.ProductVO) => {
-  detailRef.value.open(data)
+
+/** 打开详情 */
+const { currentRoute, push } = useRouter()
+const openDetail = (id: number) => {
+  push({ name: 'CrmProductDetail', params: { id } })
 }
 
 /** 删除按钮操作 */
@@ -218,8 +216,13 @@ const handleExport = async () => {
   }
 }
 
+/** 激活时 */
+onActivated(() => {
+  getList()
+})
+
 /** 初始化 **/
-onMounted(async () => {
-  await getList()
+onMounted(() => {
+  getList()
 })
 </script>

+ 0 - 6
src/views/mall/product/category/CategoryForm.vue

@@ -25,10 +25,6 @@
         <UploadImg v-model="formData.picUrl" :limit="1" :is-show-tip="false" />
         <div style="font-size: 10px" class="pl-10px">推荐 180x180 图片分辨率</div>
       </el-form-item>
-      <el-form-item label="PC 端分类图" prop="bigPicUrl">
-        <UploadImg v-model="formData.bigPicUrl" :limit="1" :is-show-tip="false" />
-        <div style="font-size: 10px" class="pl-10px">推荐 468x340 图片分辨率</div>
-      </el-form-item>
       <el-form-item label="分类排序" prop="sort">
         <el-input-number v-model="formData.sort" controls-position="right" :min="0" />
       </el-form-item>
@@ -68,7 +64,6 @@ const formData = ref({
   id: undefined,
   name: '',
   picUrl: '',
-  bigPicUrl: '',
   status: CommonStatusEnum.ENABLE
 })
 const formRules = reactive({
@@ -133,7 +128,6 @@ const resetForm = () => {
     id: undefined,
     name: '',
     picUrl: '',
-    bigPicUrl: '',
     status: CommonStatusEnum.ENABLE
   }
   formRef.value?.resetFields()

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

@@ -38,7 +38,7 @@
       <el-table-column label="名称" min-width="240" prop="name" sortable />
       <el-table-column label="分类图标" align="center" min-width="80" prop="picUrl">
         <template #default="scope">
-          <img v-if="scope.row.picUrl" :src="scope.row.picUrl" alt="移动端分类图" class="h-36px" />
+          <img :src="scope.row.picUrl" alt="移动端分类图" class="h-36px" />
         </template>
       </el-table-column>
       <el-table-column label="排序" align="center" min-width="150" prop="sort" />

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

@@ -9,7 +9,7 @@
       label-width="68px"
     >
       <el-form-item label="属性项" prop="propertyId">
-        <el-select v-model="queryParams.propertyId" class="!w-240px">
+        <el-select v-model="queryParams.propertyId" class="!w-240px" disabled>
           <el-option
             v-for="item in propertyOptions"
             :key="item.id"
@@ -158,6 +158,6 @@ const handleDelete = async (id: number) => {
 onMounted(async () => {
   await getList()
   // 属性项下拉框数据
-  propertyOptions.value = await PropertyApi.getPropertyList({})
+  propertyOptions.value.push(await PropertyApi.getProperty(queryParams.propertyId))
 })
 </script>

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

@@ -8,9 +8,9 @@
     max-height="500"
     size="small"
   >
-    <el-table-column align="center" fixed="left" label="图片" min-width="100">
+    <el-table-column align="center" label="图片" min-width="65">
       <template #default="{ row }">
-        <UploadImg v-model="row.picUrl" height="80px" width="100%" />
+        <UploadImg v-model="row.picUrl" height="50px" width="50px" />
       </template>
     </el-table-column>
     <template v-if="formData!.specType && !isBatch">
@@ -34,12 +34,19 @@
         <el-input v-model="row.barCode" class="w-100%" />
       </template>
     </el-table-column>
-    <el-table-column align="center" label="销售价(元)" min-width="168">
+    <el-table-column align="center" label="销售价" min-width="168">
       <template #default="{ row }">
-        <el-input-number v-model="row.price" :min="0" :precision="2" :step="0.1" class="w-100%" />
+        <el-input-number
+          v-model="row.price"
+          :min="0"
+          :precision="2"
+          :step="0.1"
+          class="w-100%"
+          controls-position="right"
+        />
       </template>
     </el-table-column>
-    <el-table-column align="center" label="市场价(元)" min-width="168">
+    <el-table-column align="center" label="市场价" min-width="168">
       <template #default="{ row }">
         <el-input-number
           v-model="row.marketPrice"
@@ -47,10 +54,11 @@
           :precision="2"
           :step="0.1"
           class="w-100%"
+          controls-position="right"
         />
       </template>
     </el-table-column>
-    <el-table-column align="center" label="成本价(元)" min-width="168">
+    <el-table-column align="center" label="成本价" min-width="168">
       <template #default="{ row }">
         <el-input-number
           v-model="row.costPrice"
@@ -58,22 +66,37 @@
           :precision="2"
           :step="0.1"
           class="w-100%"
+          controls-position="right"
         />
       </template>
     </el-table-column>
     <el-table-column align="center" label="库存" min-width="168">
       <template #default="{ row }">
-        <el-input-number v-model="row.stock" :min="0" class="w-100%" />
+        <el-input-number v-model="row.stock" :min="0" class="w-100%" controls-position="right" />
       </template>
     </el-table-column>
     <el-table-column align="center" label="重量(kg)" min-width="168">
       <template #default="{ row }">
-        <el-input-number v-model="row.weight" :min="0" :precision="2" :step="0.1" class="w-100%" />
+        <el-input-number
+          v-model="row.weight"
+          :min="0"
+          :precision="2"
+          :step="0.1"
+          class="w-100%"
+          controls-position="right"
+        />
       </template>
     </el-table-column>
     <el-table-column align="center" label="体积(m^3)" min-width="168">
       <template #default="{ row }">
-        <el-input-number v-model="row.volume" :min="0" :precision="2" :step="0.1" class="w-100%" />
+        <el-input-number
+          v-model="row.volume"
+          :min="0"
+          :precision="2"
+          :step="0.1"
+          class="w-100%"
+          controls-position="right"
+        />
       </template>
     </el-table-column>
     <template v-if="formData!.subCommissionType">
@@ -85,6 +108,7 @@
             :precision="2"
             :step="0.1"
             class="w-100%"
+            controls-position="right"
           />
         </template>
       </el-table-column>
@@ -96,6 +120,7 @@
             :precision="2"
             :step="0.1"
             class="w-100%"
+            controls-position="right"
           />
         </template>
       </el-table-column>
@@ -124,7 +149,12 @@
     <el-table-column v-if="isComponent" type="selection" width="45" />
     <el-table-column align="center" label="图片" min-width="80">
       <template #default="{ row }">
-        <el-image :src="row.picUrl" class="h-60px w-60px" @click="imagePreview(row.picUrl)" />
+        <el-image
+          v-if="row.picUrl"
+          :src="row.picUrl"
+          class="h-50px w-50px"
+          @click="imagePreview(row.picUrl)"
+        />
       </template>
     </el-table-column>
     <template v-if="formData!.specType && !isBatch">

+ 0 - 66
src/views/mall/product/spu/form/ActivityOrdersSort.vue

@@ -1,66 +0,0 @@
-<template>
-  <div ref="elTagWrappingRef">
-    <template v-if="activityOrders && activityOrders.length > 0">
-      <el-tag
-        v-for="activityType in activityOrders"
-        :key="activityType"
-        :type="promotionTypes.find((item) => item.value === activityType)?.colorType"
-        class="mr-[10px]"
-      >
-        {{ promotionTypes.find((item) => item.value === activityType)?.label }}
-      </el-tag>
-    </template>
-    <template v-else>
-      <el-tag
-        v-for="type in promotionTypes"
-        :key="type.value as number"
-        :type="type.colorType"
-        class="mr-[10px]"
-      >
-        {{ type.label }}
-      </el-tag>
-    </template>
-  </div>
-</template>
-<script lang="ts" setup>
-import Sortable from 'sortablejs'
-import type { DictDataType } from '@/utils/dict'
-
-defineOptions({ name: 'ActivityOrdersSort' })
-const props = defineProps<{
-  promotionTypes: DictDataType[]
-  activityOrders: number[]
-}>()
-const emit = defineEmits<{
-  (e: 'update:activityOrders', v: number[])
-}>()
-const elTagWrappingRef = ref() // elTag 容器 Ref
-
-const initSortable = () => {
-  new Sortable(elTagWrappingRef.value, {
-    swapThreshold: 1,
-    animation: 150,
-    onEnd: (el) => {
-      const innerText = el.to.innerText
-      // 将字符串按换行符分割成数组
-      const activityOrder = innerText.split('\n')
-      // 根据字符串中的顺序重新排序数组
-      const sortedActivityOrder = activityOrder.map((activityName) => {
-        return props.promotionTypes.find((item) => item.label === activityName)?.value
-      })
-      emit('update:activityOrders', sortedActivityOrder as number[])
-    }
-  })
-}
-onMounted(async () => {
-  await nextTick()
-  // 如果活动排序为空也就是新增的时候加入活动
-  if (props.activityOrders && props.activityOrders.length === 0) {
-    emit(
-      'update:activityOrders',
-      props.promotionTypes.map((item) => item.value as number)
-    )
-  }
-  initSortable()
-})
-</script>

+ 0 - 375
src/views/mall/product/spu/form/BasicInfoForm.vue

@@ -1,375 +0,0 @@
-<template>
-  <!-- 情况一:添加/修改 -->
-  <el-form
-    v-if="!isDetail"
-    ref="productSpuBasicInfoRef"
-    :model="formData"
-    :rules="rules"
-    label-width="120px"
-  >
-    <el-row>
-      <el-col :span="12">
-        <el-form-item label="商品名称" prop="name">
-          <el-input v-model="formData.name" placeholder="请输入商品名称" />
-        </el-form-item>
-      </el-col>
-      <el-col :span="12">
-        <el-form-item label="商品分类" prop="categoryId">
-          <el-cascader
-            v-model="formData.categoryId"
-            :options="categoryList"
-            :props="defaultProps"
-            class="w-1/1"
-            clearable
-            placeholder="请选择商品分类"
-            filterable
-          />
-        </el-form-item>
-      </el-col>
-      <el-col :span="12">
-        <el-form-item label="商品关键字" prop="keyword">
-          <el-input v-model="formData.keyword" placeholder="请输入商品关键字" />
-        </el-form-item>
-      </el-col>
-      <el-col :span="12">
-        <el-form-item label="单位" prop="unit">
-          <el-select v-model="formData.unit" class="w-1/1" placeholder="请选择单位">
-            <el-option
-              v-for="dict in getIntDictOptions(DICT_TYPE.PRODUCT_UNIT)"
-              :key="dict.value"
-              :label="dict.label"
-              :value="dict.value"
-            />
-          </el-select>
-        </el-form-item>
-      </el-col>
-      <el-col :span="12">
-        <el-form-item label="商品简介" prop="introduction">
-          <el-input
-            v-model="formData.introduction"
-            :rows="3"
-            placeholder="请输入商品简介"
-            type="textarea"
-          />
-        </el-form-item>
-      </el-col>
-      <el-col :span="12">
-        <el-form-item label="商品封面图" prop="picUrl">
-          <UploadImg v-model="formData.picUrl" height="80px" />
-        </el-form-item>
-      </el-col>
-      <el-col :span="24">
-        <el-form-item label="商品轮播图" prop="sliderPicUrls">
-          <UploadImgs v-model:modelValue="formData.sliderPicUrls" />
-        </el-form-item>
-      </el-col>
-      <el-col :span="12">
-        <el-form-item label="运费模板" prop="deliveryTemplateId">
-          <el-select v-model="formData.deliveryTemplateId" placeholder="请选择">
-            <el-option
-              v-for="item in deliveryTemplateList"
-              :key="item.id"
-              :label="item.name"
-              :value="item.id"
-            />
-          </el-select>
-        </el-form-item>
-      </el-col>
-      <el-col :span="12">
-        <el-form-item label="品牌" prop="brandId">
-          <el-select v-model="formData.brandId" placeholder="请选择">
-            <el-option
-              v-for="item in brandList"
-              :key="item.id"
-              :label="item.name"
-              :value="item.id"
-            />
-          </el-select>
-        </el-form-item>
-      </el-col>
-      <el-col :span="12">
-        <el-form-item label="商品规格" props="specType">
-          <el-radio-group v-model="formData.specType" @change="onChangeSpec">
-            <el-radio :label="false" class="radio">单规格</el-radio>
-            <el-radio :label="true">多规格</el-radio>
-          </el-radio-group>
-        </el-form-item>
-      </el-col>
-      <el-col :span="12">
-        <el-form-item label="分销类型" props="subCommissionType">
-          <el-radio-group v-model="formData.subCommissionType" @change="changeSubCommissionType">
-            <el-radio :label="false">默认设置</el-radio>
-            <el-radio :label="true" class="radio">单独设置</el-radio>
-          </el-radio-group>
-        </el-form-item>
-      </el-col>
-      <!-- 多规格添加-->
-      <el-col :span="24">
-        <el-form-item v-if="!formData.specType">
-          <SkuList
-            ref="skuListRef"
-            :prop-form-data="formData"
-            :propertyList="propertyList"
-            :rule-config="ruleConfig"
-          />
-        </el-form-item>
-        <el-form-item v-if="formData.specType" label="商品属性">
-          <el-button class="mb-10px mr-15px" @click="attributesAddFormRef.open">添加属性</el-button>
-          <ProductAttributes :propertyList="propertyList" @success="generateSkus" />
-        </el-form-item>
-        <template v-if="formData.specType && propertyList.length > 0">
-          <el-form-item label="批量设置">
-            <SkuList :is-batch="true" :prop-form-data="formData" :propertyList="propertyList" />
-          </el-form-item>
-          <el-form-item label="属性列表">
-            <SkuList
-              ref="skuListRef"
-              :prop-form-data="formData"
-              :propertyList="propertyList"
-              :rule-config="ruleConfig"
-            />
-          </el-form-item>
-        </template>
-      </el-col>
-    </el-row>
-  </el-form>
-
-  <!-- 情况二:详情 -->
-  <Descriptions v-if="isDetail" :data="formData" :schema="allSchemas.detailSchema">
-    <template #categoryId="{ row }"> {{ formatCategoryName(row.categoryId) }}</template>
-    <template #brandId="{ row }">
-      {{ brandList.find((item) => item.id === row.brandId)?.name }}
-    </template>
-    <template #deliveryTemplateId="{ row }">
-      {{ deliveryTemplateList.find((item) => item.id === row.deliveryTemplateId)?.name }}
-    </template>
-    <template #specType="{ row }">
-      {{ row.specType ? '多规格' : '单规格' }}
-    </template>
-    <template #subCommissionType="{ row }">
-      {{ row.subCommissionType ? '单独设置' : '默认设置' }}
-    </template>
-    <template #picUrl="{ row }">
-      <el-image :src="row.picUrl" class="h-60px w-60px" @click="imagePreview(row.picUrl)" />
-    </template>
-    <template #sliderPicUrls="{ row }">
-      <el-image
-        v-for="(item, index) in row.sliderPicUrls"
-        :key="index"
-        :src="item.url"
-        class="mr-10px h-60px w-60px"
-        @click="imagePreview(row.sliderPicUrls)"
-      />
-    </template>
-    <template #skus>
-      <SkuList
-        ref="skuDetailListRef"
-        :is-detail="isDetail"
-        :prop-form-data="formData"
-        :propertyList="propertyList"
-      />
-    </template>
-  </Descriptions>
-
-  <!-- 商品属性添加 Form 表单 -->
-  <ProductPropertyAddForm ref="attributesAddFormRef" :propertyList="propertyList" />
-</template>
-<script lang="ts" setup>
-import { PropType } from 'vue'
-import { isArray } from '@/utils/is'
-import { copyValueToTarget } from '@/utils'
-import { propTypes } from '@/utils/propTypes'
-import { checkSelectedNode, defaultProps, handleTree, treeToString } from '@/utils/tree'
-import { createImageViewer } from '@/components/ImageViewer'
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import { getPropertyList, RuleConfig, SkuList } from '@/views/mall/product/spu/components/index.ts'
-import ProductAttributes from './ProductAttributes.vue'
-import ProductPropertyAddForm from './ProductPropertyAddForm.vue'
-import { basicInfoSchema } from './spu.data'
-import type { Spu } from '@/api/mall/product/spu'
-import * as ProductCategoryApi from '@/api/mall/product/category'
-import * as ProductBrandApi from '@/api/mall/product/brand'
-import * as ExpressTemplateApi from '@/api/mall/trade/delivery/expressTemplate'
-
-defineOptions({ name: 'ProductSpuBasicInfoForm' })
-
-// sku 相关属性校验规则
-const ruleConfig: RuleConfig[] = [
-  {
-    name: 'stock',
-    rule: (arg) => arg >= 0,
-    message: '商品库存必须大于等于 1 !!!'
-  },
-  {
-    name: 'price',
-    rule: (arg) => arg >= 0.01,
-    message: '商品销售价格必须大于等于 0.01 元!!!'
-  },
-  {
-    name: 'marketPrice',
-    rule: (arg) => arg >= 0.01,
-    message: '商品市场价格必须大于等于 0.01 元!!!'
-  },
-  {
-    name: 'costPrice',
-    rule: (arg) => arg >= 0.01,
-    message: '商品成本价格必须大于等于 0.00 元!!!'
-  }
-]
-
-// ====== 商品详情相关操作 ======
-const { allSchemas } = useCrudSchemas(basicInfoSchema)
-/** 商品图预览 */
-const imagePreview = (args) => {
-  const urlList = []
-  if (isArray(args)) {
-    args.forEach((item) => {
-      urlList.push(item.url)
-    })
-  } else {
-    urlList.push(args)
-  }
-  createImageViewer({
-    urlList
-  })
-}
-
-// ====== end ======
-
-const message = useMessage() // 消息弹窗
-
-const props = defineProps({
-  propFormData: {
-    type: Object as PropType<Spu>,
-    default: () => {}
-  },
-  activeName: propTypes.string.def(''),
-  isDetail: propTypes.bool.def(false) // 是否作为详情组件
-})
-const attributesAddFormRef = ref() // 添加商品属性表单
-const productSpuBasicInfoRef = ref() // 表单 Ref
-const propertyList = ref([]) // 商品属性列表
-const skuListRef = ref() // 商品属性列表Ref
-/** 调用 SkuList generateTableData 方法*/
-const generateSkus = (propertyList) => {
-  skuListRef.value.generateTableData(propertyList)
-}
-const formData = reactive<Spu>({
-  name: '', // 商品名称
-  categoryId: null, // 商品分类
-  keyword: '', // 关键字
-  unit: null, // 单位
-  picUrl: '', // 商品封面图
-  sliderPicUrls: [], // 商品轮播图
-  introduction: '', // 商品简介
-  deliveryTemplateId: null, // 运费模版
-  brandId: null, // 商品品牌
-  specType: false, // 商品规格
-  subCommissionType: false, // 分销类型
-  skus: []
-})
-const rules = reactive({
-  name: [required],
-  categoryId: [required],
-  keyword: [required],
-  unit: [required],
-  introduction: [required],
-  picUrl: [required],
-  sliderPicUrls: [required],
-  deliveryTemplateId: [required],
-  brandId: [required],
-  specType: [required],
-  subCommissionType: [required]
-})
-
-/**
- * 将传进来的值赋值给 formData
- */
-watch(
-  () => props.propFormData,
-  (data) => {
-    if (!data) {
-      return
-    }
-    copyValueToTarget(formData, data)
-    formData.sliderPicUrls = data['sliderPicUrls']?.map((item) => ({
-      url: item
-    }))
-    propertyList.value = getPropertyList(data)
-  },
-  {
-    immediate: true
-  }
-)
-
-/**
- * 表单校验
- */
-const emit = defineEmits(['update:activeName'])
-const validate = async () => {
-  // 校验 sku
-  skuListRef.value.validateSku()
-  // 校验表单
-  if (!productSpuBasicInfoRef) return
-  return await unref(productSpuBasicInfoRef).validate((valid) => {
-    if (!valid) {
-      message.warning('商品信息未完善!!')
-      emit('update:activeName', 'basicInfo')
-      // 目的截断之后的校验
-      throw new Error('商品信息未完善!!')
-    } else {
-      // 校验通过更新数据
-      Object.assign(props.propFormData, formData)
-    }
-  })
-}
-defineExpose({ validate })
-
-/** 分销类型 */
-const changeSubCommissionType = () => {
-  // 默认为零,类型切换后也要重置为零
-  for (const item of formData.skus) {
-    item.firstBrokeragePrice = 0
-    item.secondBrokeragePrice = 0
-  }
-}
-
-/** 选择规格 */
-const onChangeSpec = () => {
-  // 重置商品属性列表
-  propertyList.value = []
-  // 重置sku列表
-  formData.skus = [
-    {
-      price: 0,
-      marketPrice: 0,
-      costPrice: 0,
-      barCode: '',
-      picUrl: '',
-      stock: 0,
-      weight: 0,
-      volume: 0,
-      firstBrokeragePrice: 0,
-      secondBrokeragePrice: 0
-    }
-  ]
-}
-
-const categoryList = ref([]) // 分类树
-/** 获取分类的节点的完整结构 */
-const formatCategoryName = (categoryId) => {
-  return treeToString(categoryList.value, categoryId)
-}
-
-const brandList = ref([]) // 精简商品品牌列表
-const deliveryTemplateList = ref([]) // 运费模版
-onMounted(async () => {
-  // 获得分类树
-  const data = await ProductCategoryApi.getCategoryList({})
-  categoryList.value = handleTree(data, 'id', 'parentId')
-  // 获取商品品牌列表
-  brandList.value = await ProductBrandApi.getSimpleBrandList()
-  // 获取运费模版
-  deliveryTemplateList.value = await ExpressTemplateApi.getSimpleTemplateList()
-})
-</script>

+ 96 - 0
src/views/mall/product/spu/form/DeliveryForm.vue

@@ -0,0 +1,96 @@
+<!-- 商品发布 - 物流设置 -->
+<template>
+  <el-form ref="formRef" :model="formData" :rules="rules" label-width="120px" :disabled="isDetail">
+    <el-form-item label="配送方式" prop="deliveryTypes">
+      <el-checkbox-group v-model="formData.deliveryTypes" class="w-80">
+        <el-checkbox
+          v-for="dict in getIntDictOptions(DICT_TYPE.TRADE_DELIVERY_TYPE)"
+          :key="dict.value"
+          :label="dict.value"
+        >
+          {{ dict.label }}
+        </el-checkbox>
+      </el-checkbox-group>
+    </el-form-item>
+    <el-form-item
+      label="运费模板"
+      prop="deliveryTemplateId"
+      v-if="formData.deliveryTypes?.includes(DeliveryTypeEnum.EXPRESS.type)"
+    >
+      <el-select placeholder="请选择运费模板" v-model="formData.deliveryTemplateId" class="w-80">
+        <el-option
+          v-for="item in deliveryTemplateList"
+          :key="item.id"
+          :label="item.name"
+          :value="item.id"
+        />
+      </el-select>
+    </el-form-item>
+  </el-form>
+</template>
+<script lang="ts" setup>
+import { PropType } from 'vue'
+import { copyValueToTarget } from '@/utils'
+import { propTypes } from '@/utils/propTypes'
+import type { Spu } from '@/api/mall/product/spu'
+import * as ExpressTemplateApi from '@/api/mall/trade/delivery/expressTemplate'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import { DeliveryTypeEnum } from '@/utils/constants'
+
+defineOptions({ name: 'ProductDeliveryForm' })
+
+const message = useMessage() // 消息弹窗
+
+const props = defineProps({
+  propFormData: {
+    type: Object as PropType<Spu>,
+    default: () => {}
+  },
+  isDetail: propTypes.bool.def(false) // 是否作为详情组件
+})
+const formRef = ref() // 表单 Ref
+const formData = reactive<Spu>({
+  deliveryTypes: [], // 配送方式
+  deliveryTemplateId: undefined // 运费模版
+})
+const rules = reactive({
+  deliveryTypes: [required],
+  deliveryTemplateId: [required]
+})
+
+/** 将传进来的值赋值给 formData */
+watch(
+  () => props.propFormData,
+  (data) => {
+    if (!data) {
+      return
+    }
+    copyValueToTarget(formData, data)
+  },
+  {
+    immediate: true
+  }
+)
+
+/** 表单校验 */
+const emit = defineEmits(['update:activeName'])
+const validate = async () => {
+  if (!formRef) return
+  try {
+    await unref(formRef)?.validate()
+    // 校验通过更新数据
+    Object.assign(props.propFormData, formData)
+  } catch (e) {
+    message.error('【物流设置】不完善,请填写相关信息')
+    emit('update:activeName', 'delivery')
+    throw e // 目的截断之后的校验
+  }
+}
+defineExpose({ validate })
+
+/** 初始化 */
+const deliveryTemplateList = ref([]) // 运费模版
+onMounted(async () => {
+  deliveryTemplateList.value = await ExpressTemplateApi.getSimpleTemplateList()
+})
+</script>

+ 19 - 47
src/views/mall/product/spu/form/DescriptionForm.vue

@@ -1,30 +1,11 @@
+<!-- 商品发布 - 商品详情 -->
 <template>
-  <!-- 情况一:添加/修改 -->
-  <el-form
-    v-if="!isDetail"
-    ref="descriptionFormRef"
-    :model="formData"
-    :rules="rules"
-    label-width="120px"
-  >
+  <el-form ref="formRef" :model="formData" :rules="rules" label-width="120px" :disabled="isDetail">
     <!--富文本编辑器组件-->
     <el-form-item label="商品详情" prop="description">
       <Editor v-model:modelValue="formData.description" />
     </el-form-item>
   </el-form>
-
-  <!-- 情况二:详情 -->
-  <Descriptions
-    v-if="isDetail"
-    :data="formData"
-    :schema="allSchemas.detailSchema"
-    class="descriptionFormDescriptions"
-  >
-    <!-- 展示 HTML 内容 -->
-    <template #description="{ row }">
-      <div v-dompurify-html="row.description" style="width: 600px"></div>
-    </template>
-  </Descriptions>
 </template>
 <script lang="ts" setup>
 import type { Spu } from '@/api/mall/product/spu'
@@ -32,13 +13,11 @@ import { Editor } from '@/components/Editor'
 import { PropType } from 'vue'
 import { propTypes } from '@/utils/propTypes'
 import { copyValueToTarget } from '@/utils'
-import { descriptionSchema } from './spu.data'
 
-defineOptions({ name: 'DescriptionForm' })
+defineOptions({ name: 'ProductDescriptionForm' })
 
 const message = useMessage() // 消息弹窗
 
-const { allSchemas } = useCrudSchemas(descriptionSchema)
 const props = defineProps({
   propFormData: {
     type: Object as PropType<Spu>,
@@ -47,7 +26,7 @@ const props = defineProps({
   activeName: propTypes.string.def(''),
   isDetail: propTypes.bool.def(false) // 是否作为详情组件
 })
-const descriptionFormRef = ref() // 表单Ref
+const formRef = ref() // 表单Ref
 const formData = ref<Spu>({
   description: '' // 商品详情
 })
@@ -55,9 +34,8 @@ const formData = ref<Spu>({
 const rules = reactive({
   description: [required]
 })
-/**
- * 富文本编辑器如果输入过再清空会有残留,需再重置一次
- */
+
+/** 富文本编辑器如果输入过再清空会有残留,需再重置一次 */
 watch(
   () => formData.value.description,
   (newValue) => {
@@ -70,9 +48,8 @@ watch(
     immediate: true
   }
 )
-/**
- * 将传进来的值赋值给formData
- */
+
+/** 将传进来的值赋值给 formData */
 watch(
   () => props.propFormData,
   (data) => {
@@ -86,24 +63,19 @@ watch(
   }
 )
 
-/**
- * 表单校验
- */
+/** 表单校验 */
 const emit = defineEmits(['update:activeName'])
 const validate = async () => {
-  // 校验表单
-  if (!descriptionFormRef) return
-  return await unref(descriptionFormRef).validate((valid) => {
-    if (!valid) {
-      message.warning('商品详情为完善!!')
-      emit('update:activeName', 'description')
-      // 目的截断之后的校验
-      throw new Error('商品详情为完善!!')
-    } else {
-      // 校验通过更新数据
-      Object.assign(props.propFormData, formData.value)
-    }
-  })
+  if (!formRef) return
+  try {
+    await unref(formRef)?.validate()
+    // 校验通过更新数据
+    Object.assign(props.propFormData, formData.value)
+  } catch (e) {
+    message.error('【商品详情】不完善,请填写相关信息')
+    emit('update:activeName', 'description')
+    throw e // 目的截断之后的校验
+  }
 }
 defineExpose({ validate })
 </script>

+ 146 - 0
src/views/mall/product/spu/form/InfoForm.vue

@@ -0,0 +1,146 @@
+<!-- 商品发布 - 基础设置 -->
+<template>
+  <el-form ref="formRef" :model="formData" :rules="rules" label-width="120px" :disabled="isDetail">
+    <el-form-item label="商品名称" prop="name">
+      <el-input
+        v-model="formData.name"
+        placeholder="请输入商品名称"
+        type="textarea"
+        :autosize="{ minRows: 2, maxRows: 2 }"
+        maxlength="64"
+        :show-word-limit="true"
+        :clearable="true"
+        class="w-80!"
+      />
+    </el-form-item>
+    <el-form-item label="商品分类" prop="categoryId">
+      <el-cascader
+        v-model="formData.categoryId"
+        :options="categoryList"
+        :props="defaultProps"
+        class="w-80"
+        clearable
+        placeholder="请选择商品分类"
+        filterable
+      />
+    </el-form-item>
+    <el-form-item label="商品品牌" prop="brandId">
+      <el-select v-model="formData.brandId" placeholder="请选择商品品牌" class="w-80">
+        <el-option
+          v-for="item in brandList"
+          :key="item.id"
+          :label="item.name"
+          :value="item.id as number"
+        />
+      </el-select>
+    </el-form-item>
+    <el-form-item label="商品关键字" prop="keyword">
+      <el-input v-model="formData.keyword" placeholder="请输入商品关键字" class="w-80!" />
+    </el-form-item>
+    <el-form-item label="商品简介" prop="introduction">
+      <el-input
+        v-model="formData.introduction"
+        placeholder="请输入商品名称"
+        type="textarea"
+        :autosize="{ minRows: 2, maxRows: 2 }"
+        maxlength="128"
+        :show-word-limit="true"
+        :clearable="true"
+        class="w-80!"
+      />
+    </el-form-item>
+    <el-form-item label="商品封面图" prop="picUrl">
+      <UploadImg v-model="formData.picUrl" height="80px" :disabled="isDetail" />
+    </el-form-item>
+    <el-form-item label="商品轮播图" prop="sliderPicUrls">
+      <UploadImgs v-model:modelValue="formData.sliderPicUrls" :disabled="isDetail" />
+    </el-form-item>
+  </el-form>
+</template>
+<script lang="ts" setup>
+import { PropType } from 'vue'
+import { copyValueToTarget } from '@/utils'
+import { propTypes } from '@/utils/propTypes'
+import { defaultProps, handleTree } from '@/utils/tree'
+import type { Spu } from '@/api/mall/product/spu'
+import * as ProductCategoryApi from '@/api/mall/product/category'
+import * as ProductBrandApi from '@/api/mall/product/brand'
+import { BrandVO } from '@/api/mall/product/brand'
+import { CategoryVO } from '@/api/mall/product/category'
+
+defineOptions({ name: 'ProductSpuInfoForm' })
+const props = defineProps({
+  propFormData: {
+    type: Object as PropType<Spu>,
+    default: () => {}
+  },
+  isDetail: propTypes.bool.def(false) // 是否作为详情组件
+})
+
+const message = useMessage() // 消息弹窗
+
+const formRef = ref() // 表单 Ref
+const formData = reactive<Spu>({
+  name: '', // 商品名称
+  categoryId: undefined, // 商品分类
+  keyword: '', // 关键字
+  picUrl: '', // 商品封面图
+  sliderPicUrls: [], // 商品轮播图
+  introduction: '', // 商品简介
+  brandId: undefined // 商品品牌
+})
+const rules = reactive({
+  name: [required],
+  categoryId: [required],
+  keyword: [required],
+  introduction: [required],
+  picUrl: [required],
+  sliderPicUrls: [required],
+  brandId: [required]
+})
+
+/** 将传进来的值赋值给 formData */
+watch(
+  () => props.propFormData,
+  (data) => {
+    if (!data) {
+      return
+    }
+    copyValueToTarget(formData, data)
+    // TODO @puhui999:优化多文件上传,看看有没可能搞成返回 v-model 图片列表这种
+    formData.sliderPicUrls = data['sliderPicUrls']?.map((item) => ({
+      url: item
+    }))
+  },
+  {
+    immediate: true
+  }
+)
+
+/** 表单校验 */
+const emit = defineEmits(['update:activeName'])
+const validate = async () => {
+  if (!formRef) return
+  try {
+    await unref(formRef)?.validate()
+    // 校验通过更新数据
+    Object.assign(props.propFormData, formData)
+  } catch (e) {
+    message.error('【基础设置】不完善,请填写相关信息')
+    emit('update:activeName', 'info')
+    throw e // 目的截断之后的校验
+  }
+}
+defineExpose({ validate })
+
+/** 初始化 */
+const brandList = ref<BrandVO[]>([]) // 商品品牌列表
+const categoryList = ref<CategoryVO[]>([]) // 商品分类树
+onMounted(async () => {
+  // 获得分类树
+  const data = await ProductCategoryApi.getCategoryList({})
+  categoryList.value = handleTree(data, 'id')
+  // 获取商品品牌列表
+  brandList.value = await ProductBrandApi.getSimpleBrandList()
+})
+</script>

+ 91 - 0
src/views/mall/product/spu/form/OtherForm.vue

@@ -0,0 +1,91 @@
+<!-- 商品发布 - 其它设置 -->
+<template>
+  <el-form ref="formRef" :model="formData" :rules="rules" label-width="120px" :disabled="isDetail">
+    <el-form-item label="商品排序" prop="sort">
+      <el-input-number
+        v-model="formData.sort"
+        :min="0"
+        placeholder="请输入商品排序"
+        class="w-80!"
+      />
+    </el-form-item>
+    <el-form-item label="赠送积分" prop="giveIntegral">
+      <el-input-number
+        v-model="formData.giveIntegral"
+        :min="0"
+        placeholder="请输入赠送积分"
+        class="w-80!"
+      />
+    </el-form-item>
+    <el-form-item label="虚拟销量" prop="virtualSalesCount">
+      <el-input-number
+        v-model="formData.virtualSalesCount"
+        :min="0"
+        placeholder="请输入虚拟销量"
+        class="w-80!"
+      />
+    </el-form-item>
+  </el-form>
+</template>
+<script lang="ts" setup>
+import type { Spu } from '@/api/mall/product/spu'
+import { PropType } from 'vue'
+import { propTypes } from '@/utils/propTypes'
+import { copyValueToTarget } from '@/utils'
+
+defineOptions({ name: 'ProductOtherForm' })
+
+const message = useMessage() // 消息弹窗
+
+const props = defineProps({
+  propFormData: {
+    type: Object as PropType<Spu>,
+    default: () => {}
+  },
+  isDetail: propTypes.bool.def(false) // 是否作为详情组件
+})
+
+const formRef = ref() // 表单Ref
+// 表单数据
+const formData = ref<Spu>({
+  sort: 0, // 商品排序
+  giveIntegral: 0, // 赠送积分
+  virtualSalesCount: 0 // 虚拟销量
+})
+// 表单规则
+const rules = reactive({
+  sort: [required],
+  giveIntegral: [required],
+  virtualSalesCount: [required]
+})
+
+/** 将传进来的值赋值给 formData */
+watch(
+  () => props.propFormData,
+  (data) => {
+    if (!data) {
+      return
+    }
+    copyValueToTarget(formData.value, data)
+  },
+  {
+    immediate: true
+  }
+)
+
+/** 表单校验 */
+const emit = defineEmits(['update:activeName'])
+const validate = async () => {
+  if (!formRef) return
+  try {
+    await unref(formRef)?.validate()
+    // 校验通过更新数据
+    Object.assign(props.propFormData, formData.value)
+  } catch (e) {
+    message.error('【其它设置】不完善,请填写相关信息')
+    emit('update:activeName', 'other')
+    throw e // 目的截断之后的校验
+  }
+}
+defineExpose({ validate })
+</script>

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

@@ -1,209 +0,0 @@
-<template>
-  <!-- 情况一:添加/修改 -->
-  <el-form
-    v-if="!isDetail"
-    ref="otherSettingsFormRef"
-    :model="formData"
-    :rules="rules"
-    label-width="120px"
-  >
-    <el-row>
-      <el-col :span="24">
-        <el-row :gutter="20">
-          <el-col :span="8">
-            <el-form-item label="商品排序" prop="sort">
-              <el-input-number v-model="formData.sort" :min="0" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="8">
-            <el-form-item label="赠送积分" prop="giveIntegral">
-              <el-input-number v-model="formData.giveIntegral" :min="0" />
-            </el-form-item>
-          </el-col>
-          <el-col :span="8">
-            <el-form-item label="虚拟销量" prop="virtualSalesCount">
-              <el-input-number
-                v-model="formData.virtualSalesCount"
-                :min="0"
-                placeholder="请输入虚拟销量"
-              />
-            </el-form-item>
-          </el-col>
-        </el-row>
-      </el-col>
-      <el-col :span="24">
-        <el-form-item label="商品推荐">
-          <el-checkbox-group v-model="checkboxGroup" @change="onChangeGroup">
-            <el-checkbox v-for="(item, index) in recommendOptions" :key="index" :label="item.value">
-              {{ item.name }}
-            </el-checkbox>
-          </el-checkbox-group>
-        </el-form-item>
-      </el-col>
-      <el-col :span="24">
-        <el-form-item label="活动优先级">
-          <ActivityOrdersSort
-            v-model:activity-orders="formData.activityOrders"
-            :promotion-types="promotionTypes"
-          />
-        </el-form-item>
-      </el-col>
-    </el-row>
-  </el-form>
-
-  <!-- 情况二:详情 -->
-  <Descriptions v-if="isDetail" :data="formData" :schema="allSchemas.detailSchema">
-    <template #recommendHot="{ row }">
-      <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.recommendHot" />
-    </template>
-    <template #recommendBenefit="{ row }">
-      <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.recommendBenefit" />
-    </template>
-    <template #recommendBest="{ row }">
-      <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.recommendBest" />
-    </template>
-    <template #recommendNew="{ row }">
-      <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.recommendNew" />
-    </template>
-    <template #recommendGood="{ row }">
-      <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="row.recommendGood" />
-    </template>
-    <template #activityOrders="{ row }">
-      <el-tag
-        v-for="activityType in row.activityOrders"
-        :key="activityType"
-        :type="promotionTypes.find((item) => item.value === activityType)?.colorType"
-        class="mr-[10px]"
-      >
-        {{ promotionTypes.find((item) => item.value === activityType)?.label }}
-      </el-tag>
-    </template>
-  </Descriptions>
-</template>
-<script lang="ts" setup>
-import type { Spu } from '@/api/mall/product/spu'
-import { PropType } from 'vue'
-import { propTypes } from '@/utils/propTypes'
-import { copyValueToTarget } from '@/utils'
-import { otherSettingsSchema } from './spu.data'
-import { DICT_TYPE, DictDataType } from '@/utils/dict'
-import ActivityOrdersSort from './ActivityOrdersSort.vue'
-
-defineOptions({ name: 'OtherSettingsForm' })
-
-const message = useMessage() // 消息弹窗
-
-const { allSchemas } = useCrudSchemas(otherSettingsSchema)
-
-const props = defineProps({
-  propFormData: {
-    type: Object as PropType<Spu>,
-    default: () => {}
-  },
-  activeName: propTypes.string.def(''),
-  isDetail: propTypes.bool.def(false) // 是否作为详情组件
-})
-
-// TODO @puhui999:这个目前先写死;主要是,这个优惠类型不好用 promotion_type_enum;因为优惠劵、会员折扣都算
-// 活动优先级处理
-const promotionTypes = ref<DictDataType[]>([
-  {
-    dictType: 'promotionTypes',
-    label: '秒杀',
-    value: 1,
-    colorType: 'warning',
-    cssClass: ''
-  },
-  {
-    dictType: 'promotionTypes',
-    label: '砍价',
-    value: 2,
-    colorType: 'warning',
-    cssClass: ''
-  },
-  {
-    dictType: 'promotionTypes',
-    label: '拼团',
-    value: 3,
-    colorType: 'warning',
-    cssClass: ''
-  }
-])
-
-const otherSettingsFormRef = ref() // 表单Ref
-// 表单数据
-const formData = ref<Spu>({
-  sort: 1, // 商品排序
-  giveIntegral: 1, // 赠送积分
-  virtualSalesCount: 1, // 虚拟销量
-  recommendHot: false, // 是否热卖
-  recommendBenefit: false, // 是否优惠
-  recommendBest: false, // 是否精品
-  recommendNew: false, // 是否新品
-  recommendGood: false, // 是否优品
-  activityOrders: [] // 活动排序
-})
-// 表单规则
-const rules = reactive({
-  sort: [required],
-  giveIntegral: [required],
-  virtualSalesCount: [required]
-})
-const recommendOptions = [
-  { name: '是否热卖', value: 'recommendHot' },
-  { name: '是否优惠', value: 'recommendBenefit' },
-  { name: '是否精品', value: 'recommendBest' },
-  { name: '是否新品', value: 'recommendNew' },
-  { name: '是否优品', value: 'recommendGood' }
-] // 商品推荐选项
-const checkboxGroup = ref<string[]>([]) // 选中的推荐选项
-
-/** 选择商品后赋值 */
-const onChangeGroup = () => {
-  recommendOptions.forEach(({ value }) => {
-    formData.value[value] = checkboxGroup.value.includes(value)
-  })
-}
-
-/**
- * 将传进来的值赋值给formData
- */
-watch(
-  () => props.propFormData,
-  (data) => {
-    if (!data) {
-      return
-    }
-    copyValueToTarget(formData.value, data)
-    recommendOptions.forEach(({ value }) => {
-      if (formData.value[value] && !checkboxGroup.value.includes(value)) {
-        checkboxGroup.value.push(value)
-      }
-    })
-  },
-  {
-    immediate: true
-  }
-)
-
-/**
- * 表单校验
- */
-const emit = defineEmits(['update:activeName'])
-const validate = async () => {
-  // 校验表单
-  if (!otherSettingsFormRef) return
-  return await unref(otherSettingsFormRef).validate((valid) => {
-    if (!valid) {
-      message.warning('商品其他设置未完善!!')
-      emit('update:activeName', 'otherSettings')
-      // 目的截断之后的校验
-      throw new Error('商品其他设置未完善!!')
-    } else {
-      // 校验通过更新数据
-      Object.assign(props.propFormData, formData.value)
-    }
-  })
-}
-defineExpose({ validate })
-</script>

+ 19 - 13
src/views/mall/product/spu/form/ProductAttributes.vue

@@ -1,9 +1,10 @@
+<!-- 商品发布 - 库存价格 - 属性列表 -->
 <template>
   <el-col v-for="(item, index) in attributeList" :key="index">
     <div>
       <el-text class="mx-1">属性名:</el-text>
-      <el-tag class="mx-1" closable type="success" @close="handleCloseProperty(index)"
-        >{{ item.name }}
+      <el-tag class="mx-1" :closable="!isDetail" type="success" @close="handleCloseProperty(index)">
+        {{ item.name }}
       </el-tag>
     </div>
     <div>
@@ -12,7 +13,7 @@
         v-for="(value, valueIndex) in item.values"
         :key="value.id"
         class="mx-1"
-        closable
+        :closable="!isDetail"
         @close="handleCloseValue(index, valueIndex)"
       >
         {{ value.name }}
@@ -43,6 +44,9 @@
 <script lang="ts" setup>
 import { ElInput } from 'element-plus'
 import * as PropertyApi from '@/api/mall/product/property'
+import { PropertyVO } from '@/api/mall/product/property'
+import { PropertyAndValues } from '@/views/mall/product/spu/components'
+import { propTypes } from '@/utils/propTypes'
 
 defineOptions({ name: 'ProductAttributes' })
 
@@ -51,7 +55,7 @@ const message = useMessage() // 消息弹窗
 const inputValue = ref('') // 输入框值
 const attributeIndex = ref<number | null>(null) // 获取焦点时记录当前属性项的index
 // 输入框显隐控制
-const inputVisible = computed(() => (index) => {
+const inputVisible = computed(() => (index: number) => {
   if (attributeIndex.value === null) return false
   if (attributeIndex.value === index) return true
 })
@@ -59,17 +63,18 @@ const inputRef = ref([]) //标签输入框Ref
 /** 解决 ref 在 v-for 中的获取问题*/
 const setInputRef = (el) => {
   if (el === null || typeof el === 'undefined') return
-  // 如果不存在id相同的元素才添加
+  // 如果不存在 id 相同的元素才添加
   if (!inputRef.value.some((item) => item.input?.attributes.id === el.input?.attributes.id)) {
     inputRef.value.push(el)
   }
 }
-const attributeList = ref([]) // 商品属性列表
+const attributeList = ref<PropertyAndValues[]>([]) // 商品属性列表
 const props = defineProps({
   propertyList: {
     type: Array,
     default: () => {}
-  }
+  },
+  isDetail: propTypes.bool.def(false) // 是否作为详情组件
 })
 
 watch(
@@ -85,23 +90,24 @@ watch(
 )
 
 /** 删除属性值*/
-const handleCloseValue = (index, valueIndex) => {
+const handleCloseValue = (index: number, valueIndex: number) => {
   attributeList.value[index].values?.splice(valueIndex, 1)
 }
+
 /** 删除属性*/
-const handleCloseProperty = (index) => {
+const handleCloseProperty = (index: number) => {
   attributeList.value?.splice(index, 1)
 }
+
 /** 显示输入框并获取焦点 */
 const showInput = async (index) => {
   attributeIndex.value = index
   inputRef.value[index].focus()
 }
 
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
-
 /** 输入框失去焦点或点击回车时触发 */
-const handleInputConfirm = async (index, propertyId) => {
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const handleInputConfirm = async (index: number, propertyId: number) => {
   if (inputValue.value) {
     // 保存属性值
     try {
@@ -110,7 +116,7 @@ const handleInputConfirm = async (index, propertyId) => {
       message.success(t('common.createSuccess'))
       emit('success', attributeList.value)
     } catch {
-      message.error('添加失败,请重试') // TODO 缺少国际化
+      message.error('添加失败,请重试')
     }
   }
   attributeIndex.value = null

+ 12 - 17
src/views/mall/product/spu/form/ProductPropertyAddForm.vue

@@ -1,5 +1,6 @@
+<!-- 商品发布 - 库存价格 - 添加属性 -->
 <template>
-  <Dialog v-model="dialogVisible" :title="dialogTitle">
+  <Dialog v-model="dialogVisible" title="添加商品属性">
     <el-form
       ref="formRef"
       v-loading="formLoading"
@@ -26,8 +27,7 @@ const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 
 const dialogVisible = ref(false) // 弹窗的是否展示
-const dialogTitle = ref('添加商品属性') // 弹窗的标题
-const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formLoading = ref(false) // 表单的加载中
 const formData = ref({
   name: ''
 })
@@ -44,7 +44,7 @@ const props = defineProps({
 })
 
 watch(
-  () => props.propertyList,
+  () => props.propertyList, // 解决 props 无法直接修改父组件的问题
   (data) => {
     if (!data) return
     attributeList.value = data
@@ -54,6 +54,7 @@ watch(
     immediate: true
   }
 )
+
 /** 打开弹窗 */
 const open = async () => {
   dialogVisible.value = true
@@ -71,19 +72,13 @@ const submitForm = async () => {
   formLoading.value = true
   try {
     const data = formData.value as PropertyApi.PropertyVO
-    // 检查属性是否已存在,如果有则返回属性和其下属性值
-    const res = await PropertyApi.getPropertyListAndValue({ name: data.name })
-    if (res.length === 0) {
-      const propertyId = await PropertyApi.createProperty(data)
-      attributeList.value.push({ id: propertyId, ...formData.value, values: [] })
-    } else {
-      if (res[0].values === null) {
-        res[0].values = []
-      }
-      // 不需要属性值
-      res[0].values = []
-      attributeList.value.push(res[0]) // 因为只用一个
-    }
+    const propertyId = await PropertyApi.createProperty(data)
+    // 添加到属性列表
+    attributeList.value.push({
+      id: propertyId,
+      ...formData.value,
+      values: []
+    })
     message.success(t('common.createSuccess'))
     dialogVisible.value = false
   } finally {

+ 187 - 0
src/views/mall/product/spu/form/SkuForm.vue

@@ -0,0 +1,187 @@
+<!-- 商品发布 - 库存价格 -->
+<template>
+  <el-form ref="formRef" :model="formData" :rules="rules" label-width="120px" :disabled="isDetail">
+    <el-form-item label="分销类型" props="subCommissionType">
+      <el-radio-group
+        v-model="formData.subCommissionType"
+        @change="changeSubCommissionType"
+        class="w-80"
+      >
+        <el-radio :label="false">默认设置</el-radio>
+        <el-radio :label="true" class="radio">单独设置</el-radio>
+      </el-radio-group>
+    </el-form-item>
+    <el-form-item label="商品规格" props="specType">
+      <el-radio-group v-model="formData.specType" @change="onChangeSpec" class="w-80">
+        <el-radio :label="false" class="radio">单规格</el-radio>
+        <el-radio :label="true">多规格</el-radio>
+      </el-radio-group>
+    </el-form-item>
+    <!-- 多规格添加-->
+    <el-form-item v-if="!formData.specType">
+      <SkuList
+        ref="skuListRef"
+        :prop-form-data="formData"
+        :property-list="propertyList"
+        :rule-config="ruleConfig"
+      />
+    </el-form-item>
+    <el-form-item v-if="formData.specType" label="商品属性">
+      <el-button class="mb-10px mr-15px" @click="attributesAddFormRef.open">添加属性</el-button>
+      <ProductAttributes
+        :property-list="propertyList"
+        @success="generateSkus"
+        :is-detail="isDetail"
+      />
+    </el-form-item>
+    <template v-if="formData.specType && propertyList.length > 0">
+      <el-form-item label="批量设置" v-if="!isDetail">
+        <SkuList :is-batch="true" :prop-form-data="formData" :property-list="propertyList" />
+      </el-form-item>
+      <el-form-item label="规格列表">
+        <SkuList
+          ref="skuListRef"
+          :prop-form-data="formData"
+          :property-list="propertyList"
+          :rule-config="ruleConfig"
+          :is-detail="isDetail"
+        />
+      </el-form-item>
+    </template>
+  </el-form>
+
+  <!-- 商品属性添加 Form 表单 -->
+  <ProductPropertyAddForm ref="attributesAddFormRef" :propertyList="propertyList" />
+</template>
+<script lang="ts" setup>
+import { PropType } from 'vue'
+import { copyValueToTarget } from '@/utils'
+import { propTypes } from '@/utils/propTypes'
+import {
+  getPropertyList,
+  PropertyAndValues,
+  RuleConfig,
+  SkuList
+} from '@/views/mall/product/spu/components/index'
+import ProductAttributes from './ProductAttributes.vue'
+import ProductPropertyAddForm from './ProductPropertyAddForm.vue'
+import type { Spu } from '@/api/mall/product/spu'
+
+defineOptions({ name: 'ProductSpuSkuForm' })
+
+// sku 相关属性校验规则
+const ruleConfig: RuleConfig[] = [
+  {
+    name: 'stock',
+    rule: (arg) => arg >= 0,
+    message: '商品库存必须大于等于 1 !!!'
+  },
+  {
+    name: 'price',
+    rule: (arg) => arg >= 0.01,
+    message: '商品销售价格必须大于等于 0.01 元!!!'
+  },
+  {
+    name: 'marketPrice',
+    rule: (arg) => arg >= 0.01,
+    message: '商品市场价格必须大于等于 0.01 元!!!'
+  },
+  {
+    name: 'costPrice',
+    rule: (arg) => arg >= 0.01,
+    message: '商品成本价格必须大于等于 0.00 元!!!'
+  }
+]
+
+const message = useMessage() // 消息弹窗
+
+const props = defineProps({
+  propFormData: {
+    type: Object as PropType<Spu>,
+    default: () => {}
+  },
+  isDetail: propTypes.bool.def(false) // 是否作为详情组件
+})
+const attributesAddFormRef = ref() // 添加商品属性表单
+const formRef = ref() // 表单 Ref
+const propertyList = ref<PropertyAndValues[]>([]) // 商品属性列表
+const skuListRef = ref() // 商品属性列表 Ref
+const formData = reactive<Spu>({
+  specType: false, // 商品规格
+  subCommissionType: false, // 分销类型
+  skus: []
+})
+const rules = reactive({
+  specType: [required],
+  subCommissionType: [required]
+})
+
+/** 将传进来的值赋值给 formData */
+watch(
+  () => props.propFormData,
+  (data) => {
+    if (!data) {
+      return
+    }
+    copyValueToTarget(formData, data)
+    // 将 SKU 的属性,整理成 PropertyAndValues 数组
+    propertyList.value = getPropertyList(data)
+  },
+  {
+    immediate: true
+  }
+)
+
+/** 表单校验 */
+const emit = defineEmits(['update:activeName'])
+const validate = async () => {
+  if (!formRef) return
+  try {
+    // 校验 sku
+    skuListRef.value.validateSku()
+    await unref(formRef).validate()
+    // 校验通过更新数据
+    Object.assign(props.propFormData, formData)
+  } catch (e) {
+    message.error('【库存价格】不完善,请填写相关信息')
+    emit('update:activeName', 'sku')
+    throw e // 目的截断之后的校验
+  }
+}
+defineExpose({ validate })
+
+/** 分销类型 */
+const changeSubCommissionType = () => {
+  // 默认为零,类型切换后也要重置为零
+  for (const item of formData.skus!) {
+    item.firstBrokeragePrice = 0
+    item.secondBrokeragePrice = 0
+  }
+}
+
+/** 选择规格 */
+const onChangeSpec = () => {
+  // 重置商品属性列表
+  propertyList.value = []
+  // 重置sku列表
+  formData.skus = [
+    {
+      price: 0,
+      marketPrice: 0,
+      costPrice: 0,
+      barCode: '',
+      picUrl: '',
+      stock: 0,
+      weight: 0,
+      volume: 0,
+      firstBrokeragePrice: 0,
+      secondBrokeragePrice: 0
+    }
+  ]
+}
+
+/** 调用 SkuList generateTableData 方法*/
+const generateSkus = (propertyList) => {
+  skuListRef.value.generateTableData(propertyList)
+}
+</script>

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

@@ -1,9 +1,25 @@
 <template>
   <ContentWrap v-loading="formLoading">
     <el-tabs v-model="activeName">
-      <el-tab-pane label="商品信息" name="basicInfo">
-        <BasicInfoForm
-          ref="basicInfoRef"
+      <el-tab-pane label="基础设置" name="info">
+        <InfoForm
+          ref="infoRef"
+          v-model:activeName="activeName"
+          :is-detail="isDetail"
+          :propFormData="formData"
+        />
+      </el-tab-pane>
+      <el-tab-pane label="价格库存" name="sku">
+        <SkuForm
+          ref="skuRef"
+          v-model:activeName="activeName"
+          :is-detail="isDetail"
+          :propFormData="formData"
+        />
+      </el-tab-pane>
+      <el-tab-pane label="物流设置" name="delivery">
+        <DeliveryForm
+          ref="deliveryRef"
           v-model:activeName="activeName"
           :is-detail="isDetail"
           :propFormData="formData"
@@ -17,9 +33,9 @@
           :propFormData="formData"
         />
       </el-tab-pane>
-      <el-tab-pane label="其他设置" name="otherSettings">
-        <OtherSettingsForm
-          ref="otherSettingsRef"
+      <el-tab-pane label="其它设置" name="other">
+        <OtherForm
+          ref="otherRef"
           v-model:activeName="activeName"
           :is-detail="isDetail"
           :propFormData="formData"
@@ -40,9 +56,11 @@
 import { cloneDeep } from 'lodash-es'
 import { useTagsViewStore } from '@/store/modules/tagsView'
 import * as ProductSpuApi from '@/api/mall/product/spu'
-import BasicInfoForm from './BasicInfoForm.vue'
+import InfoForm from './InfoForm.vue'
 import DescriptionForm from './DescriptionForm.vue'
-import OtherSettingsForm from './OtherSettingsForm.vue'
+import OtherForm from './OtherForm.vue'
+import SkuForm from './SkuForm.vue'
+import DeliveryForm from './DeliveryForm.vue'
 import { convertToInteger, floatToFixed2, formatToFraction } from '@/utils'
 
 defineOptions({ name: 'ProductSpuForm' })
@@ -54,20 +72,22 @@ const { params, name } = useRoute() // 查询参数
 const { delView } = useTagsViewStore() // 视图操作
 
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
-const activeName = ref('basicInfo') // Tag 激活的窗口
+const activeName = ref('info') // Tag 激活的窗口
 const isDetail = ref(false) // 是否查看详情
-const basicInfoRef = ref() // 商品信息Ref
-const descriptionRef = ref() // 商品详情Ref
-const otherSettingsRef = ref() // 其他设置Ref
-// spu 表单数据
+const infoRef = ref() // 商品信息 Ref
+const skuRef = ref() // 商品规格 Ref
+const deliveryRef = ref() // 物流设置 Ref
+const descriptionRef = ref() // 商品详情 Ref
+const otherRef = ref() // 其他设置 Ref
+// SPU 表单数据
 const formData = ref<ProductSpuApi.Spu>({
   name: '', // 商品名称
   categoryId: undefined, // 商品分类
   keyword: '', // 关键字
-  unit: undefined, // 单位
   picUrl: '', // 商品封面图
   sliderPicUrls: [], // 商品轮播图
   introduction: '', // 商品简介
+  deliveryTypes: [], // 配送方式数组
   deliveryTemplateId: undefined, // 运费模版
   brandId: undefined, // 商品品牌
   specType: false, // 商品规格
@@ -89,13 +109,7 @@ const formData = ref<ProductSpuApi.Spu>({
   description: '', // 商品详情
   sort: 0, // 商品排序
   giveIntegral: 0, // 赠送积分
-  virtualSalesCount: 0, // 虚拟销量
-  recommendHot: false, // 是否热卖
-  recommendBenefit: false, // 是否优惠
-  recommendBest: false, // 是否精品
-  recommendNew: false, // 是否新品
-  recommendGood: false, // 是否优品
-  activityOrders: [] // 活动排序
+  virtualSalesCount: 0 // 虚拟销量
 })
 
 /** 获得详情 */
@@ -135,13 +149,14 @@ const getDetail = async () => {
 const submitForm = async () => {
   // 提交请求
   formLoading.value = true
-  // 三个表单逐一校验,如果有一个表单校验不通过则切换到对应表单,如果有两个及以上的情况则切换到最前面的一个并弹出提示消息
-  // 校验各表单
   try {
-    await unref(basicInfoRef)?.validate()
+    // 校验各表单
+    await unref(infoRef)?.validate()
+    await unref(skuRef)?.validate()
+    await unref(deliveryRef)?.validate()
     await unref(descriptionRef)?.validate()
-    await unref(otherSettingsRef)?.validate()
-    // 深拷贝一份, 这样最终 server 端不满足,不需要恢复,
+    await unref(otherRef)?.validate()
+    // 深拷贝一份, 这样最终 server 端不满足,不需要影响原始数据
     const deepCopyFormData = cloneDeep(unref(formData.value)) as ProductSpuApi.Spu
     deepCopyFormData.skus!.forEach((item) => {
       // 给sku name赋值
@@ -181,6 +196,7 @@ const close = () => {
   delView(unref(currentRoute))
   push({ name: 'ProductSpu' })
 }
+
 /** 初始化 */
 onMounted(async () => {
   await getDetail()

+ 0 - 101
src/views/mall/product/spu/form/spu.data.ts

@@ -1,101 +0,0 @@
-import { CrudSchema } from '@/hooks/web/useCrudSchemas'
-
-export const basicInfoSchema = reactive<CrudSchema[]>([
-  {
-    label: '商品名称',
-    field: 'name'
-  },
-  {
-    label: '关键字',
-    field: 'keyword'
-  },
-  {
-    label: '商品简介',
-    field: 'introduction'
-  },
-  {
-    label: '商品分类',
-    field: 'categoryId'
-  },
-  {
-    label: '商品品牌',
-    field: 'brandId'
-  },
-  {
-    label: '商品封面图',
-    field: 'picUrl'
-  },
-  {
-    label: '商品轮播图',
-    field: 'sliderPicUrls'
-  },
-  {
-    label: '商品视频',
-    field: 'videoUrl'
-  },
-  {
-    label: '单位',
-    field: 'unit',
-    dictType: DICT_TYPE.PRODUCT_UNIT
-  },
-  {
-    label: '规格类型',
-    field: 'specType'
-  },
-  {
-    label: '分销类型',
-    field: 'subCommissionType'
-  },
-  {
-    label: '物流模版',
-    field: 'deliveryTemplateId'
-  },
-  {
-    label: '商品属性列表',
-    field: 'skus'
-  }
-])
-export const descriptionSchema = reactive<CrudSchema[]>([
-  {
-    label: '商品详情',
-    field: 'description'
-  }
-])
-export const otherSettingsSchema = reactive<CrudSchema[]>([
-  {
-    label: '商品排序',
-    field: 'sort'
-  },
-  {
-    label: '赠送积分',
-    field: 'giveIntegral'
-  },
-  {
-    label: '虚拟销量',
-    field: 'virtualSalesCount'
-  },
-  {
-    label: '是否热卖推荐',
-    field: 'recommendHot'
-  },
-  {
-    label: '是否优惠推荐',
-    field: 'recommendBenefit'
-  },
-  {
-    label: '是否精品推荐',
-    field: 'recommendBest'
-  },
-  {
-    label: '是否新品推荐',
-    field: 'recommendNew'
-  },
-  {
-    label: '是否优品推荐',
-    field: 'recommendGood'
-  },
-  {
-    label: '活动显示排序',
-    field: 'activityOrders'
-  }
-])

+ 70 - 64
src/views/mall/product/spu/index.vue

@@ -1,3 +1,4 @@
+<!-- 商品中心 - 商品列表  -->
 <template>
   <!-- 搜索工作栏 -->
   <ContentWrap>
@@ -125,27 +126,33 @@
           </el-form>
         </template>
       </el-table-column>
-      <el-table-column align="center" label="商品编号" min-width="60" prop="id" />
-      <el-table-column label="商品图" min-width="80">
+      <el-table-column label="商品编号" min-width="140" prop="id" />
+      <el-table-column label="商品信息" min-width="300">
         <template #default="{ row }">
-          <el-image :src="row.picUrl" class="h-30px w-30px" @click="imagePreview(row.picUrl)" />
+          <div class="flex">
+            <el-image
+              fit="cover"
+              :src="row.picUrl"
+              class="flex-none w-50px h-50px"
+              @click="imagePreview(row.picUrl)"
+            />
+            <div class="ml-4 overflow-hidden">
+              <el-tooltip effect="dark" :content="row.name" placement="top">
+                <div>
+                  {{ row.name }}
+                </div>
+              </el-tooltip>
+            </div>
+          </div>
         </template>
       </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 }"> {{ fenToYuan(row.price) }}元</template>
+      <el-table-column align="center" label="价格" min-width="160" prop="price">
+        <template #default="{ row }"> ¥ {{ fenToYuan(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" />
       <el-table-column align="center" label="排序" min-width="70" prop="sort" />
-      <el-table-column
-        :formatter="dateFormatter"
-        align="center"
-        label="创建时间"
-        prop="createTime"
-        width="180"
-      />
-      <el-table-column align="center" label="状态" min-width="80">
+      <el-table-column align="center" label="销售状态" min-width="80">
         <template #default="{ row }">
           <template v-if="row.status >= 0">
             <el-switch
@@ -163,16 +170,16 @@
           </template>
         </template>
       </el-table-column>
+      <el-table-column
+        :formatter="dateFormatter"
+        align="center"
+        label="创建时间"
+        prop="createTime"
+        width="180"
+      />
       <el-table-column align="center" fixed="right" label="操作" min-width="200">
         <template #default="{ row }">
-          <el-button
-            v-hasPermi="['product:spu:update']"
-            link
-            type="primary"
-            @click="openDetail(row.id)"
-          >
-            详情
-          </el-button>
+          <el-button link type="primary" @click="openDetail(row.id)"> 详情 </el-button>
           <el-button
             v-hasPermi="['product:spu:update']"
             link
@@ -196,17 +203,17 @@
               type="primary"
               @click="handleStatus02Change(row, ProductSpuStatusEnum.DISABLE.status)"
             >
-              恢复到仓库
+              恢复
             </el-button>
           </template>
           <template v-else>
             <el-button
               v-hasPermi="['product:spu:update']"
               link
-              type="primary"
+              type="danger"
               @click="handleStatus02Change(row, ProductSpuStatusEnum.RECYCLE.status)"
             >
-              加入回收
+              回收
             </el-button>
           </template>
         </template>
@@ -236,48 +243,41 @@ defineOptions({ name: 'ProductSpu' })
 
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
-const { currentRoute, push } = useRouter() // 路由跳转
+const { push } = useRouter() // 路由跳转
 
 const loading = ref(false) // 列表的加载中
 const exportLoading = ref(false) // 导出的加载中
 const total = ref(0) // 列表的总页数
-const list = ref<any[]>([]) // 列表的数据
+const list = ref<ProductSpuApi.Spu[]>([]) // 列表的数据
 // tabs 数据
 const tabsData = ref([
   {
-    count: 0,
-    name: '出售中商品',
-    type: 0
+    name: '出售中',
+    type: 0,
+    count: 0
   },
   {
-    count: 0,
-    name: '仓库中商品',
-    type: 1
+    name: '仓库中',
+    type: 1,
+    count: 0
   },
   {
-    count: 0,
-    name: '已售罄商品',
-    type: 2
+    name: '已售罄',
+    type: 2,
+    count: 0
   },
   {
-    count: 0,
     name: '警戒库存',
-    type: 3
+    type: 3,
+    count: 0
   },
   {
-    count: 0,
-    name: '商品回收站',
-    type: 4
+    name: '回收站',
+    type: 4,
+    count: 0
   }
 ])
 
-/** 获得每个 Tab 的数量 */
-const getTabsCount = async () => {
-  const res = await ProductSpuApi.getTabsCount()
-  for (let objName in res) {
-    tabsData.value[Number(objName)].count = res[objName]
-  }
-}
 const queryParams = ref({
   pageNo: 1,
   pageSize: 10,
@@ -288,11 +288,6 @@ const queryParams = ref({
 }) // 查询参数
 const queryFormRef = ref() // 搜索的表单Ref
 
-const handleTabClick = (tab: TabsPaneContext) => {
-  queryParams.value.tabType = tab.paneName as number
-  getList()
-}
-
 /** 查询列表 */
 const getList = async () => {
   loading.value = true
@@ -305,8 +300,22 @@ const getList = async () => {
   }
 }
 
+/** 切换 Tab */
+const handleTabClick = (tab: TabsPaneContext) => {
+  queryParams.value.tabType = tab.paneName as number
+  getList()
+}
+
+/** 获得每个 Tab 的数量 */
+const getTabsCount = async () => {
+  const res = await ProductSpuApi.getTabsCount()
+  for (let objName in res) {
+    tabsData.value[Number(objName)].count = res[objName]
+  }
+}
+
 /** 添加到仓库 / 回收站的状态 */
-const handleStatus02Change = async (row, newStatus: number) => {
+const handleStatus02Change = async (row: any, newStatus: number) => {
   try {
     // 二次确认
     const text = newStatus === ProductSpuStatusEnum.RECYCLE.status ? '加入到回收站' : '恢复到仓库'
@@ -322,7 +331,7 @@ const handleStatus02Change = async (row, newStatus: number) => {
 }
 
 /** 更新上架/下架状态 */
-const handleStatusChange = async (row) => {
+const handleStatusChange = async (row: any) => {
   try {
     // 二次确认
     const text = row.status ? '上架' : '下架'
@@ -407,19 +416,16 @@ const handleExport = async () => {
   }
 }
 
-const categoryList = ref() // 分类树
 /** 获取分类的节点的完整结构 */
-const formatCategoryName = (categoryId) => {
+const categoryList = ref() // 分类树
+const formatCategoryName = (categoryId: number) => {
   return treeToString(categoryList.value, categoryId)
 }
 
-// 监听路由变化更新列表,解决商品保存后,列表不刷新的问题。
-watch(
-  () => currentRoute.value,
-  () => {
-    getList()
-  }
-)
+/** 激活时 */
+onActivated(() => {
+  getList()
+})
 
 /** 初始化 **/
 onMounted(async () => {

+ 8 - 8
src/views/mall/promotion/diy/page/DiyPageForm.vue

@@ -13,8 +13,8 @@
       <el-form-item label="备注" prop="remark">
         <el-input v-model="formData.remark" placeholder="请输入备注" />
       </el-form-item>
-      <el-form-item label="预览图" prop="previewImageUrls">
-        <UploadImgs v-model="formData.previewImageUrls" />
+      <el-form-item label="预览图" prop="previewPicUrls">
+        <UploadImgs v-model="formData.previewPicUrls" />
       </el-form-item>
     </el-form>
     <template #footer>
@@ -40,7 +40,7 @@ const formData = ref({
   id: undefined,
   name: undefined,
   remark: undefined,
-  previewImageUrls: []
+  previewPicUrls: []
 })
 const formRules = reactive({
   name: [{ required: true, message: '页面名称不能为空', trigger: 'blur' }]
@@ -58,8 +58,8 @@ const open = async (type: string, id?: number) => {
     formLoading.value = true
     try {
       const diyPage = await DiyPageApi.getDiyPage(id) // 处理预览图
-      if (diyPage?.previewImageUrls?.length > 0) {
-        diyPage.previewImageUrls = diyPage.previewImageUrls.map((url: string) => {
+      if (diyPage?.previewPicUrls?.length > 0) {
+        diyPage.previewPicUrls = diyPage.previewPicUrls.map((url: string) => {
           return { url }
         })
       }
@@ -82,10 +82,10 @@ const submitForm = async () => {
   formLoading.value = true
   try {
     // 处理预览图
-    const previewImageUrls = formData.value.previewImageUrls.map((item) => {
+    const previewPicUrls = formData.value.previewPicUrls.map((item) => {
       return item['url'] ? item['url'] : item
     })
-    const data = { ...formData.value, previewImageUrls } as unknown as DiyPageApi.DiyPageVO
+    const data = { ...formData.value, previewPicUrls } as unknown as DiyPageApi.DiyPageVO
     if (formType.value === 'create') {
       await DiyPageApi.createDiyPage(data)
       message.success(t('common.createSuccess'))
@@ -107,7 +107,7 @@ const resetForm = () => {
     id: undefined,
     name: undefined,
     remark: undefined,
-    previewImageUrls: []
+    previewPicUrls: []
   }
   formRef.value?.resetFields()
 }

+ 1 - 1
src/views/mall/promotion/diy/page/decorate.vue

@@ -52,7 +52,7 @@ const resetForm = () => {
     templateId: undefined,
     name: '',
     remark: '',
-    previewImageUrls: [],
+    previewPicUrls: [],
     property: ''
   } as DiyPageApi.DiyPageVO
   formRef.value?.resetFields()

+ 3 - 3
src/views/mall/promotion/diy/page/index.vue

@@ -47,14 +47,14 @@
   <ContentWrap>
     <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
       <el-table-column label="编号" align="center" prop="id" />
-      <el-table-column label="预览图" align="center" prop="previewImageUrls">
+      <el-table-column label="预览图" align="center" prop="previewPicUrls">
         <template #default="scope">
           <el-image
             class="h-40px max-w-40px"
-            v-for="(url, index) in scope.row.previewImageUrls"
+            v-for="(url, index) in scope.row.previewPicUrls"
             :key="index"
             :src="url"
-            :preview-src-list="scope.row.previewImageUrls"
+            :preview-src-list="scope.row.previewPicUrls"
             :initial-index="index"
             preview-teleported
           />

+ 8 - 8
src/views/mall/promotion/diy/template/DiyTemplateForm.vue

@@ -13,8 +13,8 @@
       <el-form-item label="备注" prop="remark">
         <el-input v-model="formData.remark" placeholder="请输入备注" type="textarea" />
       </el-form-item>
-      <el-form-item label="预览图" prop="previewImageUrls">
-        <UploadImgs v-model="formData.previewImageUrls" />
+      <el-form-item label="预览图" prop="previewPicUrls">
+        <UploadImgs v-model="formData.previewPicUrls" />
       </el-form-item>
     </el-form>
     <template #footer>
@@ -40,7 +40,7 @@ const formData = ref({
   id: undefined,
   name: undefined,
   remark: undefined,
-  previewImageUrls: []
+  previewPicUrls: []
 })
 const formRules = reactive({
   name: [{ required: true, message: '模板名称不能为空', trigger: 'blur' }]
@@ -59,8 +59,8 @@ const open = async (type: string, id?: number) => {
     try {
       const diyTemplate = await DiyTemplateApi.getDiyTemplate(id)
       // 处理预览图
-      if (diyTemplate?.previewImageUrls?.length > 0) {
-        diyTemplate.previewImageUrls = diyTemplate.previewImageUrls.map((url: string) => {
+      if (diyTemplate?.previewPicUrls?.length > 0) {
+        diyTemplate.previewPicUrls = diyTemplate.previewPicUrls.map((url: string) => {
           return { url }
         })
       }
@@ -83,10 +83,10 @@ const submitForm = async () => {
   formLoading.value = true
   try {
     // 处理预览图
-    const previewImageUrls = formData.value.previewImageUrls.map((item) => {
+    const previewPicUrls = formData.value.previewPicUrls.map((item) => {
       return item['url'] ? item['url'] : item
     })
-    const data = { ...formData.value, previewImageUrls } as unknown as DiyTemplateApi.DiyTemplateVO
+    const data = { ...formData.value, previewPicUrls } as unknown as DiyTemplateApi.DiyTemplateVO
     if (formType.value === 'create') {
       await DiyTemplateApi.createDiyTemplate(data)
       message.success(t('common.createSuccess'))
@@ -108,7 +108,7 @@ const resetForm = () => {
     id: undefined,
     name: undefined,
     remark: undefined,
-    previewImageUrls: []
+    previewPicUrls: []
   }
   formRef.value?.resetFields()
 }

+ 1 - 1
src/views/mall/promotion/diy/template/decorate.vue

@@ -118,7 +118,7 @@ const resetForm = () => {
     used: false,
     usedTime: undefined,
     remark: '',
-    previewImageUrls: [],
+    previewPicUrls: [],
     property: '',
     pages: []
   } as DiyTemplateApi.DiyTemplatePropertyVO

+ 3 - 3
src/views/mall/promotion/diy/template/index.vue

@@ -47,14 +47,14 @@
   <ContentWrap>
     <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
       <el-table-column label="编号" align="center" prop="id" />
-      <el-table-column label="预览图" align="center" prop="previewImageUrls">
+      <el-table-column label="预览图" align="center" prop="previewPicUrls">
         <template #default="scope">
           <el-image
             class="h-40px max-w-40px"
-            v-for="(url, index) in scope.row.previewImageUrls"
+            v-for="(url, index) in scope.row.previewPicUrls"
             :key="index"
             :src="url"
-            :preview-src-list="scope.row.previewImageUrls"
+            :preview-src-list="scope.row.previewPicUrls"
             :initial-index="index"
             preview-teleported
           />

+ 148 - 113
src/views/mall/promotion/rewardActivity/RewardForm.vue

@@ -24,51 +24,96 @@
           <el-radio
             v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_CONDITION_TYPE)"
             :key="dict.value"
-            :label="parseInt(dict.value)"
-            >{{ dict.label }}</el-radio
+            :label="dict.value"
           >
+            {{ dict.label }}
+          </el-radio>
         </el-radio-group>
       </el-form-item>
       <el-form-item label="优惠设置">
-        <template v-for="(item,index) in formData.rules" :key="index">
-        <el-row type="flex">
-          <el-col :span="24" style="font-weight: bold;display: flex;">活动层级{{ index+1 }}<el-button link type="danger" style="margin-left: auto;" v-if="index!=0" @click="deleteStratum(index)">删除</el-button></el-col>
-            <e-form :ref="'formRef'+index" :model="item"  >
-               <el-form-item  label="优惠门槛:" prop="limit" label-width="100px" style="padding-left: 50px;">满<el-input style="width: 150px;padding:0 10px;" v-model="item.limit" type='number' placeholder="" /> 元
-               </el-form-item>
-               <el-form-item label="优惠内容:"  label-width="100px"  style="padding-left: 50px;">
-                <el-checkbox-group  v-model="rules[index]" style="width:100%">
-                    <el-col :span="24">
-                      <el-checkbox  label="订单金额优惠" name="type" />
-                      <el-form-item v-if="rules[index].includes('订单金额优惠')">
-                        减<el-input style="width: 150px;padding:0 20px;" v-model="item.discountPrice" type='number' placeholder="" />元
-                     </el-form-item>
-                    </el-col>
-                    <el-col :span="24"><el-checkbox v-model="item.freeDelivery" label="包邮" name="type" /></el-col>
-                    <el-col :span="24">
-                      <el-checkbox label="送积分" name="type" />
-                      <el-form-item v-if="rules[index].includes('送积分')">
-                        送<el-input style="width: 150px;padding:0 20px;" v-model="item.point" type='number' placeholder="" />积分
-                     </el-form-item>
-                    </el-col>
-                    <!-- 优惠券待处理  也可以参考优惠劵的SpuShowcase-->
-                    <!-- TODO 待实现!-->
-                    <el-col :span="24"><el-checkbox label="送优惠券" name="type" /></el-col>
+        <template v-for="(item, index) in formData.rules" :key="index">
+          <el-row type="flex">
+            <el-col :span="24" style="font-weight: bold; display: flex">
+              活动层级{{ index + 1 }}
+              <el-button
+                link
+                type="danger"
+                style="margin-left: auto"
+                v-if="index != 0"
+                @click="deleteActivityRule(index)"
+              >
+                删除
+              </el-button>
+            </el-col>
+            <e-form :ref="'formRef' + index" :model="item">
+              <el-form-item
+                label="优惠门槛:"
+                prop="limit"
+                label-width="100px"
+                style="padding-left: 50px"
+              >
+                满
+                <el-input
+                  style="width: 150px; padding: 0 10px"
+                  v-model="item.limit"
+                  type="number"
+                  placeholder=""
+                />
+                元
+              </el-form-item>
+              <el-form-item label="优惠内容:" label-width="100px" style="padding-left: 50px">
+                <el-checkbox-group v-model="activityRules[index]" style="width: 100%">
+                  <el-col :span="24">
+                    <el-checkbox label="订单金额优惠" name="type" />
+                    <el-form-item v-if="activityRules[index].includes('订单金额优惠')">
+                      减
+                      <el-input
+                        style="width: 150px; padding: 0 20px"
+                        v-model="item.discountPrice"
+                        type="number"
+                        placeholder=""
+                      />
+                      元
+                    </el-form-item>
+                  </el-col>
+                  <el-col :span="24">
+                    <el-checkbox v-model="item.freeDelivery" label="包邮" name="type" />
+                  </el-col>
+                  <el-col :span="24">
+                    <el-checkbox label="送积分" name="type" />
+                    <el-form-item v-if="activityRules[index].includes('送积分')">
+                      送
+                      <el-input
+                        style="width: 150px; padding: 0 20px"
+                        v-model="item.point"
+                        type="number"
+                        placeholder=""
+                      />
+                      积分
+                    </el-form-item>
+                  </el-col>
+                  <!-- 优惠券待处理  也可以参考优惠劵的SpuShowcase-->
+                  <!-- TODO 待实现!-->
+                  <el-col :span="24">
+                    <el-checkbox label="送优惠券" name="type" />
+                  </el-col>
                 </el-checkbox-group>
               </el-form-item>
             </e-form>
-        </el-row>
-      </template>
-        <el-button  type="primary" @click="addStratum">添加活动层级</el-button>
+          </el-row>
+        </template>
+        <!-- TODO 实现:建议改成放在每一个【活动层级】的下面,有点类似主子表 -->
+        <el-button type="primary" @click="addActivityStratum">添加活动层级</el-button>
       </el-form-item>
       <el-form-item label="活动商品" prop="productScope">
         <el-radio-group v-model="formData.productScope">
           <el-radio
             v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_PRODUCT_SCOPE)"
             :key="dict.value"
-            :label="parseInt(dict.value)"
-            >{{ dict.label }}</el-radio
+            :label="dict.value"
           >
+            {{ dict.label }}
+          </el-radio>
         </el-radio-group>
       </el-form-item>
       <!-- TODO:活动商品的开发,可以参考优惠劵的,已经搞好啦; -->
@@ -87,9 +132,9 @@
         >
           <el-option v-for="item in productSpus" :key="item.id" :label="item.name" :value="item.id">
             <span style="float: left">{{ item.name }}</span>
-            <span style="float: right; font-size: 13px; color: #8492a6"
-              >¥{{ (item.price / 100.0).toFixed(2) }}</span
-            >
+            <span style="float: right; font-size: 13px; color: #8492a6">
+              ¥{{ (item.price / 100.0).toFixed(2) }}
+            </span>
           </el-option>
         </el-select>
       </el-form-item>
@@ -106,15 +151,8 @@
 <script lang="ts" setup>
 import { getSpuSimpleList } from '@/api/mall/product/spu'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import { CommonStatusEnum } from '@/utils/constants'
-import * as RewardActivityApi from '@/api/mall/promotion/reward/rewardActivity'  
-import {
-  PromotionConditionTypeEnum,
-  PromotionProductScopeEnum,
-  PromotionActivityStatusEnum
-} from '@/utils/constants'
-// 商品数据
-const productSpus = ref<any[]>([])
+import * as RewardActivityApi from '@/api/mall/promotion/reward/rewardActivity'
+import { PromotionConditionTypeEnum, PromotionProductScopeEnum } from '@/utils/constants'
 
 /** 初始化 **/
 onMounted(() => {
@@ -127,6 +165,7 @@ defineOptions({ name: 'ProductBrandForm' })
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 
+const productSpus = ref<any[]>([]) // 商品数据
 const dialogVisible = ref(false) // 弹窗的是否展示
 const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
@@ -141,19 +180,18 @@ const formData = ref({
   remark: undefined,
   productScope: PromotionProductScopeEnum.ALL.scope,
   productSpuIds: undefined,
-  rules: [{
-    limit: undefined,
-    discountPrice:  undefined,
-    freeDelivery: undefined,
-    point:  undefined,
-    couponIds: [],
-    couponCounts: []
-  }],
+  rules: [
+    {
+      limit: undefined,
+      discountPrice: undefined,
+      freeDelivery: undefined,
+      point: undefined,
+      couponIds: [],
+      couponCounts: []
+    }
+  ]
 })
-// 优惠设置
-let rules=reactive([]);
-// 优惠券列表
-
+const activityRules = reactive([]) // 优惠设置。每个元素都是一个 [],放“包邮”、“送积分”、“订单金额优惠”
 const formRules = reactive({
   name: [{ required: true, message: '活动名称不能为空', trigger: 'blur' }],
   startAndEndTime: [{ required: true, message: '活动时间不能为空', trigger: 'blur' }],
@@ -173,23 +211,24 @@ const open = async (type: string, id?: number) => {
   if (id) {
     formLoading.value = true
     try {
-    let data= await RewardActivityApi.getReward(id);
-    data.startAndEndTime=[new Date(data.startTime), new Date(data.endTime)];
-    rules.splice(0,rules.length);
-    data.rules.forEach((item)=>{
-      let ars:string[]=reactive([]);
-      if(item.freeDelivery){
-        ars.push('包邮')
-      }
-      if(item.point){
-        ars.push('送积分')
-      }
-      if(item.discountPrice){
-        ars.push('订单金额优惠')
-      }
-      rules.push(ars)
-    })
-    formData.value=data
+      let data = await RewardActivityApi.getReward(id)
+      data.startAndEndTime = [new Date(data.startTime), new Date(data.endTime)]
+      activityRules.splice(0, activityRules.length)
+      data.rules.forEach((item) => {
+        // TODO 是不是不用 reactive,直接 [] 就可以了?
+        let array: string[] = reactive([])
+        if (item.freeDelivery) {
+          array.push('包邮')
+        }
+        if (item.point) {
+          array.push('送积分')
+        }
+        if (item.discountPrice) {
+          array.push('订单金额优惠')
+        }
+        activityRules.push(array)
+      })
+      formData.value = data
     } finally {
       formLoading.value = false
     }
@@ -205,21 +244,16 @@ const submitForm = async () => {
   const valid = await formRef.value.validate()
   if (!valid) return
   // 处理下数据兼容接口
-  formData.value.startTime= +new Date(formData.value.startAndEndTime[0])
-  formData.value.endTime=+new Date(formData.value.startAndEndTime[1])
-  console.log(rules)
-  rules.forEach((item,index)=>{
-      if(item.includes('包邮')){
-        formData.value.rules[index].freeDelivery=true;
-      }else{
-        formData.value.rules[index].freeDelivery=false;
-      }
-      if(!item.includes('送积分')){
-        formData.value.rules[index].point=undefined;
-      }
-      if(!item.includes('订单金额优惠')){
-        formData.value.rules[index].discountPrice=undefined;
-      }
+  formData.value.startTime = +new Date(formData.value.startAndEndTime[0])
+  formData.value.endTime = +new Date(formData.value.startAndEndTime[1])
+  activityRules.forEach((item, index) => {
+    formData.value.rules[index].freeDelivery = !!item.includes('包邮')
+    if (!item.includes('送积分')) {
+      formData.value.rules[index].point = undefined
+    }
+    if (!item.includes('订单金额优惠')) {
+      formData.value.rules[index].discountPrice = undefined
+    }
   })
 
   // 提交请求
@@ -241,22 +275,21 @@ const submitForm = async () => {
   }
 }
 
-const addStratum =()=>{
+const addActivityStratum = () => {
   formData.value.rules.push({
-      limit: undefined,
-      discountPrice:  undefined,
-      freeDelivery: undefined,
-      point:  undefined,
-      couponIds: [],
-      couponCounts: []
-    })
-    rules.push([]);
-    console.log(rules)
+    limit: undefined,
+    discountPrice: undefined,
+    freeDelivery: undefined,
+    point: undefined,
+    couponIds: [],
+    couponCounts: []
+  })
+  activityRules.push([])
 }
 
-const deleteStratum=(index)=>{
-  formData.value.rules.splice(index,1)
-  rules.splice(index,1)
+const deleteActivityRule = (index) => {
+  formData.value.rules.splice(index, 1)
+  activityRules.splice(index, 1)
 }
 
 /** 重置表单 */
@@ -271,20 +304,22 @@ const resetForm = () => {
     remark: undefined,
     productScope: PromotionProductScopeEnum.ALL.scope,
     productSpuIds: undefined,
-    rules: [{
-      limit: undefined,
-      discountPrice:  undefined,
-      freeDelivery: undefined,
-      point:  undefined,
-      couponIds: [],
-      couponCounts: []
-    }],
+    rules: [
+      {
+        limit: undefined,
+        discountPrice: undefined,
+        freeDelivery: undefined,
+        point: undefined,
+        couponIds: [],
+        couponCounts: []
+      }
+    ]
   }
-  rules.splice(0,rules.length);
-  rules.push(reactive([]));
+  activityRules.splice(0, activityRules.length)
+  activityRules.push(reactive([]))
   // 解决下有时刷新页面第一次点编辑报错
- nextTick(()=>{
-  formRef.value?.resetFields()
- })
+  nextTick(() => {
+    formRef.value?.resetFields()
+  })
 }
 </script>

+ 1 - 9
src/views/mall/promotion/rewardActivity/index.vue

@@ -122,8 +122,7 @@
 <script lang="ts" setup>
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
-import * as ProductBrandApi from '@/api/mall/product/brand'
-import * as RewardActivityApi from '@/api/mall/promotion/reward/rewardActivity'  
+import * as RewardActivityApi from '@/api/mall/promotion/reward/rewardActivity'
 import RewardForm from './RewardForm.vue'
 
 defineOptions({ name: 'PromotionRewardActivity' })
@@ -157,16 +156,11 @@ const getList = async () => {
 
 /** 搜索按钮操作 */
 const handleQuery = () => {
-  // console.log(queryParams)
-  // message.success('已打印搜索参数')
-  // return
   getList()
 }
 
 /** 重置按钮操作 */
 const resetQuery = () => {
-  // message.success('重置查询表单获取数据')
-  // return
   queryFormRef.value.resetFields()
   handleQuery()
 }
@@ -182,8 +176,6 @@ const handleDelete = async (id: number) => {
   try {
     // 删除的二次确认
     await message.delConfirm()
-    // message.success('您以确认删除')
-    // return
     // 发起删除
     await RewardActivityApi.deleteRewardActivity(id)
     message.success(t('common.delSuccess'))

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

@@ -119,7 +119,6 @@ const resetForm = () => {
     id: undefined,
     name: '',
     picUrl: '',
-    bigPicUrl: '',
     status: CommonStatusEnum.ENABLE
   }
   formRef.value?.resetFields()