fix: 修改 review 提到的问题,完善分类选择层级校验、完整层级展示
(cherry picked from commit e555977757
)
This commit is contained in:
parent
e4d6b4301b
commit
cf7f4ff89e
@ -7,8 +7,7 @@ export interface Property {
|
|||||||
valueName?: string // 属性值名称
|
valueName?: string // 属性值名称
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO puhui999:是不是直接叫 Sku 更简洁一点哈。type 待后面,总感觉有个类型?
|
export interface Sku {
|
||||||
export interface SkuType {
|
|
||||||
id?: number // 商品 SKU 编号
|
id?: number // 商品 SKU 编号
|
||||||
spuId?: number // SPU 编号
|
spuId?: number // SPU 编号
|
||||||
properties?: Property[] // 属性数组
|
properties?: Property[] // 属性数组
|
||||||
@ -25,8 +24,7 @@ export interface SkuType {
|
|||||||
salesCount?: number // 商品销量
|
salesCount?: number // 商品销量
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO puhui999:是不是直接叫 Spu 更简洁一点哈。type 待后面,总感觉有个类型?
|
export interface Spu {
|
||||||
export interface SpuType {
|
|
||||||
id?: number
|
id?: number
|
||||||
name?: string // 商品名称
|
name?: string // 商品名称
|
||||||
categoryId?: number | null // 商品分类
|
categoryId?: number | null // 商品分类
|
||||||
@ -39,9 +37,9 @@ export interface SpuType {
|
|||||||
brandId?: number | null // 商品品牌编号
|
brandId?: number | null // 商品品牌编号
|
||||||
specType?: boolean // 商品规格
|
specType?: boolean // 商品规格
|
||||||
subCommissionType?: boolean // 分销类型
|
subCommissionType?: boolean // 分销类型
|
||||||
skus: SkuType[] // sku数组
|
skus: Sku[] // sku数组
|
||||||
description?: string // 商品详情
|
description?: string // 商品详情
|
||||||
sort?: string // 商品排序
|
sort?: number // 商品排序
|
||||||
giveIntegral?: number // 赠送积分
|
giveIntegral?: number // 赠送积分
|
||||||
virtualSalesCount?: number // 虚拟销量
|
virtualSalesCount?: number // 虚拟销量
|
||||||
recommendHot?: boolean // 是否热卖
|
recommendHot?: boolean // 是否热卖
|
||||||
@ -62,12 +60,12 @@ export const getTabsCount = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 创建商品 Spu
|
// 创建商品 Spu
|
||||||
export const createSpu = (data: SpuType) => {
|
export const createSpu = (data: Spu) => {
|
||||||
return request.post({ url: '/product/spu/create', data })
|
return request.post({ url: '/product/spu/create', data })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新商品 Spu
|
// 更新商品 Spu
|
||||||
export const updateSpu = (data: SpuType) => {
|
export const updateSpu = (data: Spu) => {
|
||||||
return request.put({ url: '/product/spu/update', data })
|
return request.put({ url: '/product/spu/update', data })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ interface TreeHelperConfig {
|
|||||||
children: string
|
children: string
|
||||||
pid: string
|
pid: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_CONFIG: TreeHelperConfig = {
|
const DEFAULT_CONFIG: TreeHelperConfig = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
children: 'children',
|
children: 'children',
|
||||||
@ -133,6 +134,7 @@ export const filter = <T = any>(
|
|||||||
): T[] => {
|
): T[] => {
|
||||||
config = getConfig(config)
|
config = getConfig(config)
|
||||||
const children = config.children as string
|
const children = config.children as string
|
||||||
|
|
||||||
function listFilter(list: T[]) {
|
function listFilter(list: T[]) {
|
||||||
return list
|
return list
|
||||||
.map((node: any) => ({ ...node }))
|
.map((node: any) => ({ ...node }))
|
||||||
@ -141,6 +143,7 @@ export const filter = <T = any>(
|
|||||||
return func(node) || (node[children] && node[children].length)
|
return func(node) || (node[children] && node[children].length)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return listFilter(tree)
|
return listFilter(tree)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -264,6 +267,7 @@ export const handleTree = (data: any[], id?: string, parentId?: string, children
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return tree
|
return tree
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@ -300,3 +304,80 @@ export const handleTree2 = (data, id, parentId, children, rootId) => {
|
|||||||
})
|
})
|
||||||
return treeData !== '' ? treeData : data
|
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 descriptionRef = ref<ComponentRef<typeof DescriptionForm>>() // 商品详情Ref
|
||||||
const otherSettingsRef = ref<ComponentRef<typeof OtherSettingsForm>>() // 其他设置Ref
|
const otherSettingsRef = ref<ComponentRef<typeof OtherSettingsForm>>() // 其他设置Ref
|
||||||
// spu 表单数据
|
// spu 表单数据
|
||||||
const formData = ref<ProductSpuApi.SpuType>({
|
const formData = ref<ProductSpuApi.Spu>({
|
||||||
name: '', // 商品名称
|
name: '', // 商品名称
|
||||||
categoryId: null, // 商品分类
|
categoryId: null, // 商品分类
|
||||||
keyword: '', // 关键字
|
keyword: '', // 关键字
|
||||||
unit: null, // 单位
|
unit: null, // 单位
|
||||||
picUrl: '', // 商品封面图
|
picUrl: '', // 商品封面图
|
||||||
sliderPicUrls: [], // 商品轮播图
|
sliderPicUrls: [''], // 商品轮播图
|
||||||
introduction: '', // 商品简介
|
introduction: '', // 商品简介
|
||||||
deliveryTemplateId: 1, // 运费模版
|
deliveryTemplateId: null, // 运费模版
|
||||||
brandId: null, // 商品品牌
|
brandId: null, // 商品品牌
|
||||||
specType: false, // 商品规格
|
specType: false, // 商品规格
|
||||||
subCommissionType: false, // 分销类型
|
subCommissionType: false, // 分销类型
|
||||||
@ -94,7 +94,7 @@ const getDetail = async () => {
|
|||||||
if (id) {
|
if (id) {
|
||||||
formLoading.value = true
|
formLoading.value = true
|
||||||
try {
|
try {
|
||||||
const res = (await ProductSpuApi.getSpu(id)) as ProductSpuApi.SpuType
|
const res = (await ProductSpuApi.getSpu(id)) as ProductSpuApi.Spu
|
||||||
res.skus.forEach((item) => {
|
res.skus.forEach((item) => {
|
||||||
// 回显价格分转元
|
// 回显价格分转元
|
||||||
item.price = formatToFraction(item.price)
|
item.price = formatToFraction(item.price)
|
||||||
@ -120,8 +120,9 @@ const submitForm = async () => {
|
|||||||
await unref(basicInfoRef)?.validate()
|
await unref(basicInfoRef)?.validate()
|
||||||
await unref(descriptionRef)?.validate()
|
await unref(descriptionRef)?.validate()
|
||||||
await unref(otherSettingsRef)?.validate()
|
await unref(otherSettingsRef)?.validate()
|
||||||
const deepCopyFormData = cloneDeep(unref(formData.value)) // 深拷贝一份 fix:这样最终 server 端不满足,不需要恢复,
|
// 深拷贝一份, 这样最终 server 端不满足,不需要恢复,
|
||||||
// TODO 兜底处理 sku 空数据
|
const deepCopyFormData = cloneDeep(unref(formData.value))
|
||||||
|
// 兜底处理 sku 空数据
|
||||||
formData.value.skus.forEach((sku) => {
|
formData.value.skus.forEach((sku) => {
|
||||||
// 因为是空数据这里判断一下商品条码是否为空就行
|
// 因为是空数据这里判断一下商品条码是否为空就行
|
||||||
if (sku.barCode === '') {
|
if (sku.barCode === '') {
|
||||||
@ -150,7 +151,7 @@ const submitForm = async () => {
|
|||||||
})
|
})
|
||||||
deepCopyFormData.sliderPicUrls = newSliderPicUrls
|
deepCopyFormData.sliderPicUrls = newSliderPicUrls
|
||||||
// 校验都通过后提交表单
|
// 校验都通过后提交表单
|
||||||
const data = deepCopyFormData as ProductSpuApi.SpuType
|
const data = deepCopyFormData as ProductSpuApi.Spu
|
||||||
const id = params.spuId as number
|
const id = params.spuId as number
|
||||||
if (!id) {
|
if (!id) {
|
||||||
await ProductSpuApi.createSpu(data)
|
await ProductSpuApi.createSpu(data)
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<!-- TODO @puhui999:只能选根节点 -->
|
<!-- TODO @puhui999:只能选根节点 fix: 已完善-->
|
||||||
<el-form-item label="商品分类" prop="categoryId">
|
<el-form-item label="商品分类" prop="categoryId">
|
||||||
<el-tree-select
|
<el-tree-select
|
||||||
v-model="formData.categoryId"
|
v-model="formData.categoryId"
|
||||||
@ -17,6 +17,7 @@
|
|||||||
class="w-1/1"
|
class="w-1/1"
|
||||||
node-key="id"
|
node-key="id"
|
||||||
placeholder="请选择商品分类"
|
placeholder="请选择商品分类"
|
||||||
|
@change="nodeClick"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</el-col>
|
||||||
@ -119,9 +120,9 @@
|
|||||||
import { PropType } from 'vue'
|
import { PropType } from 'vue'
|
||||||
import { copyValueToTarget } from '@/utils'
|
import { copyValueToTarget } from '@/utils'
|
||||||
import { propTypes } from '@/utils/propTypes'
|
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 { 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 { UploadImg, UploadImgs } from '@/components/UploadFile'
|
||||||
import { ProductAttributes, ProductAttributesAddForm, SkuList } from './index'
|
import { ProductAttributes, ProductAttributesAddForm, SkuList } from './index'
|
||||||
import * as ProductCategoryApi from '@/api/mall/product/category'
|
import * as ProductCategoryApi from '@/api/mall/product/category'
|
||||||
@ -131,7 +132,7 @@ const message = useMessage() // 消息弹窗
|
|||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
propFormData: {
|
propFormData: {
|
||||||
type: Object as PropType<SpuType>,
|
type: Object as PropType<Spu>,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
},
|
},
|
||||||
activeName: propTypes.string.def('')
|
activeName: propTypes.string.def('')
|
||||||
@ -144,7 +145,7 @@ const skuListRef = ref() // 商品属性列表Ref
|
|||||||
const generateSkus = (propertyList) => {
|
const generateSkus = (propertyList) => {
|
||||||
skuListRef.value.generateTableData(propertyList)
|
skuListRef.value.generateTableData(propertyList)
|
||||||
}
|
}
|
||||||
const formData = reactive<SpuType>({
|
const formData = reactive<Spu>({
|
||||||
name: '', // 商品名称
|
name: '', // 商品名称
|
||||||
categoryId: null, // 商品分类
|
categoryId: null, // 商品分类
|
||||||
keyword: '', // 关键字
|
keyword: '', // 关键字
|
||||||
@ -185,9 +186,8 @@ watch(
|
|||||||
formData.sliderPicUrls = data['sliderPicUrls'].map((item) => ({
|
formData.sliderPicUrls = data['sliderPicUrls'].map((item) => ({
|
||||||
url: item
|
url: item
|
||||||
}))
|
}))
|
||||||
// TODO @puhui999:if return,减少嵌套层级
|
|
||||||
// 只有是多规格才处理
|
// 只有是多规格才处理
|
||||||
if (formData.specType) {
|
if (!formData.specType) return
|
||||||
// 直接拿返回的 skus 属性逆向生成出 propertyList
|
// 直接拿返回的 skus 属性逆向生成出 propertyList
|
||||||
const properties = []
|
const properties = []
|
||||||
formData.skus.forEach((sku) => {
|
formData.skus.forEach((sku) => {
|
||||||
@ -204,7 +204,6 @@ watch(
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
propertyList.value = properties
|
propertyList.value = properties
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
immediate: true
|
immediate: true
|
||||||
@ -216,6 +215,11 @@ watch(
|
|||||||
*/
|
*/
|
||||||
const emit = defineEmits(['update:activeName'])
|
const emit = defineEmits(['update:activeName'])
|
||||||
const validate = async () => {
|
const validate = async () => {
|
||||||
|
// 校验 sku
|
||||||
|
if (!skuListRef.value.validateSku()) {
|
||||||
|
message.warning('商品相关价格不能低于0.01元!!')
|
||||||
|
throw new Error('商品相关价格不能低于0.01元!!')
|
||||||
|
}
|
||||||
// 校验表单
|
// 校验表单
|
||||||
if (!productSpuBasicInfoRef) return
|
if (!productSpuBasicInfoRef) return
|
||||||
return await unref(productSpuBasicInfoRef).validate((valid) => {
|
return await unref(productSpuBasicInfoRef).validate((valid) => {
|
||||||
@ -263,6 +267,15 @@ const onChangeSpec = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const categoryList = ref([]) // 分类树
|
const categoryList = ref([]) // 分类树
|
||||||
|
/**
|
||||||
|
* 选择分类时触发校验
|
||||||
|
*/
|
||||||
|
const nodeClick = () => {
|
||||||
|
if (!checkSelectedNode(categoryList.value, formData.categoryId)) {
|
||||||
|
formData.categoryId = null
|
||||||
|
message.warning('必须选择二级节点!!')
|
||||||
|
}
|
||||||
|
}
|
||||||
const brandList = ref([]) // 精简商品品牌列表
|
const brandList = ref([]) // 精简商品品牌列表
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
// 获得分类树
|
// 获得分类树
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
</el-form>
|
</el-form>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" name="DescriptionForm" setup>
|
<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 { Editor } from '@/components/Editor'
|
||||||
import { PropType } from 'vue'
|
import { PropType } from 'vue'
|
||||||
import { propTypes } from '@/utils/propTypes'
|
import { propTypes } from '@/utils/propTypes'
|
||||||
@ -16,13 +16,13 @@ import { copyValueToTarget } from '@/utils'
|
|||||||
const message = useMessage() // 消息弹窗
|
const message = useMessage() // 消息弹窗
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
propFormData: {
|
propFormData: {
|
||||||
type: Object as PropType<SpuType>,
|
type: Object as PropType<Spu>,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
},
|
},
|
||||||
activeName: propTypes.string.def('')
|
activeName: propTypes.string.def('')
|
||||||
})
|
})
|
||||||
const descriptionFormRef = ref() // 表单Ref
|
const descriptionFormRef = ref() // 表单Ref
|
||||||
const formData = ref<SpuType>({
|
const formData = ref<Spu>({
|
||||||
description: '' // 商品详情
|
description: '' // 商品详情
|
||||||
})
|
})
|
||||||
// 表单规则
|
// 表单规则
|
||||||
|
@ -52,7 +52,7 @@
|
|||||||
</el-form>
|
</el-form>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" name="OtherSettingsForm" setup>
|
<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 { PropType } from 'vue'
|
||||||
import { propTypes } from '@/utils/propTypes'
|
import { propTypes } from '@/utils/propTypes'
|
||||||
import { copyValueToTarget } from '@/utils'
|
import { copyValueToTarget } from '@/utils'
|
||||||
@ -61,7 +61,7 @@ const message = useMessage() // 消息弹窗
|
|||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
propFormData: {
|
propFormData: {
|
||||||
type: Object as PropType<SpuType>,
|
type: Object as PropType<Spu>,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
},
|
},
|
||||||
activeName: propTypes.string.def('')
|
activeName: propTypes.string.def('')
|
||||||
@ -69,7 +69,7 @@ const props = defineProps({
|
|||||||
|
|
||||||
const otherSettingsFormRef = ref() // 表单Ref
|
const otherSettingsFormRef = ref() // 表单Ref
|
||||||
// 表单数据
|
// 表单数据
|
||||||
const formData = ref<SpuType>({
|
const formData = ref<Spu>({
|
||||||
sort: 1, // 商品排序
|
sort: 1, // 商品排序
|
||||||
giveIntegral: 1, // 赠送积分
|
giveIntegral: 1, // 赠送积分
|
||||||
virtualSalesCount: 1, // 虚拟销量
|
virtualSalesCount: 1, // 虚拟销量
|
||||||
|
@ -90,8 +90,7 @@ const submitForm = async () => {
|
|||||||
/** 重置表单 */
|
/** 重置表单 */
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
formData.value = {
|
formData.value = {
|
||||||
name: '',
|
name: ''
|
||||||
remark: ''
|
|
||||||
}
|
}
|
||||||
formRef.value?.resetFields()
|
formRef.value?.resetFields()
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-table
|
<el-table
|
||||||
:data="isBatch ? skuList : formData.skus"
|
:data="isBatch ? skuList : formData!.skus"
|
||||||
border
|
border
|
||||||
class="tabNumWidth"
|
class="tabNumWidth"
|
||||||
max-height="500"
|
max-height="500"
|
||||||
@ -11,7 +11,7 @@
|
|||||||
<UploadImg v-model="row.picUrl" height="80px" width="100%" />
|
<UploadImg v-model="row.picUrl" height="80px" width="100%" />
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<template v-if="formData.specType && !isBatch">
|
<template v-if="formData!.specType && !isBatch">
|
||||||
<!-- 根据商品属性动态添加 -->
|
<!-- 根据商品属性动态添加 -->
|
||||||
<el-table-column
|
<el-table-column
|
||||||
v-for="(item, index) in tableHeaders"
|
v-for="(item, index) in tableHeaders"
|
||||||
@ -21,8 +21,10 @@
|
|||||||
min-width="120"
|
min-width="120"
|
||||||
>
|
>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<!-- TODO puhui999:展示成蓝色,有点区分度哈 -->
|
<!-- TODO puhui999:展示成蓝色,有点区分度哈 fix: 字体加粗,颜色使用 #99a9bf 蓝色有点不好看哈哈-->
|
||||||
|
<span style="font-weight: bold; color: #99a9bf">
|
||||||
{{ row.properties[index]?.valueName }}
|
{{ row.properties[index]?.valueName }}
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</template>
|
</template>
|
||||||
@ -73,7 +75,7 @@
|
|||||||
<el-input-number v-model="row.volume" :min="0" :precision="2" :step="0.1" class="w-100%" />
|
<el-input-number v-model="row.volume" :min="0" :precision="2" :step="0.1" class="w-100%" />
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<template v-if="formData.subCommissionType">
|
<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 }">
|
<template #default="{ row }">
|
||||||
<el-input-number
|
<el-input-number
|
||||||
@ -97,7 +99,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</template>
|
</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 }">
|
<template #default="{ row }">
|
||||||
<el-button v-if="isBatch" link size="small" type="primary" @click="batchAdd">
|
<el-button v-if="isBatch" link size="small" type="primary" @click="batchAdd">
|
||||||
批量添加
|
批量添加
|
||||||
@ -108,15 +110,15 @@
|
|||||||
</el-table>
|
</el-table>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" name="SkuList" setup>
|
<script lang="ts" name="SkuList" setup>
|
||||||
import { PropType } from 'vue'
|
import { PropType, Ref } from 'vue'
|
||||||
import { copyValueToTarget } from '@/utils'
|
import { copyValueToTarget } from '@/utils'
|
||||||
import { propTypes } from '@/utils/propTypes'
|
import { propTypes } from '@/utils/propTypes'
|
||||||
import { UploadImg } from '@/components/UploadFile'
|
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({
|
const props = defineProps({
|
||||||
propFormData: {
|
propFormData: {
|
||||||
type: Object as PropType<SpuType>,
|
type: Object as PropType<Spu>,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
},
|
},
|
||||||
propertyList: {
|
propertyList: {
|
||||||
@ -125,8 +127,8 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
isBatch: propTypes.bool.def(false) // 是否作为批量操作组件
|
isBatch: propTypes.bool.def(false) // 是否作为批量操作组件
|
||||||
})
|
})
|
||||||
const formData = ref<SpuType>() // 表单数据
|
const formData: Ref<Spu | undefined> = ref<Spu>() // 表单数据
|
||||||
const skuList = ref<SkuType[]>([
|
const skuList = ref<Sku[]>([
|
||||||
{
|
{
|
||||||
price: 0, // 商品价格
|
price: 0, // 商品价格
|
||||||
marketPrice: 0, // 市场价
|
marketPrice: 0, // 市场价
|
||||||
@ -140,24 +142,37 @@ const skuList = ref<SkuType[]>([
|
|||||||
subCommissionSecondPrice: 0 // 二级分销的佣金
|
subCommissionSecondPrice: 0 // 二级分销的佣金
|
||||||
}
|
}
|
||||||
]) // 批量添加时的临时数据
|
]) // 批量添加时的临时数据
|
||||||
// TODO @puhui999:保存时,每个商品规格的表单要校验下。例如说,销售金额最低是 0.01 这种。
|
|
||||||
|
|
||||||
/** 批量添加 */
|
/** 批量添加 */
|
||||||
const batchAdd = () => {
|
const batchAdd = () => {
|
||||||
formData.value.skus.forEach((item) => {
|
formData.value!.skus.forEach((item) => {
|
||||||
copyValueToTarget(item, skuList.value[0])
|
copyValueToTarget(item, skuList.value[0])
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 删除 sku */
|
/** 删除 sku */
|
||||||
const deleteSku = (row) => {
|
const deleteSku = (row) => {
|
||||||
const index = formData.value.skus.findIndex(
|
const index = formData.value!.skus.findIndex(
|
||||||
// 直接把列表转成字符串比较
|
// 直接把列表转成字符串比较
|
||||||
(sku) => JSON.stringify(sku.properties) === JSON.stringify(row.properties)
|
(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 }[]>([]) // 多属性表头
|
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
|
* 将传进来的值赋值给 skuList
|
||||||
@ -185,14 +200,13 @@ const generateTableData = (propertyList: any[]) => {
|
|||||||
valueName: v.name
|
valueName: v.name
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
// TODO @puhui:是不是 buildSkuList,这样容易理解一点哈。item 改成 sku
|
const buildSkuList = build(propertyValues)
|
||||||
const buildList = build(propertyValues)
|
|
||||||
// 如果回显的 sku 属性和添加的属性不一致则重置 skus 列表
|
// 如果回显的 sku 属性和添加的属性不一致则重置 skus 列表
|
||||||
if (!validateData(propertyList)) {
|
if (!validateData(propertyList)) {
|
||||||
// 如果不一致则重置表数据,默认添加新的属性重新生成 sku 列表
|
// 如果不一致则重置表数据,默认添加新的属性重新生成 sku 列表
|
||||||
formData.value!.skus = []
|
formData.value!.skus = []
|
||||||
}
|
}
|
||||||
for (const item of buildList) {
|
for (const item of buildSkuList) {
|
||||||
const row = {
|
const row = {
|
||||||
properties: Array.isArray(item) ? item : [item], // 如果只有一个属性的话返回的是一个 property 对象
|
properties: Array.isArray(item) ? item : [item], // 如果只有一个属性的话返回的是一个 property 对象
|
||||||
price: 0,
|
price: 0,
|
||||||
@ -213,7 +227,7 @@ const generateTableData = (propertyList: any[]) => {
|
|||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
formData.value.skus.push(row)
|
formData.value!.skus.push(row)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -222,7 +236,7 @@ const generateTableData = (propertyList: any[]) => {
|
|||||||
*/
|
*/
|
||||||
const validateData = (propertyList: any[]) => {
|
const validateData = (propertyList: any[]) => {
|
||||||
const skuPropertyIds = []
|
const skuPropertyIds = []
|
||||||
formData.value.skus.forEach((sku) =>
|
formData.value!.skus.forEach((sku) =>
|
||||||
sku.properties
|
sku.properties
|
||||||
?.map((property) => property.propertyId)
|
?.map((property) => property.propertyId)
|
||||||
.forEach((propertyId) => {
|
.forEach((propertyId) => {
|
||||||
@ -263,7 +277,7 @@ watch(
|
|||||||
() => props.propertyList,
|
() => props.propertyList,
|
||||||
(propertyList) => {
|
(propertyList) => {
|
||||||
// 如果不是多规格则结束
|
// 如果不是多规格则结束
|
||||||
if (!formData.value.specType) {
|
if (!formData.value!.specType) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 如果当前组件作为批量添加数据使用,则重置表数据
|
// 如果当前组件作为批量添加数据使用,则重置表数据
|
||||||
@ -313,5 +327,5 @@ watch(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
// 暴露出生成 sku 方法,给添加属性成功时调用
|
// 暴露出生成 sku 方法,给添加属性成功时调用
|
||||||
defineExpose({ generateTableData })
|
defineExpose({ generateTableData, validateSku })
|
||||||
</script>
|
</script>
|
||||||
|
@ -8,18 +8,16 @@
|
|||||||
class="-mb-15px"
|
class="-mb-15px"
|
||||||
label-width="68px"
|
label-width="68px"
|
||||||
>
|
>
|
||||||
<!-- TODO @puhui999:品牌应该是数据下拉哈 -->
|
<el-form-item label="商品名称" prop="name">
|
||||||
<el-form-item label="品牌名称" prop="name">
|
|
||||||
<el-input
|
<el-input
|
||||||
v-model="queryParams.name"
|
v-model="queryParams.name"
|
||||||
class="!w-240px"
|
class="!w-240px"
|
||||||
clearable
|
clearable
|
||||||
placeholder="请输入品牌名称"
|
placeholder="请输入商品名称"
|
||||||
@keyup.enter="handleQuery"
|
@keyup.enter="handleQuery"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<!-- TODO 分类只能选择二级分类目前还没做,还是先以联调通顺为主 -->
|
<!-- TODO 分类只能选择二级分类目前还没做,还是先以联调通顺为主 fixL: 已完善 -->
|
||||||
<!-- TODO puhui999:我们要不改成支持选择一级。如果选择一级,后端要递归查询下子分类,然后去 in? -->
|
|
||||||
<el-form-item label="商品分类" prop="categoryId">
|
<el-form-item label="商品分类" prop="categoryId">
|
||||||
<el-tree-select
|
<el-tree-select
|
||||||
v-model="queryParams.categoryId"
|
v-model="queryParams.categoryId"
|
||||||
@ -29,6 +27,7 @@
|
|||||||
class="w-1/1"
|
class="w-1/1"
|
||||||
node-key="id"
|
node-key="id"
|
||||||
placeholder="请选择商品分类"
|
placeholder="请选择商品分类"
|
||||||
|
@change="nodeClick"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="创建时间" prop="createTime">
|
<el-form-item label="创建时间" prop="createTime">
|
||||||
@ -80,7 +79,7 @@
|
|||||||
/>
|
/>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
<el-table v-loading="loading" :data="list">
|
<el-table v-loading="loading" :data="list">
|
||||||
<!-- TODO puhui:这几个属性哈,一行三个
|
<!-- TODO puhui:这几个属性哈,一行三个 fix
|
||||||
商品分类:服装鞋包/箱包
|
商品分类:服装鞋包/箱包
|
||||||
商品市场价格:100.00
|
商品市场价格:100.00
|
||||||
成本价:0.00
|
成本价:0.00
|
||||||
@ -88,23 +87,52 @@
|
|||||||
虚拟销量:999 -->
|
虚拟销量:999 -->
|
||||||
<el-table-column type="expand" width="30">
|
<el-table-column type="expand" width="30">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-form class="demo-table-expand" inline label-position="left">
|
<el-form class="demo-table-expand" label-position="left">
|
||||||
<el-form-item label="市场价:">
|
<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>
|
<span>{{ formatToFraction(row.marketPrice) }}</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="成本价:">
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-form-item label="成本价:">
|
||||||
<span>{{ formatToFraction(row.costPrice) }}</span>
|
<span>{{ formatToFraction(row.costPrice) }}</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="虚拟销量:">
|
</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>
|
<span>{{ row.virtualSalesCount }}</span>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
</el-form>
|
</el-form>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column key="id" align="center" label="商品编号" prop="id" />
|
<el-table-column key="id" align="center" label="商品编号" prop="id" />
|
||||||
<el-table-column label="商品图" min-width="80">
|
<el-table-column label="商品图" min-width="80">
|
||||||
<template #default="{ row }">
|
<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>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column :show-overflow-tooltip="true" label="商品名称" min-width="300" prop="name" />
|
<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 { cloneDeep } from 'lodash-es'
|
||||||
import { createImageViewer } from '@/components/ImageViewer'
|
import { createImageViewer } from '@/components/ImageViewer'
|
||||||
import { dateFormatter } from '@/utils/formatTime'
|
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 { ProductSpuStatusEnum } from '@/utils/constants'
|
||||||
import { formatToFraction } from '@/utils'
|
import { formatToFraction } from '@/utils'
|
||||||
import download from '@/utils/download'
|
import download from '@/utils/download'
|
||||||
@ -391,7 +419,7 @@ const handleExport = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听路由变化更新列表 TODO @puhui999:这个是必须加的么?fix: 因为编辑表单是以路由的方式打开,保存表单后列表不会刷新
|
// 监听路由变化更新列表,解决商品保存后,列表不刷新的问题。
|
||||||
watch(
|
watch(
|
||||||
() => currentRoute.value,
|
() => currentRoute.value,
|
||||||
() => {
|
() => {
|
||||||
@ -400,6 +428,22 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const categoryList = ref() // 分类树
|
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 () => {
|
onMounted(async () => {
|
||||||
await getTabsCount()
|
await getTabsCount()
|
||||||
|
Loading…
Reference in New Issue
Block a user