Bladeren bron

!153 提交商品管理第二版
Merge pull request !153 from puhui999/dev-to-dev

芋道源码 1 jaar geleden
bovenliggende
commit
855e4d3d02

+ 5 - 0
src/api/mall/product/brand.ts

@@ -54,3 +54,8 @@ export const getBrand = (id: number) => {
 export const getBrandParam = (params: PageParam) => {
   return request.get({ url: '/product/brand/page', params })
 }
+
+// 获得商品品牌精简信息列表
+export const getSimpleBrandList = () => {
+  return request.get({ url: '/product/brand/list-all-simple' })
+}

+ 0 - 39
src/api/mall/product/management/spu.ts

@@ -1,39 +0,0 @@
-import request from '@/config/axios'
-import type { SpuType } from './type/spuType' // TODO  @puhui999: type 和 api 一起放,简单一点哈~
-
-// TODO @puhui999:中英文之间有空格
-
-// 获得spu列表 TODO @puhui999:这个是 getSpuPage 哈
-export const getSpuList = (params: PageParam) => {
-  return request.get({ url: '/product/spu/page', params })
-}
-
-// 获得spu列表tabsCount
-export const getTabsCount = () => {
-  return request.get({ url: '/product/spu/tabsCount' })
-}
-
-// 创建商品spu
-export const createSpu = (data: SpuType) => {
-  return request.post({ url: '/product/spu/create', data })
-}
-
-// 更新商品spu
-export const updateSpu = (data: SpuType) => {
-  return request.put({ url: '/product/spu/update', data })
-}
-
-// 更新商品spu status
-export const updateStatus = (data: { id: number; status: number }) => {
-  return request.put({ url: '/product/spu/updateStatus', data })
-}
-
-// 获得商品 spu
-export const getSpu = (id: number) => {
-  return request.get({ url: `/product/spu/get-detail?id=${id}` })
-}
-
-// 删除商品Spu
-export const deleteSpu = (id: number) => {
-  return request.delete({ url: `/product/spu/delete?id=${id}` })
-}

+ 0 - 79
src/api/mall/product/management/type/skuType.ts

@@ -1,79 +0,0 @@
-export interface Property {
-  /**
-   * 属性编号
-   *
-   * 关联 {@link ProductPropertyDO#getId()}
-   */
-  propertyId?: number
-  /**
-   * 属性值编号
-   *
-   * 关联 {@link ProductPropertyValueDO#getId()}
-   */
-  valueId?: number
-  /**
-   * 属性值名称
-   */
-  valueName?: string
-}
-
-export interface SkuType {
-  /**
-   * 商品 SKU 编号,自增
-   */
-  id?: number
-  /**
-   * SPU 编号
-   */
-  spuId?: number
-  /**
-   * 属性数组,JSON 格式
-   */
-  properties?: Property[]
-  /**
-   * 商品价格,单位:分
-   */
-  price?: number
-  /**
-   * 市场价,单位:分
-   */
-  marketPrice?: number
-  /**
-   * 成本价,单位:分
-   */
-  costPrice?: number
-  /**
-   * 商品条码
-   */
-  barCode?: string
-  /**
-   * 图片地址
-   */
-  picUrl?: string
-  /**
-   * 库存
-   */
-  stock?: number
-  /**
-   * 商品重量,单位:kg 千克
-   */
-  weight?: number
-  /**
-   * 商品体积,单位:m^3 平米
-   */
-  volume?: number
-
-  /**
-   * 一级分销的佣金,单位:分
-   */
-  subCommissionFirstPrice?: number
-  /**
-   * 二级分销的佣金,单位:分
-   */
-  subCommissionSecondPrice?: number
-
-  /**
-   * 商品销量
-   */
-  salesCount?: number
-}

+ 0 - 25
src/api/mall/product/management/type/spuType.ts

@@ -1,25 +0,0 @@
-import { SkuType } from './skuType'
-
-export interface SpuType {
-  id?: number
-  name?: string // 商品名称
-  categoryId?: number | null // 商品分类
-  keyword?: string // 关键字
-  unit?: number | null // 单位
-  picUrl?: string // 商品封面图
-  sliderPicUrls?: string[] // 商品轮播图
-  introduction?: string // 商品简介
-  deliveryTemplateId?: number // 运费模版
-  specType?: boolean // 商品规格
-  subCommissionType?: boolean // 分销类型
-  skus: SkuType[] // sku数组
-  description?: string // 商品详情
-  sort?: string // 商品排序
-  giveIntegral?: number // 赠送积分
-  virtualSalesCount?: number // 虚拟销量
-  recommendHot?: boolean // 是否热卖
-  recommendBenefit?: boolean // 是否优惠
-  recommendBest?: boolean // 是否精品
-  recommendNew?: boolean // 是否新品
-  recommendGood?: boolean // 是否优品
-}

+ 90 - 0
src/api/mall/product/spu.ts

@@ -0,0 +1,90 @@
+import request from '@/config/axios'
+
+export interface Property {
+  propertyId?: number // 属性编号
+  propertyName?: string // 属性名称
+  valueId?: number // 属性值编号
+  valueName?: string // 属性值名称
+}
+
+export interface SkuType {
+  id?: number // 商品 SKU 编号
+  spuId?: number // SPU 编号
+  properties?: Property[] // 属性数组
+  price?: number // 商品价格
+  marketPrice?: number // 市场价
+  costPrice?: number // 成本价
+  barCode?: string // 商品条码
+  picUrl?: string // 图片地址
+  stock?: number // 库存
+  weight?: number // 商品重量,单位:kg 千克
+  volume?: number // 商品体积,单位:m^3 平米
+  subCommissionFirstPrice?: number // 一级分销的佣金
+  subCommissionSecondPrice?: number // 二级分销的佣金
+  salesCount?: number // 商品销量
+}
+
+export interface SpuType {
+  id?: number
+  name?: string // 商品名称
+  categoryId?: number | null // 商品分类
+  keyword?: string // 关键字
+  unit?: number | null // 单位
+  picUrl?: string // 商品封面图
+  sliderPicUrls?: string[] // 商品轮播图
+  introduction?: string // 商品简介
+  deliveryTemplateId?: number | null // 运费模版
+  brandId?: number | null // 商品品牌编号
+  specType?: boolean // 商品规格
+  subCommissionType?: boolean // 分销类型
+  skus: SkuType[] // sku数组
+  description?: string // 商品详情
+  sort?: string // 商品排序
+  giveIntegral?: number // 赠送积分
+  virtualSalesCount?: number // 虚拟销量
+  recommendHot?: boolean // 是否热卖
+  recommendBenefit?: boolean // 是否优惠
+  recommendBest?: boolean // 是否精品
+  recommendNew?: boolean // 是否新品
+  recommendGood?: boolean // 是否优品
+}
+
+// 获得 Spu 列表
+export const getSpuPage = (params: PageParam) => {
+  return request.get({ url: '/product/spu/page', params })
+}
+
+// 获得 Spu 列表 tabsCount
+export const getTabsCount = () => {
+  return request.get({ url: '/product/spu/get-count' })
+}
+
+// 创建商品 Spu
+export const createSpu = (data: SpuType) => {
+  return request.post({ url: '/product/spu/create', data })
+}
+
+// 更新商品 Spu
+export const updateSpu = (data: SpuType) => {
+  return request.put({ url: '/product/spu/update', data })
+}
+
+// 更新商品 Spu status
+export const updateStatus = (data: { id: number; status: number }) => {
+  return request.put({ url: '/product/spu/update-status', data })
+}
+
+// 获得商品 Spu
+export const getSpu = (id: number) => {
+  return request.get({ url: `/product/spu/get-detail?id=${id}` })
+}
+
+// 删除商品 Spu
+export const deleteSpu = (id: number) => {
+  return request.delete({ url: `/product/spu/delete?id=${id}` })
+}
+
+// 导出商品 Spu
+export const exportUser = (params) => {
+  return request.download({ url: '/product/spu/export', params })
+}

+ 44 - 14
src/components/UploadFile/src/UploadImgs.vue

@@ -1,19 +1,19 @@
 <template>
   <div class="upload-box">
     <el-upload
+      v-model:file-list="fileList"
+      :accept="fileType.join(',')"
       :action="updateUrl"
-      list-type="picture-card"
+      :before-upload="beforeUpload"
       :class="['upload', drag ? 'no-border' : '']"
-      v-model:file-list="fileList"
-      :multiple="true"
-      :limit="limit"
+      :drag="drag"
       :headers="uploadHeaders"
-      :before-upload="beforeUpload"
+      :limit="limit"
+      :multiple="true"
+      :on-error="uploadError"
       :on-exceed="handleExceed"
       :on-success="uploadSuccess"
-      :on-error="uploadError"
-      :drag="drag"
-      :accept="fileType.join(',')"
+      list-type="picture-card"
     >
       <div class="upload-empty">
         <slot name="empty">
@@ -40,15 +40,15 @@
     </div>
     <el-image-viewer
       v-if="imgViewVisible"
-      @close="imgViewVisible = false"
       :url-list="[viewImageUrl]"
+      @close="imgViewVisible = false"
     />
   </div>
 </template>
-<script setup lang="ts" name="UploadImgs">
+<script lang="ts" name="UploadImgs" setup>
 import { PropType } from 'vue'
+import type { UploadFile, UploadProps, UploadUserFile } from 'element-plus'
 import { ElNotification } from 'element-plus'
-import type { UploadProps, UploadFile, UploadUserFile } from 'element-plus'
 
 import { propTypes } from '@/utils/propTypes'
 import { getAccessToken, getTenantId } from '@/utils/auth'
@@ -88,8 +88,19 @@ const uploadHeaders = ref({
   'tenant-id': getTenantId()
 })
 
-const fileList = ref<UploadUserFile[]>(props.modelValue)
-
+const fileList = ref<UploadUserFile[]>()
+// fix: 改为动态监听赋值解决图片回显问题
+watch(
+  () => props.modelValue,
+  (data) => {
+    if (!data) return
+    fileList.value = data
+  },
+  {
+    deep: true,
+    immediate: true
+  }
+)
 /**
  * @description 文件上传之前判断
  * @param rawFile 上传的文件
@@ -116,9 +127,11 @@ const beforeUpload: UploadProps['beforeUpload'] = (rawFile) => {
 interface UploadEmits {
   (e: 'update:modelValue', value: UploadUserFile[]): void
 }
+
 const emit = defineEmits<UploadEmits>()
 const uploadSuccess = (response, uploadFile: UploadFile) => {
   if (!response) return
+  // TODO 多图上传组件成功后只是把保存成功后的url替换掉组件选图时的文件路径,所以返回的fileList包含的是一个包含文件信息的对象列表
   uploadFile.url = response.data
   emit('update:modelValue', fileList.value)
   message.success('上传成功')
@@ -159,35 +172,40 @@ const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile) => {
 }
 </script>
 
-<style scoped lang="scss">
+<style lang="scss" scoped>
 .is-error {
   .upload {
     :deep(.el-upload--picture-card),
     :deep(.el-upload-dragger) {
       border: 1px dashed var(--el-color-danger) !important;
+
       &:hover {
         border-color: var(--el-color-primary) !important;
       }
     }
   }
 }
+
 :deep(.disabled) {
   .el-upload--picture-card,
   .el-upload-dragger {
     cursor: not-allowed;
     background: var(--el-disabled-bg-color) !important;
     border: 1px dashed var(--el-border-color-darker);
+
     &:hover {
       border-color: var(--el-border-color-darker) !important;
     }
   }
 }
+
 .upload-box {
   .no-border {
     :deep(.el-upload--picture-card) {
       border: none !important;
     }
   }
+
   :deep(.upload) {
     .el-upload-dragger {
       display: flex;
@@ -199,14 +217,17 @@ const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile) => {
       overflow: hidden;
       border: 1px dashed var(--el-border-color-darker);
       border-radius: v-bind(borderRadius);
+
       &:hover {
         border: 1px dashed var(--el-color-primary);
       }
     }
+
     .el-upload-dragger.is-dragover {
       background-color: var(--el-color-primary-light-9);
       border: 2px dashed var(--el-color-primary) !important;
     }
+
     .el-upload-list__item,
     .el-upload--picture-card {
       width: v-bind(width);
@@ -214,11 +235,13 @@ const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile) => {
       background-color: transparent;
       border-radius: v-bind(borderRadius);
     }
+
     .upload-image {
       width: 100%;
       height: 100%;
       object-fit: contain;
     }
+
     .upload-handle {
       position: absolute;
       top: 0;
@@ -233,6 +256,7 @@ const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile) => {
       background: rgb(0 0 0 / 60%);
       opacity: 0;
       transition: var(--el-transition-duration-fast);
+
       .handle-icon {
         display: flex;
         flex-direction: column;
@@ -240,15 +264,18 @@ const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile) => {
         justify-content: center;
         padding: 0 6%;
         color: aliceblue;
+
         .el-icon {
           margin-bottom: 15%;
           font-size: 140%;
         }
+
         span {
           font-size: 100%;
         }
       }
     }
+
     .el-upload-list__item {
       &:hover {
         .upload-handle {
@@ -256,6 +283,7 @@ const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile) => {
         }
       }
     }
+
     .upload-empty {
       display: flex;
       flex-direction: column;
@@ -263,12 +291,14 @@ const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile) => {
       font-size: 12px;
       line-height: 30px;
       color: var(--el-color-info);
+
       .el-icon {
         font-size: 28px;
         color: var(--el-text-color-secondary);
       }
     }
   }
+
   .el-upload__tip {
     line-height: 15px;
     text-align: center;

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

@@ -349,22 +349,35 @@ const remainingRouter: AppRouteRecordRaw[] = [
   {
     path: '/product',
     component: Layout,
-    name: 'ProductManagementEdit',
+    name: 'Product',
     meta: {
       hidden: true
     },
     children: [
       {
-        path: 'productManagementAdd', // TODO @puhui999:最好拆成 add 和 edit 两个路由;添加商品;修改商品
+        path: 'productSpuAdd', // TODO @puhui999:最好拆成 add 和 edit 两个路由;添加商品;修改商品 fix
         component: () => import('@/views/mall/product/spu/addForm.vue'),
-        name: 'ProductManagementAdd',
+        name: 'ProductSpuAdd',
         meta: {
           noCache: true,
           hidden: true,
           canTo: true,
           icon: 'ep:edit',
           title: '添加商品',
-          activeMenu: '/product/product-management'
+          activeMenu: '/product/product-spu'
+        }
+      },
+      {
+        path: 'productSpuEdit/:spuId(\\d+)',
+        component: () => import('@/views/mall/product/spu/addForm.vue'),
+        name: 'productSpuEdit',
+        meta: {
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          icon: 'ep:edit',
+          title: '编辑商品',
+          activeMenu: '/product/product-spu'
         }
       }
     ]

+ 39 - 0
src/utils/index.ts

@@ -155,3 +155,42 @@ export const fileSizeFormatter = (row, column, cellValue) => {
   const sizeStr = size.toFixed(2) //保留的小数位数
   return sizeStr + ' ' + unitArr[index]
 }
+
+/**
+ * 将值复制到目标对象,且以目标对象属性为准,例:target: {a:1} source:{a:2,b:3} 结果为:{a:2}
+ * @param target 目标对象
+ * @param source 源对象
+ */
+export const copyValueToTarget = (target, source) => {
+  const newObj = Object.assign({}, target, source)
+  // 删除多余属性
+  Object.keys(newObj).forEach((key) => {
+    // 如果不是target中的属性则删除
+    if (Object.keys(target).indexOf(key) === -1) {
+      delete newObj[key]
+    }
+  })
+  // 更新目标对象值
+  Object.assign(target, newObj)
+}
+
+/**
+ * 将一个整数转换为分数保留两位小数
+ * @param num
+ */
+export const formatToFraction = (num: number | string | undefined): number => {
+  if (typeof num === 'undefined') return 0
+  const parsedNumber = typeof num === 'string' ? parseFloat(num) : num
+  return parseFloat((parsedNumber / 100).toFixed(2))
+}
+
+/**
+ * 将一个分数转换为整数
+ * @param num
+ */
+export const convertToInteger = (num: number | string | undefined): number => {
+  if (typeof num === 'undefined') return 0
+  const parsedNumber = typeof num === 'string' ? parseFloat(num) : num
+  // TODO 分转元后还有小数则四舍五入
+  return Math.round(parsedNumber * 100)
+}

+ 0 - 18
src/utils/object.ts

@@ -1,18 +0,0 @@
-// TODO @puhui999:这个方法,可以考虑放到 index.js
-/**
- * 将值复制到目标对象,且以目标对象属性为准,例:target: {a:1} source:{a:2,b:3} 结果为:{a:2}
- * @param target 目标对象
- * @param source 源对象
- */
-export const copyValueToTarget = (target, source) => {
-  const newObj = Object.assign({}, target, source)
-  // 删除多余属性
-  Object.keys(newObj).forEach((key) => {
-    // 如果不是target中的属性则删除
-    if (Object.keys(target).indexOf(key) === -1) {
-      delete newObj[key]
-    }
-  })
-  // 更新目标对象值
-  Object.assign(target, newObj)
-}

+ 81 - 142
src/views/mall/product/spu/addForm.vue

@@ -3,21 +3,21 @@
     <el-tabs v-model="activeName">
       <el-tab-pane label="商品信息" name="basicInfo">
         <BasicInfoForm
-          ref="BasicInfoRef"
+          ref="basicInfoRef"
           v-model:activeName="activeName"
           :propFormData="formData"
         />
       </el-tab-pane>
       <el-tab-pane label="商品详情" name="description">
         <DescriptionForm
-          ref="DescriptionRef"
+          ref="descriptionRef"
           v-model:activeName="activeName"
           :propFormData="formData"
         />
       </el-tab-pane>
       <el-tab-pane label="其他设置" name="otherSettings">
         <OtherSettingsForm
-          ref="OtherSettingsRef"
+          ref="otherSettingsRef"
           v-model:activeName="activeName"
           :propFormData="formData"
         />
@@ -31,88 +31,56 @@
     </el-form>
   </ContentWrap>
 </template>
-<script lang="ts" name="ProductManagementForm" setup>
+<script lang="ts" name="ProductSpuForm" setup>
+import { cloneDeep } from 'lodash-es'
 import { useTagsViewStore } from '@/store/modules/tagsView'
 import { BasicInfoForm, DescriptionForm, OtherSettingsForm } from './components'
-import type { SpuType } from '@/api/mall/product/management/type/spuType' // 业务api
-import * as managementApi from '@/api/mall/product/management/spu'
-import * as PropertyApi from '@/api/mall/product/property'
+// 业务api
+import * as ProductSpuApi from '@/api/mall/product/spu'
+import { convertToInteger, formatToFraction } from '@/utils'
+
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
 const { push, currentRoute } = useRouter() // 路由
-const { query } = useRoute() // 查询参数
+const { params } = useRoute() // 查询参数
 const { delView } = useTagsViewStore() // 视图操作
 
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const activeName = ref('basicInfo') // Tag 激活的窗口
-const BasicInfoRef = ref<ComponentRef<typeof BasicInfoForm>>() // 商品信息Ref
-const DescriptionRef = ref<ComponentRef<typeof DescriptionForm>>() // 商品详情Ref
-const OtherSettingsRef = ref<ComponentRef<typeof OtherSettingsForm>>() // 其他设置Ref
-const formData = ref<SpuType>({
-  name: '213', // 商品名称
+const basicInfoRef = ref<ComponentRef<typeof BasicInfoForm>>() // 商品信息Ref
+const descriptionRef = ref<ComponentRef<typeof DescriptionForm>>() // 商品详情Ref
+const otherSettingsRef = ref<ComponentRef<typeof OtherSettingsForm>>() // 其他设置Ref
+// spu 表单数据
+const formData = ref<ProductSpuApi.SpuType>({
+  name: '', // 商品名称
   categoryId: null, // 商品分类
-  keyword: '213', // 关键字
+  keyword: '', // 关键字
   unit: null, // 单位
-  picUrl:
-    'http://127.0.0.1:48080/admin-api/infra/file/4/get/6ffdf8f5dfc03f7ceec1ff1ebc138adb8b721a057d505ccfb0e61a8783af1371.png', // 商品封面图
-  sliderPicUrls: [
-    {
-      name: 'http://127.0.0.1:48080/admin-api/infra/file/4/get/6ffdf8f5dfc03f7ceec1ff1ebc138adb8b721a057d505ccfb0e61a8783af1371.png',
-      url: 'http://127.0.0.1:48080/admin-api/infra/file/4/get/6ffdf8f5dfc03f7ceec1ff1ebc138adb8b721a057d505ccfb0e61a8783af1371.png'
-    }
-  ], // 商品轮播图
-  introduction: '213', // 商品简介
-  deliveryTemplateId: 0, // 运费模版
+  picUrl: '', // 商品封面图
+  sliderPicUrls: [], // 商品轮播图
+  introduction: '', // 商品简介
+  deliveryTemplateId: 1, // 运费模版
+  brandId: null, // 商品品牌
   specType: false, // 商品规格
   subCommissionType: false, // 分销类型
   skus: [
     {
-      /**
-       * 商品价格,单位:分 TODO @puhui999:注释放在尾巴哈,简洁一点~
-       */
-      price: 0,
-      /**
-       * 市场价,单位:分
-       */
-      marketPrice: 0,
-      /**
-       * 成本价,单位:分
-       */
-      costPrice: 0,
-      /**
-       * 商品条码
-       */
-      barCode: '',
-      /**
-       * 图片地址
-       */
-      picUrl: '',
-      /**
-       * 库存
-       */
-      stock: 0,
-      /**
-       * 商品重量,单位:kg 千克
-       */
-      weight: 0,
-      /**
-       * 商品体积,单位:m^3 平米
-       */
-      volume: 0,
-      /**
-       * 一级分销的佣金,单位:分
-       */
-      subCommissionFirstPrice: 0,
-      /**
-       * 二级分销的佣金,单位:分
-       */
-      subCommissionSecondPrice: 0
+      price: 0, // 商品价格
+      marketPrice: 0, // 市场价
+      costPrice: 0, // 成本价
+      barCode: '', // 商品条码
+      picUrl: '', // 图片地址
+      stock: 0, // 库存
+      weight: 0, // 商品重量
+      volume: 0, // 商品体积
+      subCommissionFirstPrice: 0, // 一级分销的佣金
+      subCommissionSecondPrice: 0 // 二级分销的佣金
     }
   ],
-  description: '5425', // 商品详情
-  sort: 1, // 商品排序
-  giveIntegral: 1, // 赠送积分
-  virtualSalesCount: 1, // 虚拟销量
+  description: '', // 商品详情
+  sort: 0, // 商品排序
+  giveIntegral: 0, // 赠送积分
+  virtualSalesCount: 0, // 虚拟销量
   recommendHot: false, // 是否热卖
   recommendBenefit: false, // 是否优惠
   recommendBest: false, // 是否精品
@@ -122,19 +90,20 @@ const formData = ref<SpuType>({
 
 /** 获得详情 */
 const getDetail = async () => {
-  const id = query.id as unknown as number
+  const id = params.spuId as number
   if (id) {
     formLoading.value = true
     try {
-      const res = (await managementApi.getSpu(id)) as SpuType
+      const res = (await ProductSpuApi.getSpu(id)) as ProductSpuApi.SpuType
+      res.skus.forEach((item) => {
+        // 回显价格分转元
+        item.price = formatToFraction(item.price)
+        item.marketPrice = formatToFraction(item.marketPrice)
+        item.costPrice = formatToFraction(item.costPrice)
+        item.subCommissionFirstPrice = formatToFraction(item.subCommissionFirstPrice)
+        item.subCommissionSecondPrice = formatToFraction(item.subCommissionSecondPrice)
+      })
       formData.value = res
-      // 直接取第一个值就能得到所有属性的id
-      // TODO @puhui999:可以直接拿 propertyName 拼接处规格 id + 属性,可以看下商品 uniapp 详情的做法
-      const propertyIds = res.skus[0]?.properties.map((item) => item.propertyId)
-      const PropertyS = await PropertyApi.getPropertyListAndValue({ propertyIds })
-      await nextTick()
-      // 回显商品属性
-      BasicInfoRef.value.addAttribute(PropertyS)
     } finally {
       formLoading.value = false
     }
@@ -145,96 +114,66 @@ const getDetail = async () => {
 const submitForm = async () => {
   // 提交请求
   formLoading.value = true
-  const newSkus = JSON.parse(JSON.stringify(formData.value.skus)) //深拷贝一份skus保存失败时使用
-  // TODO 三个表单逐一校验,如果有一个表单校验不通过则切换到对应表单,如果有两个及以上的情况则切换到最前面的一个并弹出提示消息
+  // 三个表单逐一校验,如果有一个表单校验不通过则切换到对应表单,如果有两个及以上的情况则切换到最前面的一个并弹出提示消息
   // 校验各表单
   try {
-    await unref(BasicInfoRef)?.validate()
-    await unref(DescriptionRef)?.validate()
-    await unref(OtherSettingsRef)?.validate()
-    // TODO @puhui:直接做深拷贝?这样最终 server 端不满足,不需要恢复
-    // 处理掉一些无关数据
-    formData.value.skus.forEach((item) => {
-      // 给sku name赋值
-      item.name = formData.value.name
-      // 多规格情况移除skus相关属性值value
-      if (formData.value.specType) {
-        item.properties.forEach((item2) => {
-          delete item2.valueName
-        })
+    await unref(basicInfoRef)?.validate()
+    await unref(descriptionRef)?.validate()
+    await unref(otherSettingsRef)?.validate()
+    const deepCopyFormData = cloneDeep(unref(formData.value)) // 深拷贝一份 fix:这样最终 server 端不满足,不需要恢复
+    // TODO 兜底处理 sku 空数据
+    formData.value.skus.forEach((sku) => {
+      // 因为是空数据这里判断一下商品条码是否为空就行
+      if (sku.barCode === '') {
+        const index = deepCopyFormData.skus.findIndex(
+          (item) => JSON.stringify(item.properties) === JSON.stringify(sku.properties)
+        )
+        // 删除这条 sku
+        deepCopyFormData.skus.splice(index, 1)
       }
     })
+    deepCopyFormData.skus.forEach((item) => {
+      // 给sku name赋值
+      item.name = deepCopyFormData.name
+      // sku相关价格元转分
+      item.price = convertToInteger(item.price)
+      item.marketPrice = convertToInteger(item.marketPrice)
+      item.costPrice = convertToInteger(item.costPrice)
+      item.subCommissionFirstPrice = convertToInteger(item.subCommissionFirstPrice)
+      item.subCommissionSecondPrice = convertToInteger(item.subCommissionSecondPrice)
+    })
     // 处理轮播图列表
     const newSliderPicUrls = []
-    formData.value.sliderPicUrls.forEach((item) => {
+    deepCopyFormData.sliderPicUrls.forEach((item) => {
       // 如果是前端选的图
-      // TODO @puhui999:疑问哈,为啥会是 object 呀?
-      if (typeof item === 'object') {
-        newSliderPicUrls.push(item.url)
-      } else {
-        newSliderPicUrls.push(item)
-      }
+      // TODO @puhui999:疑问哈,为啥会是 object 呀?fix
+      typeof item === 'object' ? newSliderPicUrls.push(item.url) : newSliderPicUrls.push(item)
     })
-    formData.value.sliderPicUrls = newSliderPicUrls
+    deepCopyFormData.sliderPicUrls = newSliderPicUrls
     // 校验都通过后提交表单
-    const data = formData.value as SpuType
-    // 移除skus.
-    const id = query.id as unknown as number
+    const data = deepCopyFormData as ProductSpuApi.SpuType
+    const id = params.spuId as number
     if (!id) {
-      await managementApi.createSpu(data)
+      await ProductSpuApi.createSpu(data)
       message.success(t('common.createSuccess'))
     } else {
-      await managementApi.updateSpu(data)
+      await ProductSpuApi.updateSpu(data)
       message.success(t('common.updateSuccess'))
     }
     close()
-  } catch (e) {
-    // 如果是后端校验失败,恢复skus数据
-    if (typeof e === 'string') {
-      formData.value.skus = newSkus
-    }
   } finally {
     formLoading.value = false
   }
 }
 
-/**
- * 重置表单
- */
-const resetForm = async () => {
-  formData.value = {
-    name: '', // 商品名称
-    categoryId: 0, // 商品分类
-    keyword: '', // 关键字
-    unit: '', // 单位
-    picUrl: '', // 商品封面图
-    sliderPicUrls: [], // 商品轮播图
-    introduction: '', // 商品简介
-    deliveryTemplateId: 0, // 运费模版
-    selectRule: '',
-    specType: false, // 商品规格
-    subCommissionType: false, // 分销类型
-    description: '', // 商品详情
-    sort: 1, // 商品排序
-    giveIntegral: 1, // 赠送积分
-    virtualSalesCount: 1, // 虚拟销量
-    recommendHot: false, // 是否热卖
-    recommendBenefit: false, // 是否优惠
-    recommendBest: false, // 是否精品
-    recommendNew: false, // 是否新品
-    recommendGood: false // 是否优品
-  }
-}
 /** 关闭按钮 */
 const close = () => {
-  // TODO @puhui999:是不是不用 reset 呀?close 默认销毁
-  resetForm()
   delView(unref(currentRoute))
-  push('/product/product-management')
+  push('/product/product-spu')
 }
 
 /** 初始化 */
-onMounted(() => {
-  getDetail()
+onMounted(async () => {
+  await getDetail()
 })
 </script>

+ 75 - 36
src/views/mall/product/spu/components/BasicInfoForm.vue

@@ -1,5 +1,5 @@
 <template>
-  <el-form ref="ProductManagementBasicInfoRef" :model="formData" :rules="rules" label-width="120px">
+  <el-form ref="productSpuBasicInfoRef" :model="formData" :rules="rules" label-width="120px">
     <el-row>
       <el-col :span="12">
         <el-form-item label="商品名称" prop="name">
@@ -14,9 +14,9 @@
             :data="categoryList"
             :props="defaultProps"
             check-strictly
+            class="w-1/1"
             node-key="id"
             placeholder="请选择商品分类"
-            class="w-1/1"
           />
         </el-form-item>
       </el-col>
@@ -27,7 +27,7 @@
       </el-col>
       <el-col :span="12">
         <el-form-item label="单位" prop="unit">
-          <el-select v-model="formData.unit" placeholder="请选择单位" class="w-1/1">
+          <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"
@@ -54,18 +54,28 @@
       </el-col>
       <el-col :span="24">
         <el-form-item label="商品轮播图" prop="sliderPicUrls">
-          <UploadImgs v-model="formData.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="请选择" class="w-1/1">
+          <el-select v-model="formData.deliveryTemplateId" placeholder="请选择">
             <el-option v-for="item in []" :key="item.id" :label="item.name" :value="item.id" />
           </el-select>
+          <el-button class="ml-20px">运费模板</el-button>
         </el-form-item>
       </el-col>
       <el-col :span="12">
-        <el-button class="ml-20px">运费模板</el-button>
+        <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">
@@ -86,36 +96,38 @@
       <!-- 多规格添加-->
       <el-col :span="24">
         <el-form-item v-if="formData.specType" label="商品属性">
-          <!-- TODO @puhui999:参考 https://admin.java.crmeb.net/store/list/creatProduct 添加规格好做么?添加的时候,不用输入备注哈 -->
-          <el-button class="mr-15px mb-10px" @click="AttributesAddFormRef.open">添加规格</el-button>
-          <ProductAttributes :attribute-data="attributeList" />
+          <!-- TODO @puhui999:参考 https://admin.java.crmeb.net/store/list/creatProduct 添加规格好做么?添加的时候,不用输入备注哈 fix-->
+          <el-button class="mr-15px mb-10px" @click="attributesAddFormRef.open">添加规格</el-button>
+          <ProductAttributes :propertyList="propertyList" @success="generateSkus" />
         </el-form-item>
-        <template v-if="formData.specType && attributeList.length > 0">
+        <template v-if="formData.specType && propertyList.length > 0">
           <el-form-item label="批量设置">
-            <SkuList :attributeList="attributeList" :is-batch="true" :prop-form-data="formData" />
+            <SkuList :is-batch="true" :prop-form-data="formData" :propertyList="propertyList" />
           </el-form-item>
           <el-form-item label="属性列表">
-            <SkuList :attributeList="attributeList" :prop-form-data="formData" />
+            <SkuList ref="skuListRef" :prop-form-data="formData" :propertyList="propertyList" />
           </el-form-item>
         </template>
         <el-form-item v-if="!formData.specType">
-          <SkuList :attributeList="attributeList" :prop-form-data="formData" />
+          <SkuList :prop-form-data="formData" :propertyList="propertyList" />
         </el-form-item>
       </el-col>
     </el-row>
   </el-form>
-  <ProductAttributesAddForm ref="AttributesAddFormRef" @success="addAttribute" />
+  <ProductAttributesAddForm ref="attributesAddFormRef" :propertyList="propertyList" />
 </template>
-<script lang="ts" name="ProductManagementBasicInfoForm" setup>
+<script lang="ts" name="ProductSpuBasicInfoForm" setup>
 import { PropType } from 'vue'
+import { copyValueToTarget } from '@/utils'
+import { propTypes } from '@/utils/propTypes'
 import { defaultProps, handleTree } from '@/utils/tree'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import type { SpuType } from '@/api/mall/product/management/type/spuType'
+import type { SpuType } from '@/api/mall/product/spu'
 import { UploadImg, UploadImgs } from '@/components/UploadFile'
-import { copyValueToTarget } from '@/utils/object'
 import { ProductAttributes, ProductAttributesAddForm, SkuList } from './index'
 import * as ProductCategoryApi from '@/api/mall/product/category'
-import { propTypes } from '@/utils/propTypes'
+import { getSimpleBrandList } from '@/api/mall/product/brand'
+
 const message = useMessage() // 消息弹窗
 
 const props = defineProps({
@@ -125,27 +137,25 @@ const props = defineProps({
   },
   activeName: propTypes.string.def('')
 })
-const AttributesAddFormRef = ref() // 添加商品属性表单 TODO @puhui999:小写开头哈
-const ProductManagementBasicInfoRef = ref() // 表单Ref TODO @puhui999:小写开头哈
-// TODO @puhui999:attributeList 改成 propertyList,会更统一一点
-const attributeList = ref([]) // 商品属性列表
-/** 添加商品属性 */ // TODO @puhui999:propFormData 算出来
-const addAttribute = (property: any) => {
-  if (Array.isArray(property)) {
-    attributeList.value = property
-    return
-  }
-  attributeList.value.push(property)
+const attributesAddFormRef = ref() // 添加商品属性表单 TODO @puhui999:小写开头哈 fix
+const productSpuBasicInfoRef = ref() // 表单Ref TODO @puhui999:小写开头哈  fix
+// TODO @puhui999:attributeList 改成 propertyList,会更统一一点 fix
+const propertyList = ref([]) // 商品属性列表
+const skuListRef = ref() // 商品属性列表Ref
+/** 调用 SkuList generateTableData 方法*/
+const generateSkus = (propertyList) => {
+  skuListRef.value.generateTableData(propertyList)
 }
 const formData = reactive<SpuType>({
   name: '', // 商品名称
-  categoryId: undefined, // 商品分类
+  categoryId: null, // 商品分类
   keyword: '', // 关键字
   unit: '', // 单位
   picUrl: '', // 商品封面图
   sliderPicUrls: [], // 商品轮播图
   introduction: '', // 商品简介
   deliveryTemplateId: 1, // 运费模版
+  brandId: null, // 商品品牌
   specType: false, // 商品规格
   subCommissionType: false, // 分销类型
   skus: []
@@ -159,6 +169,7 @@ const rules = reactive({
   picUrl: [required],
   sliderPicUrls: [required],
   // deliveryTemplateId: [required],
+  brandId: [required],
   specType: [required],
   subCommissionType: [required]
 })
@@ -170,10 +181,35 @@ watch(
   () => props.propFormData,
   (data) => {
     if (!data) return
+    // fix:三个表单组件监听赋值必须使用 copyValueToTarget 使用 formData.value = data 会监听非常多次
     copyValueToTarget(formData, data)
+    // fix: 多图上传组件需要一个包含url属性的对象才能正常回显
+    formData.sliderPicUrls = data['sliderPicUrls'].map((item) => ({
+      url: item
+    }))
+    // 只有是多规格才处理
+    if (formData.specType) {
+      // TODO @puhui999:可以直接拿 propertyName 拼接处规格 id + 属性,可以看下商品 uniapp 详情的做法
+      // fix: 直接拿返回的 skus 属性逆向生成出 propertyList
+      const properties = []
+      formData.skus.forEach((sku) => {
+        sku.properties.forEach(({ propertyId, propertyName, valueId, valueName }) => {
+          // 添加属性
+          if (!properties.some((item) => item.id === propertyId)) {
+            properties.push({ id: propertyId, name: propertyName, values: [] })
+          }
+          // 添加属性值
+          const index = properties.findIndex((item) => item.id === propertyId)
+          if (!properties[index].values.some((value) => value.id === valueId)) {
+            properties[index].values.push({ id: valueId, name: valueName })
+          }
+        })
+      })
+      propertyList.value = properties
+    }
   },
   {
-    deep: true,
+    // fix: 去掉深度监听只有对象引用发生改变的时候才执行,解决改一动多的问题
     immediate: true
   }
 )
@@ -184,8 +220,8 @@ watch(
 const emit = defineEmits(['update:activeName'])
 const validate = async () => {
   // 校验表单
-  if (!ProductManagementBasicInfoRef) return
-  return await unref(ProductManagementBasicInfoRef).validate((valid) => {
+  if (!productSpuBasicInfoRef) return
+  return await unref(productSpuBasicInfoRef).validate((valid) => {
     if (!valid) {
       message.warning('商品信息未完善!!')
       emit('update:activeName', 'basicInfo')
@@ -197,7 +233,7 @@ const validate = async () => {
     }
   })
 }
-defineExpose({ validate, addAttribute })
+defineExpose({ validate })
 
 /** 分销类型 */
 const changeSubCommissionType = () => {
@@ -211,7 +247,7 @@ const changeSubCommissionType = () => {
 /** 选择规格 */
 const onChangeSpec = () => {
   // 重置商品属性列表
-  attributeList.value = []
+  propertyList.value = []
   // 重置sku列表
   formData.skus = [
     {
@@ -229,10 +265,13 @@ const onChangeSpec = () => {
   ]
 }
 
-const categoryList = ref() // 分类树
+const categoryList = ref([]) // 分类树
+const brandList = ref([]) // 精简商品品牌列表
 onMounted(async () => {
   // 获得分类树
   const data = await ProductCategoryApi.getCategoryList({})
   categoryList.value = handleTree(data, 'id', 'parentId')
+  // 获取商品品牌列表
+  brandList.value = await getSimpleBrandList()
 })
 </script>

+ 8 - 9
src/views/mall/product/spu/components/DescriptionForm.vue

@@ -1,5 +1,5 @@
 <template>
-  <el-form ref="DescriptionFormRef" :model="formData" :rules="rules" label-width="120px">
+  <el-form ref="descriptionFormRef" :model="formData" :rules="rules" label-width="120px">
     <!--富文本编辑器组件-->
     <el-form-item label="商品详情" prop="description">
       <Editor v-model:modelValue="formData.description" />
@@ -7,11 +7,11 @@
   </el-form>
 </template>
 <script lang="ts" name="DescriptionForm" setup>
-import type { SpuType } from '@/api/mall/product/management/type/spuType'
+import type { SpuType } from '@/api/mall/product/spu'
 import { Editor } from '@/components/Editor'
 import { PropType } from 'vue'
-import { copyValueToTarget } from '@/utils/object'
 import { propTypes } from '@/utils/propTypes'
+import { copyValueToTarget } from '@/utils'
 
 const message = useMessage() // 消息弹窗
 const props = defineProps({
@@ -21,7 +21,7 @@ const props = defineProps({
   },
   activeName: propTypes.string.def('')
 })
-const DescriptionFormRef = ref() // 表单Ref
+const descriptionFormRef = ref() // 表单Ref
 const formData = ref<SpuType>({
   description: '' // 商品详情
 })
@@ -29,7 +29,6 @@ const formData = ref<SpuType>({
 const rules = reactive({
   description: [required]
 })
-
 /**
  * 富文本编辑器如果输入过再清空会有残留,需再重置一次
  */
@@ -45,7 +44,6 @@ watch(
     immediate: true
   }
 )
-
 /**
  * 将传进来的值赋值给formData
  */
@@ -53,10 +51,11 @@ watch(
   () => props.propFormData,
   (data) => {
     if (!data) return
+    // fix:三个表单组件监听赋值必须使用 copyValueToTarget 使用 formData.value = data 会监听非常多次
     copyValueToTarget(formData.value, data)
   },
   {
-    deep: true,
+    // fix: 去掉深度监听只有对象引用发生改变的时候才执行,解决改一动多的问题
     immediate: true
   }
 )
@@ -67,8 +66,8 @@ watch(
 const emit = defineEmits(['update:activeName'])
 const validate = async () => {
   // 校验表单
-  if (!DescriptionFormRef) return
-  return unref(DescriptionFormRef).validate((valid) => {
+  if (!descriptionFormRef) return
+  return await unref(descriptionFormRef).validate((valid) => {
     if (!valid) {
       message.warning('商品详情为完善!!')
       emit('update:activeName', 'description')

+ 56 - 64
src/views/mall/product/spu/components/OtherSettingsForm.vue

@@ -1,32 +1,34 @@
 <template>
-  <el-form ref="OtherSettingsFormRef" :model="formData" :rules="rules" label-width="120px">
+  <el-form ref="otherSettingsFormRef" :model="formData" :rules="rules" label-width="120px">
     <el-row>
-      <!-- TODO @puhui999:横着三个哈 -->
+      <!-- TODO @puhui999:横着三个哈  fix-->
       <el-col :span="24">
-        <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 :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 recommend" :key="index" :label="item.value">
+            <el-checkbox v-for="(item, index) in recommendOptions" :key="index" :label="item.value">
               {{ item.name }}
             </el-checkbox>
           </el-checkbox-group>
@@ -51,10 +53,11 @@
   </el-form>
 </template>
 <script lang="ts" name="OtherSettingsForm" setup>
-import type { SpuType } from '@/api/mall/product/management/type/spuType'
+import type { SpuType } from '@/api/mall/product/spu'
 import { PropType } from 'vue'
-import { copyValueToTarget } from '@/utils/object'
 import { propTypes } from '@/utils/propTypes'
+import { copyValueToTarget } from '@/utils'
+
 const message = useMessage() // 消息弹窗
 
 const props = defineProps({
@@ -64,35 +67,8 @@ const props = defineProps({
   },
   activeName: propTypes.string.def('')
 })
-// 商品推荐选项 TODO @puhui999:这种叫 recommendOptions 会更合适哈
-const recommend = [
-  { name: '是否热卖', value: 'recommendHot' },
-  { name: '是否优惠', value: 'recommendBenefit' },
-  { name: '是否精品', value: 'recommendBest' },
-  { name: '是否新品', value: 'recommendNew' },
-  { name: '是否优品', value: 'recommendGood' }
-]
-const checkboxGroup = ref<string[]>(['recommendHot']) // 选中推荐选项
-/** 选择商品后赋值 */
-const onChangeGroup = () => {
-  // TODO @puhui999:是不是可以遍历 recommend,然后进行是否选中;
-  checkboxGroup.value.includes('recommendHot')
-    ? (formData.value.recommendHot = true)
-    : (formData.value.recommendHot = false)
-  checkboxGroup.value.includes('recommendBenefit')
-    ? (formData.value.recommendBenefit = true)
-    : (formData.value.recommendBenefit = false)
-  checkboxGroup.value.includes('recommendBest')
-    ? (formData.value.recommendBest = true)
-    : (formData.value.recommendBest = false)
-  checkboxGroup.value.includes('recommendNew')
-    ? (formData.value.recommendNew = true)
-    : (formData.value.recommendNew = false)
-  checkboxGroup.value.includes('recommendGood')
-    ? (formData.value.recommendGood = true)
-    : (formData.value.recommendGood = false)
-}
-const OtherSettingsFormRef = ref() // 表单Ref
+
+const otherSettingsFormRef = ref() // 表单Ref
 // 表单数据
 const formData = ref<SpuType>({
   sort: 1, // 商品排序
@@ -110,6 +86,23 @@ const rules = reactive({
   giveIntegral: [required],
   virtualSalesCount: [required]
 })
+// TODO @puhui999:这种叫 recommendOptions 会更合适哈 fix
+const recommendOptions = [
+  { name: '是否热卖', value: 'recommendHot' },
+  { name: '是否优惠', value: 'recommendBenefit' },
+  { name: '是否精品', value: 'recommendBest' },
+  { name: '是否新品', value: 'recommendNew' },
+  { name: '是否优品', value: 'recommendGood' }
+] // 商品推荐选项
+const checkboxGroup = ref<string[]>([]) // 选中的推荐选项
+
+/** 选择商品后赋值 */
+const onChangeGroup = () => {
+  // TODO @puhui999:是不是可以遍历 recommend,然后进行是否选中;fix
+  recommendOptions.forEach(({ value }) => {
+    formData.value[value] = checkboxGroup.value.includes(value)
+  })
+}
 
 /**
  * 将传进来的值赋值给formData
@@ -118,29 +111,28 @@ watch(
   () => props.propFormData,
   (data) => {
     if (!data) return
+    // fix:三个表单组件监听赋值必须使用 copyValueToTarget 使用 formData.value = data 会监听非常多次
     copyValueToTarget(formData.value, data)
-    // TODO 如果先修改其他设置的值,再改变商品详情或是商品信息会重置其他设置页面中的相关值 下一个版本修复
-    checkboxGroup.value = []
-    formData.value.recommendHot ? checkboxGroup.value.push('recommendHot') : ''
-    formData.value.recommendBenefit ? checkboxGroup.value.push('recommendBenefit') : ''
-    formData.value.recommendBest ? checkboxGroup.value.push('recommendBest') : ''
-    formData.value.recommendNew ? checkboxGroup.value.push('recommendNew') : ''
-    formData.value.recommendGood ? checkboxGroup.value.push('recommendGood') : ''
+    recommendOptions.forEach(({ value }) => {
+      // TODO 如果先修改其他设置的值,再改变商品详情或是商品信息会重置其他设置页面中的相关值 fix:已修复
+      if (formData.value[value] && !checkboxGroup.value.includes(value)) {
+        checkboxGroup.value.push(value)
+      }
+    })
   },
   {
-    deep: true,
+    // fix: 去掉深度监听只有对象引用发生改变的时候才执行,解决改一动多的问题
     immediate: true
   }
 )
-
 /**
  * 表单校验
  */
 const emit = defineEmits(['update:activeName'])
 const validate = async () => {
   // 校验表单
-  if (!OtherSettingsFormRef) return
-  return await unref(OtherSettingsFormRef).validate((valid) => {
+  if (!otherSettingsFormRef) return
+  return await unref(otherSettingsFormRef).validate((valid) => {
     if (!valid) {
       message.warning('商品其他设置未完善!!')
       emit('update:activeName', 'otherSettings')

+ 27 - 12
src/views/mall/product/spu/components/ProductAttributes.vue

@@ -2,23 +2,25 @@
   <el-col v-for="(item, index) in attributeList" :key="index">
     <div>
       <el-text class="mx-1">属性名:</el-text>
-      <el-text class="mx-1">{{ item.name }}</el-text>
+      <el-tag class="mx-1" closable type="success" @close="handleCloseProperty(index)"
+        >{{ item.name }}
+      </el-tag>
     </div>
     <div>
       <el-text class="mx-1">属性值:</el-text>
       <el-tag
         v-for="(value, valueIndex) in item.values"
         :key="value.id"
-        :disable-transitions="false"
         class="mx-1"
         closable
-        @close="handleClose(index, valueIndex)"
+        @close="handleCloseValue(index, valueIndex)"
       >
         {{ value.name }}
       </el-tag>
       <el-input
         v-show="inputVisible(index)"
-        ref="InputRef"
+        :id="`input${index}`"
+        :ref="setInputRef"
         v-model="inputValue"
         class="!w-20"
         size="small"
@@ -51,17 +53,25 @@ const inputVisible = computed(() => (index) => {
   if (attributeIndex.value === null) return false
   if (attributeIndex.value === index) return true
 })
-const InputRef = ref() //标签输入框Ref
+const inputRef = ref([]) //标签输入框Ref
+/** 解决 ref 在 v-for 中的获取问题*/
+const setInputRef = (el) => {
+  if (el === null || typeof el === 'undefined') return
+  // 如果不存在id相同的元素才添加
+  if (!inputRef.value.some((item) => item.input?.attributes.id === el.input?.attributes.id)) {
+    inputRef.value.push(el)
+  }
+}
 const attributeList = ref([]) // 商品属性列表
 const props = defineProps({
-  attributeData: {
+  propertyList: {
     type: Array,
     default: () => {}
   }
 })
 
 watch(
-  () => props.attributeData,
+  () => props.propertyList,
   (data) => {
     if (!data) return
     attributeList.value = data
@@ -72,18 +82,22 @@ watch(
   }
 )
 
-/** 删除标签 tagValue 标签值*/
-const handleClose = (index, valueIndex) => {
+/** 删除属性值*/
+const handleCloseValue = (index, valueIndex) => {
   attributeList.value[index].values?.splice(valueIndex, 1)
 }
-
+/** 删除属性*/
+const handleCloseProperty = (index) => {
+  attributeList.value?.splice(index, 1)
+}
 /** 显示输入框并获取焦点 */
 const showInput = async (index) => {
   attributeIndex.value = index
-  // 因为组件在ref中所以需要用索引获取对应的Ref
-  InputRef.value[index]!.input!.focus()
+  inputRef.value[index].focus()
 }
 
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+
 /** 输入框失去焦点或点击回车时触发 */
 const handleInputConfirm = async (index, propertyId) => {
   if (inputValue.value) {
@@ -92,6 +106,7 @@ const handleInputConfirm = async (index, propertyId) => {
       const id = await PropertyApi.createPropertyValue({ propertyId, name: inputValue.value })
       attributeList.value[index].values.push({ id, name: inputValue.value })
       message.success(t('common.createSuccess'))
+      emit('success', attributeList.value)
     } catch {
       message.error('添加失败,请重试') // TODO 缺少国际化
     }

+ 22 - 9
src/views/mall/product/spu/components/ProductAttributesAddForm.vue

@@ -7,12 +7,9 @@
       :rules="formRules"
       label-width="80px"
     >
-      <el-form-item label="名称" prop="name">
+      <el-form-item label="属性名称" prop="name">
         <el-input v-model="formData.name" placeholder="请输入名称" />
       </el-form-item>
-      <el-form-item label="备注" prop="remark">
-        <el-input v-model="formData.remark" placeholder="请输入内容" type="textarea" />
-      </el-form-item>
     </el-form>
     <template #footer>
       <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
@@ -30,14 +27,31 @@ const dialogVisible = ref(false) // 弹窗的是否展示
 const dialogTitle = ref('添加商品属性') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formData = ref({
-  name: '',
-  remark: ''
+  name: ''
 })
 const formRules = reactive({
   name: [{ required: true, message: '名称不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
+const attributeList = ref([]) // 商品属性列表
+const props = defineProps({
+  propertyList: {
+    type: Array,
+    default: () => {}
+  }
+})
 
+watch(
+  () => props.propertyList,
+  (data) => {
+    if (!data) return
+    attributeList.value = data
+  },
+  {
+    deep: true,
+    immediate: true
+  }
+)
 /** 打开弹窗 */
 const open = async () => {
   dialogVisible.value = true
@@ -46,7 +60,6 @@ const open = async () => {
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
 /** 提交表单 */
-const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
 const submitForm = async () => {
   // 校验表单
   if (!formRef) return
@@ -60,12 +73,12 @@ const submitForm = async () => {
     const res = await PropertyApi.getPropertyListAndValue({ name: data.name })
     if (res.length === 0) {
       const propertyId = await PropertyApi.createProperty(data)
-      emit('success', { id: propertyId, ...formData.value, values: [] })
+      attributeList.value.push({ id: propertyId, ...formData.value, values: [] })
     } else {
       if (res[0].values === null) {
         res[0].values = []
       }
-      emit('success', res[0]) // 因为只用一个
+      attributeList.value.push(res[0]) // 因为只用一个
     }
     message.success(t('common.createSuccess'))
     dialogVisible.value = false

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

@@ -1,6 +1,6 @@
 <template>
   <el-table
-    :data="isBatch ? SkuData : formData.skus"
+    :data="isBatch ? skuList : formData.skus"
     border
     class="tabNumWidth"
     max-height="500"
@@ -14,7 +14,7 @@
     <template v-if="formData.specType && !isBatch">
       <!--  根据商品属性动态添加  -->
       <el-table-column
-        v-for="(item, index) in tableHeaderList"
+        v-for="(item, index) in tableHeaders"
         :key="index"
         :label="item.label"
         align="center"
@@ -25,162 +25,141 @@
         </template>
       </el-table-column>
     </template>
-    <!-- TODO @puhui999: controls-position="right" 可以去掉哈,不然太长了,手动输入更方便 -->
+    <!-- TODO @puhui999: controls-position=" " 可以去掉哈,不然太长了,手动输入更方便 fix -->
     <el-table-column align="center" label="商品条码" min-width="168">
       <template #default="{ row }">
         <el-input v-model="row.barCode" class="w-100%" />
       </template>
     </el-table-column>
-    <!-- TODO @puhui999:用户输入的时候,是按照元;分主要是我们自己用; -->
-    <el-table-column align="center" label="销售价()" min-width="168">
+    <!-- TODO @puhui999:用户输入的时候,是按照元;分主要是我们自己用;fix -->
+    <el-table-column align="center" label="销售价()" min-width="168">
       <template #default="{ row }">
-        <el-input-number v-model="row.price" :min="0" class="w-100%" controls-position="right" />
+        <el-input-number v-model="row.price" :min="0" :precision="2" :step="0.1" 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.marketPrice"
           :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.costPrice"
           :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">
       <template #default="{ row }">
-        <el-input-number v-model="row.stock" :min="0" class="w-100%" controls-position="right" />
+        <el-input-number v-model="row.stock" :min="0" class="w-100%" />
       </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" class="w-100%" controls-position="right" />
+        <el-input-number v-model="row.weight" :min="0" :precision="2" :step="0.1" class="w-100%" />
       </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" class="w-100%" controls-position="right" />
+        <el-input-number v-model="row.volume" :min="0" :precision="2" :step="0.1" class="w-100%" />
       </template>
     </el-table-column>
     <template v-if="formData.subCommissionType">
-      <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.subCommissionFirstPrice"
             :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.subCommissionSecondPrice"
             :min="0"
+            :precision="2"
+            :step="0.1"
             class="w-100%"
-            controls-position="right"
           />
         </template>
       </el-table-column>
     </template>
     <el-table-column v-if="formData.specType" align="center" fixed="right" label="操作" width="80">
-      <template #default>
+      <template #default="{ row }">
         <el-button v-if="isBatch" link size="small" type="primary" @click="batchAdd">
           批量添加
         </el-button>
-        <el-button v-else link size="small" type="primary">删除</el-button>
+        <el-button v-else link size="small" type="primary" @click="deleteSku(row)">删除</el-button>
       </template>
     </el-table-column>
   </el-table>
 </template>
 <script lang="ts" name="SkuList" setup>
-import { UploadImg } from '@/components/UploadFile'
 import { PropType } from 'vue'
-import { SpuType } from '@/api/mall/product/management/type/spuType'
+import { copyValueToTarget } from '@/utils'
 import { propTypes } from '@/utils/propTypes'
-import { SkuType } from '@/api/mall/product/management/type/skuType'
-import { copyValueToTarget } from '@/utils/object'
+import { UploadImg } from '@/components/UploadFile'
+import type { Property, SkuType, SpuType } from '@/api/mall/product/spu'
 
 const props = defineProps({
   propFormData: {
     type: Object as PropType<SpuType>,
     default: () => {}
   },
-  attributeList: {
+  propertyList: {
     type: Array,
     default: () => []
   },
-  isBatch: propTypes.bool.def(false) // 是否批量操作
+  isBatch: propTypes.bool.def(false) // 是否作为批量操作组件
 })
 const formData = ref<SpuType>() // 表单数据
-// 批量添加时的零时数据 TODO @puhui999:小写开头哈;然后变量都尾注释
-const SkuData = ref<SkuType[]>([
+const skuList = ref<SkuType[]>([
   {
-    /**
-     * 商品价格,单位:分
-     */
-    price: 0,
-    /**
-     * 市场价,单位:分
-     */
-    marketPrice: 0,
-    /**
-     * 成本价,单位:分
-     */
-    costPrice: 0,
-    /**
-     * 商品条码
-     */
-    barCode: '',
-    /**
-     * 图片地址
-     */
-    picUrl: '',
-    /**
-     * 库存
-     */
-    stock: 0,
-    /**
-     * 商品重量,单位:kg 千克
-     */
-    weight: 0,
-    /**
-     * 商品体积,单位:m^3 平米
-     */
-    volume: 0,
-    /**
-     * 一级分销的佣金,单位:分
-     */
-    subCommissionFirstPrice: 0,
-    /**
-     * 二级分销的佣金,单位:分
-     */
-    subCommissionSecondPrice: 0
+    price: 0, // 商品价格
+    marketPrice: 0, // 市场价
+    costPrice: 0, // 成本价
+    barCode: '', // 商品条码
+    picUrl: '', // 图片地址
+    stock: 0, // 库存
+    weight: 0, // 商品重量
+    volume: 0, // 商品体积
+    subCommissionFirstPrice: 0, // 一级分销的佣金
+    subCommissionSecondPrice: 0 // 二级分销的佣金
   }
-])
+]) // 批量添加时的临时数据
 
 /** 批量添加 */
 const batchAdd = () => {
   formData.value.skus.forEach((item) => {
-    copyValueToTarget(item, SkuData.value[0])
+    copyValueToTarget(item, skuList.value[0])
   })
 }
-
-const tableHeaderList = ref<{ prop: string; label: string }[]>([])
+/** 删除 sku */
+const deleteSku = (row) => {
+  const index = formData.value.skus.findIndex(
+    // 直接把列表转成字符串比较
+    (sku) => JSON.stringify(sku.properties) === JSON.stringify(row.properties)
+  )
+  formData.value.skus.splice(index, 1)
+}
+const tableHeaders = ref<{ prop: string; label: string }[]>([]) // 多属性表头
 
 /**
- * 将传进来的值赋值给SkuData
+ * 将传进来的值赋值给skuList
  */
 watch(
   () => props.propFormData,
@@ -194,35 +173,27 @@ watch(
   }
 )
 
-// TODO @芋艿:看看 chatgpt 可以进一步下面几个方法的实现不
+// TODO @芋艿:看看 chatgpt 可以进一步下面几个方法的实现不 fix: 添加相关处理逻辑解决编辑表单时或查看详情时数据回显问题
 /** 生成表数据 */
-const generateTableData = (data: any[]) => {
-  // 构建数据结构
-  const propertiesItemList = []
-  for (const item of data) {
-    const objList = []
-    for (const v of item.values) {
-      const obj = { propertyId: 0, valueId: 0, valueName: '' }
-      obj.propertyId = item.id
-      obj.valueId = v.id
-      obj.valueName = v.name
-      objList.push(obj)
-    }
-    propertiesItemList.push(objList)
-  }
+const generateTableData = (propertyList: any[]) => {
+  // 构建数据结构 fix: 使用map替换多重for循环
+  const propertiesItemList = propertyList.map((item) =>
+    item.values.map((v) => ({
+      propertyId: item.id,
+      propertyName: item.name,
+      valueId: v.id,
+      valueName: v.name
+    }))
+  )
   const buildList = build(propertiesItemList)
-  // 如果构建后的组合数跟sku数量一样的话则不用处理,添加新属性没有属性值也不做处理 (解决编辑表单时或查看详情时数据回显问题)
-  if (
-    buildList.length === formData.value.skus.length ||
-    data.some((item) => item.values.length === 0)
-  ) {
-    return
+  // 如果回显的 sku 属性和添加的属性不一致则重置 skus 列表
+  if (!validateData(propertyList)) {
+    // 如果不一致则重置表数据,默认添加新的属性重新生成sku列表
+    formData.value!.skus = []
   }
-  // 重置表数据
-  formData.value!.skus = []
-  buildList.forEach((item) => {
+  for (const item of buildList) {
     const row = {
-      properties: [],
+      properties: Array.isArray(item) ? item : [item], // 如果只有一个属性的话返回的是一个property对象
       price: 0,
       marketPrice: 0,
       costPrice: 0,
@@ -234,32 +205,49 @@ const generateTableData = (data: any[]) => {
       subCommissionFirstPrice: 0,
       subCommissionSecondPrice: 0
     }
-    // 判断是否是单一属性的情况
-    if (Array.isArray(item)) {
-      row.properties = item
-    } else {
-      row.properties.push(item)
+    const index = formData.value!.skus.findIndex(
+      (sku) => JSON.stringify(sku.properties) === JSON.stringify(row.properties)
+    )
+    // 如果存在属性相同的 sku 则不做处理
+    if (index !== -1) {
+      continue
     }
     formData.value.skus.push(row)
-  })
+  }
+}
+/**
+ * 生成 skus 前置校验
+ */
+const validateData = (propertyList: any[]) => {
+  const skuPropertyIds = []
+  formData.value.skus.forEach((sku) =>
+    sku.properties
+      ?.map((property) => property.propertyId)
+      .forEach((propertyId) => {
+        if (skuPropertyIds.indexOf(propertyId) === -1) {
+          skuPropertyIds.push(propertyId)
+        }
+      })
+  )
+  const propertyIds = propertyList.map((item) => item.id)
+  return skuPropertyIds.length === propertyIds.length
 }
-
 /** 构建所有排列组合 */
-const build = (list: any[]) => {
-  if (list.length === 0) {
+const build = (propertyValuesList: Property[][]) => {
+  if (propertyValuesList.length === 0) {
     return []
-  } else if (list.length === 1) {
-    return list[0]
+  } else if (propertyValuesList.length === 1) {
+    return propertyValuesList[0]
   } else {
-    const result = []
-    const rest = build(list.slice(1))
-    for (let i = 0; i < list[0].length; i++) {
+    const result: Property[][] = []
+    const rest = build(propertyValuesList.slice(1))
+    for (let i = 0; i < propertyValuesList[0].length; i++) {
       for (let j = 0; j < rest.length; j++) {
         // 第一次不是数组结构,后面的都是数组结构
         if (Array.isArray(rest[j])) {
-          result.push([list[0][i], ...rest[j]])
+          result.push([propertyValuesList[0][i], ...rest[j]])
         } else {
-          result.push([list[0][i], rest[j]])
+          result.push([propertyValuesList[0][i], rest[j]])
         }
       }
     }
@@ -269,13 +257,13 @@ const build = (list: any[]) => {
 
 /** 监听属性列表生成相关参数和表头 */
 watch(
-  () => props.attributeList,
-  (data) => {
+  () => props.propertyList,
+  (propertyList) => {
     // 如果不是多规格则结束
     if (!formData.value.specType) return
     // 如果当前组件作为批量添加数据使用则重置表数据
     if (props.isBatch) {
-      SkuData.value = [
+      skuList.value = [
         {
           price: 0,
           marketPrice: 0,
@@ -291,19 +279,25 @@ watch(
       ]
     }
     // 判断代理对象是否为空
-    if (JSON.stringify(data) === '[]') return
+    if (JSON.stringify(propertyList) === '[]') return
     // 重置表头
-    tableHeaderList.value = []
+    tableHeaders.value = []
     // 生成表头
-    data.forEach((item, index) => {
+    propertyList.forEach((item, index) => {
       // name加属性项index区分属性值
-      tableHeaderList.value.push({ prop: `name${index}`, label: item.name })
+      tableHeaders.value.push({ prop: `name${index}`, label: item.name })
     })
-    generateTableData(data)
+    // 如果回显的 sku 属性和添加的属性一致则不处理
+    if (validateData(propertyList)) return
+    // 添加新属性没有属性值也不做处理
+    if (propertyList.some((item) => item.values.length === 0)) return
+    generateTableData(propertyList)
   },
   {
     deep: true,
     immediate: true
   }
 )
+// 暴露出生成 sku 方法给添加属性成功时调用 fix: 为了在只有一个属性下 spu 回显 skus 属性和和商品属性个数一致的情况下 添加属性值时添加 sku
+defineExpose({ generateTableData })
 </script>

+ 124 - 109
src/views/mall/product/spu/index.vue

@@ -8,7 +8,7 @@
       class="-mb-15px"
       label-width="68px"
     >
-      <!-- TODO @puhui999:https://admin.java.crmeb.net/store/index,参考,使用分类 + 标题搜索 -->
+      <!-- TODO @puhui999:https://admin.java.crmeb.net/store/index,参考,使用分类 + 标题搜索 fix-->
       <el-form-item label="品牌名称" prop="name">
         <el-input
           v-model="queryParams.name"
@@ -18,15 +18,17 @@
           @keyup.enter="handleQuery"
         />
       </el-form-item>
-      <el-form-item label="状态" prop="status">
-        <el-select v-model="queryParams.status" class="!w-240px" clearable placeholder="请选择状态">
-          <el-option
-            v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
-            :key="dict.value"
-            :label="dict.label"
-            :value="dict.value"
-          />
-        </el-select>
+      <!--  TODO 分类只能选择二级分类目前还没做,还是先以联调通顺为主 -->
+      <el-form-item label="商品分类" prop="categoryId">
+        <el-tree-select
+          v-model="queryParams.categoryId"
+          :data="categoryList"
+          :props="defaultProps"
+          check-strictly
+          class="w-1/1"
+          node-key="id"
+          placeholder="请选择商品分类"
+        />
       </el-form-item>
       <el-form-item label="创建时间" prop="createTime">
         <el-date-picker
@@ -59,7 +61,7 @@
 
   <!-- 列表 -->
   <ContentWrap>
-    <el-tabs v-model="queryParams.tabType" @tab-click="handleClick">
+    <el-tabs v-model="queryParams.tabType" @tab-click="handleTabClick">
       <el-tab-pane
         v-for="item in tabsData"
         :key="item.type"
@@ -68,35 +70,40 @@
       />
     </el-tabs>
     <el-table v-loading="loading" :data="list">
-      <!-- TODO puhui999: ID 编号的展示 -->
-      <!--   TODO 暂时不做折叠数据   -->
-      <!--      <el-table-column type="expand">-->
-      <!--        <template #default="{ row }">-->
-      <!--          <el-form inline label-position="left">-->
-      <!--            <el-form-item label="市场价:">-->
-      <!--              <span>{{ row.marketPrice }}</span>-->
-      <!--            </el-form-item>-->
-      <!--            <el-form-item label="成本价:">-->
-      <!--              <span>{{ row.costPrice }}</span>-->
-      <!--            </el-form-item>-->
-      <!--            <el-form-item label="虚拟销量:">-->
-      <!--              <span>{{ row.virtualSalesCount }}</span>-->
-      <!--            </el-form-item>-->
-      <!--          </el-form>-->
-      <!--        </template>-->
-      <!--      </el-table-column>-->
+      <!--   TODO 折叠数据按需增加暂定以下三个   -->
+      <el-table-column type="expand" width="30">
+        <template #default="{ row }">
+          <el-form class="demo-table-expand" inline label-position="left">
+            <el-form-item label="市场价:">
+              <span>{{ formatToFraction(row.marketPrice) }}</span>
+            </el-form-item>
+            <el-form-item label="成本价:">
+              <span>{{ formatToFraction(row.costPrice) }}</span>
+            </el-form-item>
+            <el-form-item label="虚拟销量:">
+              <span>{{ row.virtualSalesCount }}</span>
+            </el-form-item>
+          </el-form>
+        </template>
+      </el-table-column>
+      <!-- TODO puhui999: ID 编号的展示 fix-->
+      <el-table-column key="id" align="center" label="商品编号" prop="id" />
       <el-table-column label="商品图" min-width="80">
         <template #default="{ row }">
           <el-image
             :src="row.picUrl"
-            style="width: 36px; height: 36px"
+            style="width: 30px; height: 30px"
             @click="imagePreview(row.picUrl)"
           />
         </template>
       </el-table-column>
       <el-table-column :show-overflow-tooltip="true" label="商品名称" min-width="300" prop="name" />
       <!-- TODO 价格 / 100.0 -->
-      <el-table-column align="center" label="商品售价" min-width="90" prop="price" />
+      <el-table-column align="center" label="商品售价" min-width="90" prop="price">
+        <template #default="{ row }">
+          {{ formatToFraction(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" />
@@ -107,24 +114,31 @@
         prop="createTime"
         width="180"
       />
-      <el-table-column fixed="right" label="状态" min-width="80">
+      <el-table-column align="center" label="状态" min-width="80">
         <template #default="{ row }">
-          <!-- TODO @puhui:是不是不用 Number(row.status) 去比较哈,直接 row.status < 0 -->
-          <el-switch
-            v-model="row.status"
-            :active-value="1"
-            :disabled="Number(row.status) < 0"
-            :inactive-value="0"
-            active-text="上架"
-            inactive-text="下架"
-            inline-prompt
-            @change="changeStatus(row)"
-          />
+          <!-- fix: 修复打开回收站时商品误触改变商品状态的事件,因为el-switch只用0和1没有-1所以当商品状态为-1时状态使用el-tag显示  -->
+          <template v-if="row.status >= 0">
+            <el-switch
+              v-model="row.status"
+              :active-value="1"
+              :inactive-value="0"
+              active-text="上架"
+              inactive-text="下架"
+              inline-prompt
+              @change="changeStatus(row)"
+            />
+          </template>
+          <template v-else>
+            <el-tag type="info">回收站</el-tag>
+          </template>
         </template>
       </el-table-column>
-      <el-table-column align="center" fixed="right" label="操作" min-width="150">
+      <el-table-column align="center" fixed="right" label="操作" min-width="200">
         <template #default="{ row }">
           <!-- TODO @puhui999:【详情】,可以后面点做哈 -->
+          <el-button v-hasPermi="['product:spu:update']" link type="primary" @click="openDetail">
+            详情
+          </el-button>
           <template v-if="queryParams.tabType === 4">
             <el-button
               v-hasPermi="['product:spu:delete']"
@@ -138,13 +152,15 @@
               v-hasPermi="['product:spu:update']"
               link
               type="primary"
-              @click="addToTrash(row, ProductSpuStatusEnum.DISABLE.status)"
+              @click="changeStatus(row, ProductSpuStatusEnum.DISABLE.status)"
             >
               恢复到仓库
             </el-button>
           </template>
           <template v-else>
+            <!-- 只有不是上架和回收站的商品可以编辑 -->
             <el-button
+              v-if="queryParams.tabType !== 0"
               v-hasPermi="['product:spu:update']"
               link
               type="primary"
@@ -156,7 +172,7 @@
               v-hasPermi="['product:spu:update']"
               link
               type="primary"
-              @click="addToTrash(row, ProductSpuStatusEnum.RECYCLE.status)"
+              @click="changeStatus(row, ProductSpuStatusEnum.RECYCLE.status)"
             >
               加入回收站
             </el-button>
@@ -172,21 +188,18 @@
       @pagination="getList"
     />
   </ContentWrap>
-  <!-- https://kailong110120130.gitee.io/vue-element-plus-admin-doc/components/image-viewer.html,可以用这个么? -->
-  <!-- 必须在表格外面展示。不然单元格会遮挡图层 -->
-  <el-image-viewer
-    v-if="imgViewVisible"
-    :url-list="imageViewerList"
-    @close="imgViewVisible = false"
-  />
 </template>
-<script lang="ts" name="ProductList" setup>
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+<script lang="ts" name="ProductSpu" setup>
+import { TabsPaneContext } from 'element-plus'
+import { cloneDeep } from 'lodash-es'
+import { createImageViewer } from '@/components/ImageViewer'
 import { dateFormatter } from '@/utils/formatTime'
-// TODO @puhui999:managementApi=》ProductSpuApi
-import * as managementApi from '@/api/mall/product/management/spu'
+import { defaultProps, handleTree } from '@/utils/tree'
 import { ProductSpuStatusEnum } from '@/utils/constants'
-import { TabsPaneContext } from 'element-plus'
+import * as ProductSpuApi from '@/api/mall/product/spu'
+import * as ProductCategoryApi from '@/api/mall/product/category'
+import { formatToFraction } from '@/utils'
+
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 const { currentRoute, push } = useRouter() // 路由跳转
@@ -225,26 +238,21 @@ const tabsData = ref([
 
 /** 获得每个 Tab 的数量 */
 const getTabsCount = async () => {
-  // TODO @puhui999:这里是不是可以不要 try catch 哈
-  try {
-    const res = await managementApi.getTabsCount()
-    for (let objName in res) {
-      tabsData.value[Number(objName)].count = res[objName]
-    }
-  } catch {}
+  // TODO @puhui999:这里是不是可以不要 try catch 哈 fix
+  const res = await ProductSpuApi.getTabsCount()
+  for (let objName in res) {
+    tabsData.value[Number(objName)].count = res[objName]
+  }
 }
-
-const imgViewVisible = ref(false) // 商品图预览
-const imageViewerList = ref<string[]>([]) // 商品图预览列表
 const queryParams = ref({
   pageNo: 1,
   pageSize: 10,
   tabType: 0
-})
-const queryFormRef = ref() // 搜索的表单
+}) // 查询参数
+const queryFormRef = ref() // 搜索的表单Ref
 
-// TODO @puhui999:可以改成 handleTabClick:更准确一点;
-const handleClick = (tab: TabsPaneContext) => {
+// TODO @puhui999:可以改成 handleTabClick:更准确一点;fix
+const handleTabClick = (tab: TabsPaneContext) => {
   queryParams.value.tabType = tab.paneName
   getList()
 }
@@ -253,7 +261,7 @@ const handleClick = (tab: TabsPaneContext) => {
 const getList = async () => {
   loading.value = true
   try {
-    const data = await managementApi.getSpuList(queryParams.value)
+    const data = await ProductSpuApi.getSpuPage(queryParams.value)
     list.value = data.list
     total.value = data.total
   } finally {
@@ -261,7 +269,7 @@ const getList = async () => {
   }
 }
 
-// TODO @puhui999:是不是 changeStatus 和 addToTrash 调用一个统一的方法,去更新状态。这样逻辑会更干净一些。
+// TODO @puhui999:是不是 changeStatus 和 addToTrash 调用一个统一的方法,去更新状态。这样逻辑会更干净一些。fix
 /**
  * 更改 SPU 状态
  *
@@ -269,10 +277,12 @@ const getList = async () => {
  * @param status 更改前的值
  */
 const changeStatus = async (row, status?: number) => {
-  // TODO 测试过程中似乎有点问题,下一版修复
+  // fix: 将加入回收站功能合并到changeStatus并优化
+  const deepCopyValue = cloneDeep(unref(row))
+  if (typeof status !== 'undefined') deepCopyValue.status = status
   try {
     let text = ''
-    switch (row.status) {
+    switch (deepCopyValue.status) {
       case ProductSpuStatusEnum.DISABLE.status:
         text = ProductSpuStatusEnum.DISABLE.name
         break
@@ -283,21 +293,21 @@ const changeStatus = async (row, status?: number) => {
         text = `加入${ProductSpuStatusEnum.RECYCLE.name}`
         break
     }
+    // fix: 修复恢复到仓库的信息提示
     await message.confirm(
-      row.status === -1 ? `确认要将[${row.name}]${text}吗?` : `确认要${text}[${row.name}]吗?`
+      deepCopyValue.status === -1
+        ? `确认要将[${row.name}]${text}吗?`
+        : row.status === -1 // 再判断一次原对象是否等于-1,例: 把回收站中的商品恢复到仓库中,事件触发时原对象status为-1 深拷贝对象status被赋值为0
+        ? `确认要将[${row.name}]恢复到仓库吗?`
+        : `确认要${text}[${row.name}]吗?`
     )
-    await managementApi.updateStatus({ id: row.id, status: row.status })
+    await ProductSpuApi.updateStatus({ id: deepCopyValue.id, status: deepCopyValue.status })
     message.success('更新状态成功')
     // 刷新 tabs 数据
     await getTabsCount()
     // 刷新列表
     await getList()
   } catch {
-    // 取消加入回收站时回显数据
-    if (typeof status !== 'undefined') {
-      row.status = status
-      return
-    }
     // 取消更改状态时回显数据
     row.status =
       row.status === ProductSpuStatusEnum.DISABLE.status
@@ -306,26 +316,13 @@ const changeStatus = async (row, status?: number) => {
   }
 }
 
-/**
- * 加入回收站
- *
- * @param row
- * @param status
- */
-const addToTrash = (row, status) => {
-  // 复制一份原值
-  const num = Number(`${row.status}`)
-  row.status = status
-  changeStatus(row, num)
-}
-
 /** 删除按钮操作 */
 const handleDelete = async (id: number) => {
   try {
     // 删除的二次确认
     await message.delConfirm()
     // 发起删除
-    await managementApi.deleteSpu(id)
+    await ProductSpuApi.deleteSpu(id)
     message.success(t('common.delSuccess'))
     // 刷新tabs数据
     await getTabsCount()
@@ -339,8 +336,10 @@ const handleDelete = async (id: number) => {
  * @param imgUrl
  */
 const imagePreview = (imgUrl: string) => {
-  imageViewerList.value = [imgUrl]
-  imgViewVisible.value = true
+  // fix: 改用https://kailong110120130.gitee.io/vue-element-plus-admin-doc/components/image-viewer.html 预览图片
+  createImageViewer({
+    urlList: [imgUrl]
+  })
 }
 
 /** 搜索按钮操作 */
@@ -362,27 +361,43 @@ const resetQuery = () => {
 const openForm = (id?: number) => {
   // 修改
   if (typeof id === 'number') {
-    push('/product/productManagementAdd?id=' + id)
+    push('/product/productSpuEdit/' + id)
     return
   }
   // 新增
-  push('/product/productManagementAdd')
+  push('/product/productSpuAdd')
 }
-
-// 监听路由变化更新列表 TODO @puhui999:这个是必须加的么?
+/**
+ * 查看商品详情
+ */
+const openDetail = () => {
+  message.alert('查看详情未完善!!!')
+}
+// 监听路由变化更新列表 TODO @puhui999:这个是必须加的么?fix: 因为编辑表单是以路由的方式打开,保存表单后列表不会刷新
 watch(
   () => currentRoute.value,
   () => {
     getList()
-  },
-  {
-    immediate: true
   }
 )
-
+const categoryList = ref() // 分类树
 /** 初始化 **/
-onMounted(() => {
-  getTabsCount()
-  getList()
+onMounted(async () => {
+  await getTabsCount()
+  await getList()
+  // 获得分类树
+  const data = await ProductCategoryApi.getCategoryList({})
+  categoryList.value = handleTree(data, 'id', 'parentId')
 })
 </script>
+<style lang="scss" scoped>
+.demo-table-expand {
+  padding-left: 42px;
+
+  :deep(.el-form-item__label) {
+    width: 82px;
+    font-weight: bold;
+    color: #99a9bf;
+  }
+}
+</style>