diff --git a/src/api/mall/product/spu.ts b/src/api/mall/product/spu.ts index fd55e126..5555ce10 100644 --- a/src/api/mall/product/spu.ts +++ b/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 }) } diff --git a/src/utils/tree.ts b/src/utils/tree.ts index 91d53f8c..513b656d 100644 --- a/src/utils/tree.ts +++ b/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[] => { 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 = ( 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 +} diff --git a/src/views/mall/product/spu/addForm.vue b/src/views/mall/product/spu/addForm.vue index 18f9d0b6..1ae1a4c2 100644 --- a/src/views/mall/product/spu/addForm.vue +++ b/src/views/mall/product/spu/addForm.vue @@ -51,15 +51,15 @@ const basicInfoRef = ref>() // 商品信息Re const descriptionRef = ref>() // 商品详情Ref const otherSettingsRef = ref>() // 其他设置Ref // spu 表单数据 -const formData = ref({ +const formData = ref({ 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) diff --git a/src/views/mall/product/spu/components/BasicInfoForm.vue b/src/views/mall/product/spu/components/BasicInfoForm.vue index d800e332..60fc7c69 100644 --- a/src/views/mall/product/spu/components/BasicInfoForm.vue +++ b/src/views/mall/product/spu/components/BasicInfoForm.vue @@ -7,7 +7,7 @@ - + @@ -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, + type: Object as PropType, default: () => {} }, activeName: propTypes.string.def('') @@ -144,7 +145,7 @@ const skuListRef = ref() // 商品属性列表Ref const generateSkus = (propertyList) => { skuListRef.value.generateTableData(propertyList) } -const formData = reactive({ +const formData = reactive({ 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 () => { // 获得分类树 diff --git a/src/views/mall/product/spu/components/DescriptionForm.vue b/src/views/mall/product/spu/components/DescriptionForm.vue index fbae9a86..23a3e99a 100644 --- a/src/views/mall/product/spu/components/DescriptionForm.vue +++ b/src/views/mall/product/spu/components/DescriptionForm.vue @@ -7,7 +7,7 @@ diff --git a/src/views/mall/product/spu/index.vue b/src/views/mall/product/spu/index.vue index 539171b0..34719775 100644 --- a/src/views/mall/product/spu/index.vue +++ b/src/views/mall/product/spu/index.vue @@ -8,18 +8,16 @@ class="-mb-15px" label-width="68px" > - - + - - + @@ -80,31 +79,60 @@ /> - +虚拟销量:999 --> @@ -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()