Browse Source

fix:解决商品上一版遗留的各种小bug关键部分已添加fix注释。完成的TODO也已添加fix标记

puhui999 1 year ago
parent
commit
0c42b76a52

+ 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' })
+}

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

@@ -34,6 +34,7 @@ export interface SpuType {
   sliderPicUrls?: string[] // 商品轮播图
   introduction?: string // 商品简介
   deliveryTemplateId?: number | null // 运费模版
+  brandId?: number | null // 商品品牌编号
   specType?: boolean // 商品规格
   subCommissionType?: boolean // 分销类型
   skus: SkuType[] // sku数组

+ 21 - 0
src/utils/index.ts

@@ -173,3 +173,24 @@ export const copyValueToTarget = (target, source) => {
   // 更新目标对象值
   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)
+}

+ 46 - 36
src/views/mall/product/spu/addForm.vue

@@ -38,6 +38,7 @@ import { BasicInfoForm, DescriptionForm, OtherSettingsForm } from './components'
 // 业务api
 import * as ProductSpuApi from '@/api/mall/product/spu'
 import * as PropertyApi from '@/api/mall/product/property'
+import { convertToInteger, formatToFraction } from '@/utils'
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
@@ -60,6 +61,7 @@ const formData = ref<ProductSpuApi.SpuType>({
   sliderPicUrls: [], // 商品轮播图
   introduction: '', // 商品简介
   deliveryTemplateId: 1, // 运费模版
+  brandId: null, // 商品品牌
   specType: false, // 商品规格
   subCommissionType: false, // 分销类型
   skus: [
@@ -94,14 +96,34 @@ const getDetail = async () => {
     formLoading.value = true
     try {
       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)
+      // 只有是多规格才处理
+      if (res.specType) {
+        // TODO @puhui999:可以直接拿 propertyName 拼接处规格 id + 属性,可以看下商品 uniapp 详情的做法
+        // fix: 考虑到 sku 数量和通过属性算出来的sku不一致的情况
+        const propertyIds = []
+        res.skus.forEach((sku) =>
+          sku.properties
+            ?.map((property) => property.propertyId)
+            .forEach((propertyId) => {
+              if (propertyIds.indexOf(propertyId) === -1) {
+                propertyIds.push(propertyId)
+              }
+            })
+        )
+        const properties = await PropertyApi.getPropertyListAndValue({ propertyIds })
+        await nextTick()
+        // 回显商品属性
+        basicInfoRef.value.addAttribute(properties)
+      }
     } finally {
       formLoading.value = false
     }
@@ -119,10 +141,26 @@ const submitForm = async () => {
     await unref(descriptionRef)?.validate()
     await unref(otherSettingsRef)?.validate()
     const deepCopyFormData = cloneDeep(unref(formData.value)) // 深拷贝一份 fix:这样最终 server 端不满足,不需要恢复,
-    // 处理掉一些无关数据
+    // TODO 兜底处理 sku 空数据详见 SkuList TODO
+    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 = []
@@ -148,34 +186,6 @@ const submitForm = async () => {
   }
 }
 
-/**
- * 重置表单
- * fix:先注释保留,如果后期没有使用到则移除
- */
-// 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 = () => {
   delView(unref(currentRoute))

+ 26 - 6
src/views/mall/product/spu/components/BasicInfoForm.vue

@@ -59,13 +59,23 @@
       </el-col>
       <el-col :span="12">
         <el-form-item label="运费模板" prop="deliveryTemplateId">
-          <el-select v-model="formData.deliveryTemplateId" class="w-1/1" placeholder="请选择">
+          <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">
@@ -108,14 +118,15 @@
 </template>
 <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/spu'
 import { UploadImg, UploadImgs } from '@/components/UploadFile'
 import { ProductAttributes, ProductAttributesAddForm, SkuList } from './index'
 import * as ProductCategoryApi from '@/api/mall/product/category'
-import { propTypes } from '@/utils/propTypes'
-import { copyValueToTarget } from '@/utils'
+import { getSimpleBrandList } from '@/api/mall/product/brand'
 
 const message = useMessage() // 消息弹窗
 
@@ -135,15 +146,20 @@ const propertyList = ref([]) // 商品属性列表
 const addAttribute = (property: any) => {
   Array.isArray(property) ? (propertyList.value = property) : propertyList.value.push(property)
 }
+/** 调用 SkuList generateTableData 方法*/
+// const generateSkus(propertyList){
+//   skuList.value.generateTableData()
+// }
 const formData = reactive<SpuType>({
   name: '', // 商品名称
-  categoryId: undefined, // 商品分类
+  categoryId: null, // 商品分类
   keyword: '', // 关键字
   unit: '', // 单位
   picUrl: '', // 商品封面图
   sliderPicUrls: [], // 商品轮播图
   introduction: '', // 商品简介
   deliveryTemplateId: 1, // 运费模版
+  brandId: null, // 商品品牌
   specType: false, // 商品规格
   subCommissionType: false, // 分销类型
   skus: []
@@ -157,6 +173,7 @@ const rules = reactive({
   picUrl: [required],
   sliderPicUrls: [required],
   // deliveryTemplateId: [required],
+  brandId: [required],
   specType: [required],
   subCommissionType: [required]
 })
@@ -232,10 +249,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>

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

@@ -34,17 +34,29 @@
     <!-- 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%" />
+        <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">
       <template #default="{ row }">
-        <el-input-number v-model="row.marketPrice" :min="0" class="w-100%" />
+        <el-input-number
+          v-model="row.marketPrice"
+          :min="0"
+          :precision="2"
+          :step="0.1"
+          class="w-100%"
+        />
       </template>
     </el-table-column>
     <el-table-column align="center" label="成本价(元)" min-width="168">
       <template #default="{ row }">
-        <el-input-number v-model="row.costPrice" :min="0" class="w-100%" />
+        <el-input-number
+          v-model="row.costPrice"
+          :min="0"
+          :precision="2"
+          :step="0.1"
+          class="w-100%"
+        />
       </template>
     </el-table-column>
     <el-table-column align="center" label="库存" min-width="168">
@@ -54,42 +66,54 @@
     </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%" />
+        <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%" />
+        <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">
         <template #default="{ row }">
-          <el-input-number v-model="row.subCommissionFirstPrice" :min="0" class="w-100%" />
+          <el-input-number
+            v-model="row.subCommissionFirstPrice"
+            :min="0"
+            :precision="2"
+            :step="0.1"
+            class="w-100%"
+          />
         </template>
       </el-table-column>
       <el-table-column align="center" label="二级返佣(元)" min-width="168">
         <template #default="{ row }">
-          <el-input-number v-model="row.subCommissionSecondPrice" :min="0" class="w-100%" />
+          <el-input-number
+            v-model="row.subCommissionSecondPrice"
+            :min="0"
+            :precision="2"
+            :step="0.1"
+            class="w-100%"
+          />
         </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 type { Property, SkuType, SpuType } from '@/api/mall/product/spu'
-import { propTypes } from '@/utils/propTypes'
 import { copyValueToTarget } from '@/utils'
+import { propTypes } from '@/utils/propTypes'
+import { UploadImg } from '@/components/UploadFile'
+import type { Property, SkuType, SpuType } from '@/api/mall/product/spu'
 
 const props = defineProps({
   propFormData: {
@@ -124,7 +148,14 @@ const batchAdd = () => {
     copyValueToTarget(item, skuList.value[0])
   })
 }
-
+/** 删除 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 }[]>([]) // 多属性表头
 
 /**
@@ -142,11 +173,11 @@ watch(
   }
 )
 
-// TODO @芋艿:看看 chatgpt 可以进一步下面几个方法的实现不 fix
+// TODO @芋艿:看看 chatgpt 可以进一步下面几个方法的实现不 fix: 添加相关处理逻辑解决编辑表单时或查看详情时数据回显问题
 /** 生成表数据 */
-const generateTableData = (data: any[]) => {
+const generateTableData = (propertyList: any[]) => {
   // 构建数据结构 fix: 使用map替换多重for循环
-  const propertiesItemList = data.map((item) =>
+  const propertiesItemList = propertyList.map((item) =>
     item.values.map((v) => ({
       propertyId: item.id,
       propertyName: item.name,
@@ -155,19 +186,14 @@ const generateTableData = (data: any[]) => {
     }))
   )
   const buildList = build(propertiesItemList)
-  // 如果构建后的组合数跟sku数量一样的话则不用处理,添加新属性没有属性值也不做处理
-  // fix: 解决编辑表单时或查看详情时数据回显问题
-  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: Array.isArray(item) ? item : [item],
+      properties: Array.isArray(item) ? item : [item], // 如果只有一个属性的话返回的是一个property对象
       price: 0,
       marketPrice: 0,
       costPrice: 0,
@@ -179,8 +205,36 @@ const generateTableData = (data: any[]) => {
       subCommissionFirstPrice: 0,
       subCommissionSecondPrice: 0
     }
+    const index = formData.value!.skus.findIndex(
+      (sku) => JSON.stringify(sku.properties) === JSON.stringify(row.properties)
+    )
+    // 如果存在属性相同的 sku 则不做处理
+    if (index !== -1) {
+      continue
+    }
+    /**
+     * TODO 嗯。。有一个问题回显数据时已删除的 sku 会被重新添加暂时没想到好办法,保存时先手动重新删除一下因为是一条空数据很好辨别 不手动删也没是提交表单时会检测删除空sku来兜底
+     *
+     */
     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 = (propertyValuesList: Property[][]) => {
@@ -237,6 +291,10 @@ watch(
       // name加属性项index区分属性值
       tableHeaders.value.push({ prop: `name${index}`, label: item.name })
     })
+    // 如果回显的 sku 属性和添加的属性一致则不处理
+    if (validateData(propertyList)) return
+    // 添加新属性没有属性值也不做处理
+    if (propertyList.some((item) => item.values.length === 0)) return
     generateTableData(propertyList)
   },
   {

+ 20 - 5
src/views/mall/product/spu/index.vue

@@ -75,10 +75,10 @@
         <template #default="{ row }">
           <el-form class="demo-table-expand" inline label-position="left">
             <el-form-item label="市场价:">
-              <span>{{ row.marketPrice }}</span>
+              <span>{{ formatToFraction(row.marketPrice) }}</span>
             </el-form-item>
             <el-form-item label="成本价:">
-              <span>{{ row.costPrice }}</span>
+              <span>{{ formatToFraction(row.costPrice) }}</span>
             </el-form-item>
             <el-form-item label="虚拟销量:">
               <span>{{ row.virtualSalesCount }}</span>
@@ -99,7 +99,11 @@
       </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" />
@@ -129,9 +133,12 @@
           </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']"
@@ -151,7 +158,9 @@
             </el-button>
           </template>
           <template v-else>
+            <!-- 只有不是上架和回收站的商品可以编辑 -->
             <el-button
+              v-if="queryParams.tabType !== 0"
               v-hasPermi="['product:spu:update']"
               link
               type="primary"
@@ -189,6 +198,7 @@ import { defaultProps, handleTree } from '@/utils/tree'
 import { ProductSpuStatusEnum } from '@/utils/constants'
 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() // 国际化
@@ -357,7 +367,12 @@ const openForm = (id?: number) => {
   // 新增
   push('/product/productSpuAdd')
 }
-
+/**
+ * 查看商品详情
+ */
+const openDetail = () => {
+  message.alert('查看详情未完善!!!')
+}
 // 监听路由变化更新列表 TODO @puhui999:这个是必须加的么?fix: 因为编辑表单是以路由的方式打开,保存表单后列表不会刷新
 watch(
   () => currentRoute.value,