parent
							
								
									e4d6b4301b
								
							
						
					
					
						commit
						cf7f4ff89e
					
				| 
						 | 
				
			
			@ -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 })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 () => {
 | 
			
		||||
  // 获得分类树
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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: '' // 商品详情
 | 
			
		||||
})
 | 
			
		||||
// 表单规则
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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, // 虚拟销量
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -90,8 +90,7 @@ const submitForm = async () => {
 | 
			
		|||
/** 重置表单 */
 | 
			
		||||
const resetForm = () => {
 | 
			
		||||
  formData.value = {
 | 
			
		||||
    name: '',
 | 
			
		||||
    remark: ''
 | 
			
		||||
    name: ''
 | 
			
		||||
  }
 | 
			
		||||
  formRef.value?.resetFields()
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue