Browse Source

fix: 修改 review 提到的问题,完善分类选择层级校验、完整层级展示

(cherry picked from commit e555977757f76516638ce2c8fb90eb6a186bc25b)
puhui999 2 years ago
parent
commit
cf7f4ff89e

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

@@ -7,8 +7,7 @@ export interface Property {
   valueName?: string // 属性值名称
 }
 
-// TODO puhui999:是不是直接叫 Sku 更简洁一点哈。type 待后面,总感觉有个类型?
-export interface SkuType {
+export interface Sku {
   id?: number // 商品 SKU 编号
   spuId?: number // SPU 编号
   properties?: Property[] // 属性数组
@@ -25,8 +24,7 @@ export interface SkuType {
   salesCount?: number // 商品销量
 }
 
-// TODO puhui999:是不是直接叫 Spu 更简洁一点哈。type 待后面,总感觉有个类型?
-export interface SpuType {
+export interface Spu {
   id?: number
   name?: string // 商品名称
   categoryId?: number | null // 商品分类
@@ -39,9 +37,9 @@ export interface SpuType {
   brandId?: number | null // 商品品牌编号
   specType?: boolean // 商品规格
   subCommissionType?: boolean // 分销类型
-  skus: SkuType[] // sku数组
+  skus: Sku[] // sku数组
   description?: string // 商品详情
-  sort?: string // 商品排序
+  sort?: number // 商品排序
   giveIntegral?: number // 赠送积分
   virtualSalesCount?: number // 虚拟销量
   recommendHot?: boolean // 是否热卖
@@ -62,12 +60,12 @@ export const getTabsCount = () => {
 }
 
 // 创建商品 Spu
-export const createSpu = (data: SpuType) => {
+export const createSpu = (data: Spu) => {
   return request.post({ url: '/product/spu/create', data })
 }
 
 // 更新商品 Spu
-export const updateSpu = (data: SpuType) => {
+export const updateSpu = (data: Spu) => {
   return request.put({ url: '/product/spu/update', data })
 }
 

+ 81 - 0
src/utils/tree.ts

@@ -3,6 +3,7 @@ interface TreeHelperConfig {
   children: string
   pid: string
 }
+
 const DEFAULT_CONFIG: TreeHelperConfig = {
   id: 'id',
   children: 'children',
@@ -133,6 +134,7 @@ export const filter = <T = any>(
 ): T[] => {
   config = getConfig(config)
   const children = config.children as string
+
   function listFilter(list: T[]) {
     return list
       .map((node: any) => ({ ...node }))
@@ -141,6 +143,7 @@ export const filter = <T = any>(
         return func(node) || (node[children] && node[children].length)
       })
   }
+
   return listFilter(tree)
 }
 
@@ -264,6 +267,7 @@ export const handleTree = (data: any[], id?: string, parentId?: string, children
       }
     }
   }
+
   return tree
 }
 /**
@@ -300,3 +304,80 @@ export const handleTree2 = (data, id, parentId, children, rootId) => {
   })
   return treeData !== '' ? treeData : data
 }
+/**
+ *
+ * @param tree 要操作的树结构数据
+ * @param nodeId 需要判断在什么层级的数据
+ * @param level 检查的级别, 默认检查到二级
+ */
+export const checkSelectedNode = (tree: any[], nodeId, level = 2) => {
+  if (typeof tree === 'undefined' || !Array.isArray(tree) || tree.length === 0) {
+    console.warn('tree must be an array')
+    return false
+  }
+  // 校验是否是一级节点
+  if (tree.some((item) => item.id === nodeId)) {
+    return false
+  }
+  // 递归计数
+  let count = 1
+
+  // 深层次校验
+  function performAThoroughValidation(arr) {
+    count += 1
+    for (const item of arr) {
+      if (item.id === nodeId) {
+        return true
+      } else if (typeof item.children !== 'undefined' && item.children.length !== 0) {
+        performAThoroughValidation(item.children)
+      }
+    }
+    return false
+  }
+
+  for (const item of tree) {
+    count = 1
+    if (performAThoroughValidation(item.children)) {
+      // 找到后对比是否是期望的层级
+      if (count >= level) return true
+    }
+  }
+  return false
+}
+/**
+ * 获取节点的完整结构
+ * @param tree 树数据
+ * @param nodeId 节点 id
+ */
+export const treeToString = (tree: any[], nodeId) => {
+  if (typeof tree === 'undefined' || !Array.isArray(tree) || tree.length === 0) {
+    console.warn('tree must be an array')
+    return ''
+  }
+  // 校验是否是一级节点
+  const node = tree.find((item) => item.id === nodeId)
+  if (typeof node !== 'undefined') {
+    return node.name
+  }
+  let str = ''
+
+  function performAThoroughValidation(arr) {
+    for (const item of arr) {
+      if (item.id === nodeId) {
+        str += `/${item.name}`
+        return true
+      } else if (typeof item.children !== 'undefined' && item.children.length !== 0) {
+        performAThoroughValidation(item.children)
+      }
+    }
+    return false
+  }
+
+  for (const item of tree) {
+    str = `${item.name}`
+    if (performAThoroughValidation(item.children)) {
+      break
+    }
+  }
+  return str
+}

+ 8 - 7
src/views/mall/product/spu/addForm.vue

@@ -51,15 +51,15 @@ const basicInfoRef = ref<ComponentRef<typeof BasicInfoForm>>() // 商品信息Re
 const descriptionRef = ref<ComponentRef<typeof DescriptionForm>>() // 商品详情Ref
 const otherSettingsRef = ref<ComponentRef<typeof OtherSettingsForm>>() // 其他设置Ref
 // spu 表单数据
-const formData = ref<ProductSpuApi.SpuType>({
+const formData = ref<ProductSpuApi.Spu>({
   name: '', // 商品名称
   categoryId: null, // 商品分类
   keyword: '', // 关键字
   unit: null, // 单位
   picUrl: '', // 商品封面图
-  sliderPicUrls: [], // 商品轮播图
+  sliderPicUrls: [''], // 商品轮播图
   introduction: '', // 商品简介
-  deliveryTemplateId: 1, // 运费模版
+  deliveryTemplateId: null, // 运费模版
   brandId: null, // 商品品牌
   specType: false, // 商品规格
   subCommissionType: false, // 分销类型
@@ -94,7 +94,7 @@ const getDetail = async () => {
   if (id) {
     formLoading.value = true
     try {
-      const res = (await ProductSpuApi.getSpu(id)) as ProductSpuApi.SpuType
+      const res = (await ProductSpuApi.getSpu(id)) as ProductSpuApi.Spu
       res.skus.forEach((item) => {
         // 回显价格分转元
         item.price = formatToFraction(item.price)
@@ -120,8 +120,9 @@ const submitForm = async () => {
     await unref(basicInfoRef)?.validate()
     await unref(descriptionRef)?.validate()
     await unref(otherSettingsRef)?.validate()
-    const deepCopyFormData = cloneDeep(unref(formData.value)) // 深拷贝一份 fix:这样最终 server 端不满足,不需要恢复,
-    // TODO 兜底处理 sku 空数据
+    // 深拷贝一份, 这样最终 server 端不满足,不需要恢复,
+    const deepCopyFormData = cloneDeep(unref(formData.value))
+    // 兜底处理 sku 空数据
     formData.value.skus.forEach((sku) => {
       // 因为是空数据这里判断一下商品条码是否为空就行
       if (sku.barCode === '') {
@@ -150,7 +151,7 @@ const submitForm = async () => {
     })
     deepCopyFormData.sliderPicUrls = newSliderPicUrls
     // 校验都通过后提交表单
-    const data = deepCopyFormData as ProductSpuApi.SpuType
+    const data = deepCopyFormData as ProductSpuApi.Spu
     const id = params.spuId as number
     if (!id) {
       await ProductSpuApi.createSpu(data)

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

@@ -7,7 +7,7 @@
         </el-form-item>
       </el-col>
       <el-col :span="12">
-        <!-- TODO @puhui999:只能选根节点 -->
+        <!-- TODO @puhui999:只能选根节点 fix: 已完善-->
         <el-form-item label="商品分类" prop="categoryId">
           <el-tree-select
             v-model="formData.categoryId"
@@ -17,6 +17,7 @@
             class="w-1/1"
             node-key="id"
             placeholder="请选择商品分类"
+            @change="nodeClick"
           />
         </el-form-item>
       </el-col>
@@ -119,9 +120,9 @@
 import { PropType } from 'vue'
 import { copyValueToTarget } from '@/utils'
 import { propTypes } from '@/utils/propTypes'
-import { defaultProps, handleTree } from '@/utils/tree'
+import { checkSelectedNode, defaultProps, handleTree } from '@/utils/tree'
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
-import type { SpuType } from '@/api/mall/product/spu'
+import type { Spu } 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'
@@ -131,7 +132,7 @@ const message = useMessage() // 消息弹窗
 
 const props = defineProps({
   propFormData: {
-    type: Object as PropType<SpuType>,
+    type: Object as PropType<Spu>,
     default: () => {}
   },
   activeName: propTypes.string.def('')
@@ -144,7 +145,7 @@ const skuListRef = ref() // 商品属性列表Ref
 const generateSkus = (propertyList) => {
   skuListRef.value.generateTableData(propertyList)
 }
-const formData = reactive<SpuType>({
+const formData = reactive<Spu>({
   name: '', // 商品名称
   categoryId: null, // 商品分类
   keyword: '', // 关键字
@@ -185,26 +186,24 @@ watch(
     formData.sliderPicUrls = data['sliderPicUrls'].map((item) => ({
       url: item
     }))
-    // TODO @puhui999:if return,减少嵌套层级
     // 只有是多规格才处理
-    if (formData.specType) {
-      //  直接拿返回的 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 })
-          }
-        })
+    if (!formData.specType) return
+    //  直接拿返回的 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
-    }
+    })
+    propertyList.value = properties
   },
   {
     immediate: true
@@ -216,6 +215,11 @@ watch(
  */
 const emit = defineEmits(['update:activeName'])
 const validate = async () => {
+  // 校验 sku
+  if (!skuListRef.value.validateSku()) {
+    message.warning('商品相关价格不能低于0.01元!!')
+    throw new Error('商品相关价格不能低于0.01元!!')
+  }
   // 校验表单
   if (!productSpuBasicInfoRef) return
   return await unref(productSpuBasicInfoRef).validate((valid) => {
@@ -263,6 +267,15 @@ const onChangeSpec = () => {
 }
 
 const categoryList = ref([]) // 分类树
+/**
+ * 选择分类时触发校验
+ */
+const nodeClick = () => {
+  if (!checkSelectedNode(categoryList.value, formData.categoryId)) {
+    formData.categoryId = null
+    message.warning('必须选择二级节点!!')
+  }
+}
 const brandList = ref([]) // 精简商品品牌列表
 onMounted(async () => {
   // 获得分类树

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

@@ -7,7 +7,7 @@
   </el-form>
 </template>
 <script lang="ts" name="DescriptionForm" setup>
-import type { SpuType } from '@/api/mall/product/spu'
+import type { Spu } from '@/api/mall/product/spu'
 import { Editor } from '@/components/Editor'
 import { PropType } from 'vue'
 import { propTypes } from '@/utils/propTypes'
@@ -16,13 +16,13 @@ import { copyValueToTarget } from '@/utils'
 const message = useMessage() // 消息弹窗
 const props = defineProps({
   propFormData: {
-    type: Object as PropType<SpuType>,
+    type: Object as PropType<Spu>,
     default: () => {}
   },
   activeName: propTypes.string.def('')
 })
 const descriptionFormRef = ref() // 表单Ref
-const formData = ref<SpuType>({
+const formData = ref<Spu>({
   description: '' // 商品详情
 })
 // 表单规则

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

@@ -52,7 +52,7 @@
   </el-form>
 </template>
 <script lang="ts" name="OtherSettingsForm" setup>
-import type { SpuType } from '@/api/mall/product/spu'
+import type { Spu } from '@/api/mall/product/spu'
 import { PropType } from 'vue'
 import { propTypes } from '@/utils/propTypes'
 import { copyValueToTarget } from '@/utils'
@@ -61,7 +61,7 @@ const message = useMessage() // 消息弹窗
 
 const props = defineProps({
   propFormData: {
-    type: Object as PropType<SpuType>,
+    type: Object as PropType<Spu>,
     default: () => {}
   },
   activeName: propTypes.string.def('')
@@ -69,7 +69,7 @@ const props = defineProps({
 
 const otherSettingsFormRef = ref() // 表单Ref
 // 表单数据
-const formData = ref<SpuType>({
+const formData = ref<Spu>({
   sort: 1, // 商品排序
   giveIntegral: 1, // 赠送积分
   virtualSalesCount: 1, // 虚拟销量

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

@@ -90,8 +90,7 @@ const submitForm = async () => {
 /** 重置表单 */
 const resetForm = () => {
   formData.value = {
-    name: '',
-    remark: ''
+    name: ''
   }
   formRef.value?.resetFields()
 }

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

@@ -1,6 +1,6 @@
 <template>
   <el-table
-    :data="isBatch ? skuList : formData.skus"
+    :data="isBatch ? skuList : formData!.skus"
     border
     class="tabNumWidth"
     max-height="500"
@@ -11,7 +11,7 @@
         <UploadImg v-model="row.picUrl" height="80px" width="100%" />
       </template>
     </el-table-column>
-    <template v-if="formData.specType && !isBatch">
+    <template v-if="formData!.specType && !isBatch">
       <!--  根据商品属性动态添加 -->
       <el-table-column
         v-for="(item, index) in tableHeaders"
@@ -21,8 +21,10 @@
         min-width="120"
       >
         <template #default="{ row }">
-          <!-- TODO puhui999:展示成蓝色,有点区分度哈 -->
-          {{ row.properties[index]?.valueName }}
+          <!-- TODO puhui999:展示成蓝色,有点区分度哈 fix: 字体加粗,颜色使用 #99a9bf 蓝色有点不好看哈哈-->
+          <span style="font-weight: bold; color: #99a9bf">
+            {{ row.properties[index]?.valueName }}
+          </span>
         </template>
       </el-table-column>
     </template>
@@ -73,7 +75,7 @@
         <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">
+    <template v-if="formData!.subCommissionType">
       <el-table-column align="center" label="一级返佣(元)" min-width="168">
         <template #default="{ row }">
           <el-input-number
@@ -97,7 +99,7 @@
         </template>
       </el-table-column>
     </template>
-    <el-table-column v-if="formData.specType" align="center" fixed="right" label="操作" width="80">
+    <el-table-column v-if="formData?.specType" align="center" fixed="right" label="操作" width="80">
       <template #default="{ row }">
         <el-button v-if="isBatch" link size="small" type="primary" @click="batchAdd">
           批量添加
@@ -108,15 +110,15 @@
   </el-table>
 </template>
 <script lang="ts" name="SkuList" setup>
-import { PropType } from 'vue'
+import { PropType, Ref } from 'vue'
 import { copyValueToTarget } from '@/utils'
 import { propTypes } from '@/utils/propTypes'
 import { UploadImg } from '@/components/UploadFile'
-import type { Property, SkuType, SpuType } from '@/api/mall/product/spu'
+import type { Property, Sku, Spu } from '@/api/mall/product/spu'
 
 const props = defineProps({
   propFormData: {
-    type: Object as PropType<SpuType>,
+    type: Object as PropType<Spu>,
     default: () => {}
   },
   propertyList: {
@@ -125,8 +127,8 @@ const props = defineProps({
   },
   isBatch: propTypes.bool.def(false) // 是否作为批量操作组件
 })
-const formData = ref<SpuType>() // 表单数据
-const skuList = ref<SkuType[]>([
+const formData: Ref<Spu | undefined> = ref<Spu>() // 表单数据
+const skuList = ref<Sku[]>([
   {
     price: 0, // 商品价格
     marketPrice: 0, // 市场价
@@ -140,24 +142,37 @@ const skuList = ref<SkuType[]>([
     subCommissionSecondPrice: 0 // 二级分销的佣金
   }
 ]) // 批量添加时的临时数据
-// TODO @puhui999:保存时,每个商品规格的表单要校验下。例如说,销售金额最低是 0.01 这种。
 
 /** 批量添加 */
 const batchAdd = () => {
-  formData.value.skus.forEach((item) => {
+  formData.value!.skus.forEach((item) => {
     copyValueToTarget(item, skuList.value[0])
   })
 }
 
 /** 删除 sku */
 const deleteSku = (row) => {
-  const index = formData.value.skus.findIndex(
+  const index = formData.value!.skus.findIndex(
     // 直接把列表转成字符串比较
     (sku) => JSON.stringify(sku.properties) === JSON.stringify(row.properties)
   )
-  formData.value.skus.splice(index, 1)
+  formData.value!.skus.splice(index, 1)
 }
 const tableHeaders = ref<{ prop: string; label: string }[]>([]) // 多属性表头
+/**
+ * 保存时,每个商品规格的表单要校验下。例如说,销售金额最低是 0.01 这种。
+ */
+const validateSku = (): boolean => {
+  const checks = ['price', 'marketPrice', 'costPrice']
+  let validate = true // 默认通过
+  for (const sku of formData.value!.skus) {
+    if (checks.some((check) => sku[check] < 0.01)) {
+      validate = false // 只要有一个不通过则直接不通过
+      break
+    }
+  }
+  return validate
+}
 
 /**
  * 将传进来的值赋值给 skuList
@@ -185,14 +200,13 @@ const generateTableData = (propertyList: any[]) => {
       valueName: v.name
     }))
   )
-  // TODO @puhui:是不是 buildSkuList,这样容易理解一点哈。item 改成 sku
-  const buildList = build(propertyValues)
+  const buildSkuList = build(propertyValues)
   // 如果回显的 sku 属性和添加的属性不一致则重置 skus 列表
   if (!validateData(propertyList)) {
     // 如果不一致则重置表数据,默认添加新的属性重新生成 sku 列表
     formData.value!.skus = []
   }
-  for (const item of buildList) {
+  for (const item of buildSkuList) {
     const row = {
       properties: Array.isArray(item) ? item : [item], // 如果只有一个属性的话返回的是一个 property 对象
       price: 0,
@@ -213,7 +227,7 @@ const generateTableData = (propertyList: any[]) => {
     if (index !== -1) {
       continue
     }
-    formData.value.skus.push(row)
+    formData.value!.skus.push(row)
   }
 }
 
@@ -222,7 +236,7 @@ const generateTableData = (propertyList: any[]) => {
  */
 const validateData = (propertyList: any[]) => {
   const skuPropertyIds = []
-  formData.value.skus.forEach((sku) =>
+  formData.value!.skus.forEach((sku) =>
     sku.properties
       ?.map((property) => property.propertyId)
       .forEach((propertyId) => {
@@ -263,7 +277,7 @@ watch(
   () => props.propertyList,
   (propertyList) => {
     // 如果不是多规格则结束
-    if (!formData.value.specType) {
+    if (!formData.value!.specType) {
       return
     }
     // 如果当前组件作为批量添加数据使用,则重置表数据
@@ -313,5 +327,5 @@ watch(
   }
 )
 // 暴露出生成 sku 方法,给添加属性成功时调用
-defineExpose({ generateTableData })
+defineExpose({ generateTableData, validateSku })
 </script>

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

@@ -8,18 +8,16 @@
       class="-mb-15px"
       label-width="68px"
     >
-      <!-- TODO @puhui999:品牌应该是数据下拉哈 -->
-      <el-form-item label="品牌名称" prop="name">
+      <el-form-item label="商品名称" prop="name">
         <el-input
           v-model="queryParams.name"
           class="!w-240px"
           clearable
-          placeholder="请输入品名称"
+          placeholder="请输入品名称"
           @keyup.enter="handleQuery"
         />
       </el-form-item>
-      <!--  TODO 分类只能选择二级分类目前还没做,还是先以联调通顺为主 -->
-      <!-- TODO puhui999:我们要不改成支持选择一级。如果选择一级,后端要递归查询下子分类,然后去 in? -->
+      <!--  TODO 分类只能选择二级分类目前还没做,还是先以联调通顺为主 fixL: 已完善 -->
       <el-form-item label="商品分类" prop="categoryId">
         <el-tree-select
           v-model="queryParams.categoryId"
@@ -29,6 +27,7 @@
           class="w-1/1"
           node-key="id"
           placeholder="请选择商品分类"
+          @change="nodeClick"
         />
       </el-form-item>
       <el-form-item label="创建时间" prop="createTime">
@@ -80,31 +79,60 @@
       />
     </el-tabs>
     <el-table v-loading="loading" :data="list">
-      <!-- TODO puhui:这几个属性哈,一行三个
+      <!-- TODO puhui:这几个属性哈,一行三个 fix
       商品分类:服装鞋包/箱包
 商品市场价格:100.00
 成本价:0.00
 收藏:5
-虚拟销量:999   -->
+虚拟销量:999  -->
       <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 class="demo-table-expand" label-position="left">
+            <el-row>
+              <el-col :span="24">
+                <el-row>
+                  <el-col :span="8">
+                    <el-form-item label="商品分类:">
+                      <span>{{ categoryString(row.categoryId) }}</span>
+                    </el-form-item>
+                  </el-col>
+                  <el-col :span="8">
+                    <el-form-item label="市场价:">
+                      <span>{{ formatToFraction(row.marketPrice) }}</span>
+                    </el-form-item>
+                  </el-col>
+                  <el-col :span="8">
+                    <el-form-item label="成本价:">
+                      <span>{{ formatToFraction(row.costPrice) }}</span>
+                    </el-form-item>
+                  </el-col>
+                </el-row>
+              </el-col>
+            </el-row>
+            <el-row>
+              <el-col :span="24">
+                <el-row>
+                  <el-col :span="8">
+                    <el-form-item label="收藏:">
+                      <!-- TODO 没有这个属性,暂时写死 5 个 -->
+                      <span>5</span>
+                    </el-form-item>
+                  </el-col>
+                  <el-col :span="8">
+                    <el-form-item label="虚拟销量:">
+                      <span>{{ row.virtualSalesCount }}</span>
+                    </el-form-item>
+                  </el-col>
+                </el-row>
+              </el-col>
+            </el-row>
           </el-form>
         </template>
       </el-table-column>
       <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" @click="imagePreview(row.picUrl)" class="w-30px h-30px" />
+          <el-image :src="row.picUrl" class="w-30px h-30px" @click="imagePreview(row.picUrl)" />
         </template>
       </el-table-column>
       <el-table-column :show-overflow-tooltip="true" label="商品名称" min-width="300" prop="name" />
@@ -202,7 +230,7 @@ import { TabsPaneContext } from 'element-plus'
 import { cloneDeep } from 'lodash-es'
 import { createImageViewer } from '@/components/ImageViewer'
 import { dateFormatter } from '@/utils/formatTime'
-import { defaultProps, handleTree } from '@/utils/tree'
+import { checkSelectedNode, defaultProps, handleTree, treeToString } from '@/utils/tree'
 import { ProductSpuStatusEnum } from '@/utils/constants'
 import { formatToFraction } from '@/utils'
 import download from '@/utils/download'
@@ -391,7 +419,7 @@ const handleExport = async () => {
   }
 }
 
-// 监听路由变化更新列表 TODO @puhui999:这个是必须加的么?fix: 因为编辑表单是以路由的方式打开,保存表单后列表不刷新
+// 监听路由变化更新列表,解决商品保存后列表不刷新的问题。
 watch(
   () => currentRoute.value,
   () => {
@@ -400,6 +428,22 @@ watch(
 )
 
 const categoryList = ref() // 分类树
+/**
+ * 获取分类的节点的完整结构
+ * @param categoryId 分类id
+ */
+const categoryString = (categoryId) => {
+  return treeToString(categoryList.value, categoryId)
+}
+/**
+ * 校验所选是否为二级节点
+ */
+const nodeClick = () => {
+  if (!checkSelectedNode(categoryList.value, queryParams.value.categoryId)) {
+    queryParams.value.categoryId = null
+    message.warning('必须选择二级节点!!')
+  }
+}
 /** 初始化 **/
 onMounted(async () => {
   await getTabsCount()