feat:【mall 商城】商品发布 - 库存价格【antd】30%: 初始化
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
import type { VbenFormSchema } from '#/adapter/form';
|
import type { VbenFormSchema } from '#/adapter/form';
|
||||||
|
|
||||||
|
import { h } from 'vue';
|
||||||
|
|
||||||
import { DeliveryTypeEnum, DICT_TYPE } from '@vben/constants';
|
import { DeliveryTypeEnum, DICT_TYPE } from '@vben/constants';
|
||||||
import { getDictOptions } from '@vben/hooks';
|
import { getDictOptions } from '@vben/hooks';
|
||||||
import { handleTree } from '@vben/utils';
|
import { handleTree } from '@vben/utils';
|
||||||
@@ -9,6 +11,8 @@ import { getSimpleBrandList } from '#/api/mall/product/brand';
|
|||||||
import { getCategoryList } from '#/api/mall/product/category';
|
import { getCategoryList } from '#/api/mall/product/category';
|
||||||
import { getSimpleTemplateList } from '#/api/mall/trade/delivery/expressTemplate';
|
import { getSimpleTemplateList } from '#/api/mall/trade/delivery/expressTemplate';
|
||||||
|
|
||||||
|
import SkuList from './sku-list.vue';
|
||||||
|
|
||||||
/** 基础设置的表单 */
|
/** 基础设置的表单 */
|
||||||
export function useInfoFormSchema(): VbenFormSchema[] {
|
export function useInfoFormSchema(): VbenFormSchema[] {
|
||||||
return [
|
return [
|
||||||
@@ -104,7 +108,11 @@ export function useInfoFormSchema(): VbenFormSchema[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 价格库存的表单 */
|
/** 价格库存的表单 */
|
||||||
export function useSkuFormSchema(): VbenFormSchema[] {
|
export function useSkuFormSchema(
|
||||||
|
propertyList: any[] = [],
|
||||||
|
ruleConfig: any[] = [],
|
||||||
|
isDetail: boolean = false,
|
||||||
|
): VbenFormSchema[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
fieldName: 'id',
|
fieldName: 'id',
|
||||||
@@ -152,7 +160,65 @@ export function useSkuFormSchema(): VbenFormSchema[] {
|
|||||||
},
|
},
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
// TODO @xingyu:待补充商品属性
|
// 单规格时显示的 SkuList
|
||||||
|
{
|
||||||
|
fieldName: 'singleSkuList',
|
||||||
|
label: '',
|
||||||
|
component: h(SkuList),
|
||||||
|
componentProps: {
|
||||||
|
propertyList,
|
||||||
|
ruleConfig,
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: ['specType'],
|
||||||
|
// 当 specType 为 false(单规格)时显示
|
||||||
|
show: (values) => values.specType === false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 多规格时显示的商品属性(占位,实际通过插槽渲染)
|
||||||
|
{
|
||||||
|
fieldName: 'productAttributes',
|
||||||
|
label: '商品属性',
|
||||||
|
component: 'Input',
|
||||||
|
componentProps: {},
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: ['specType'],
|
||||||
|
// 当 specType 为 true(多规格)时显示
|
||||||
|
show: (values) => values.specType === true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 多规格 - 批量设置
|
||||||
|
{
|
||||||
|
fieldName: 'batchSkuList',
|
||||||
|
label: '批量设置',
|
||||||
|
component: h(SkuList),
|
||||||
|
componentProps: {
|
||||||
|
isBatch: true,
|
||||||
|
propertyList,
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: ['specType'],
|
||||||
|
// 当 specType 为 true(多规格)且 propertyList 有数据时显示,且非详情模式
|
||||||
|
show: (values) =>
|
||||||
|
values.specType === true && propertyList.length > 0 && !isDetail,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 多规格 - 规格列表
|
||||||
|
{
|
||||||
|
fieldName: 'multiSkuList',
|
||||||
|
label: '规格列表',
|
||||||
|
component: h(SkuList),
|
||||||
|
componentProps: {
|
||||||
|
propertyList,
|
||||||
|
ruleConfig,
|
||||||
|
isDetail,
|
||||||
|
},
|
||||||
|
dependencies: {
|
||||||
|
triggerFields: ['specType'],
|
||||||
|
// 当 specType 为 true(多规格)且 propertyList 有数据时显示
|
||||||
|
show: (values) => values.specType === true && propertyList.length > 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { PropertyAndValues, RuleConfig } from './index';
|
||||||
|
|
||||||
import type { MallSpuApi } from '#/api/mall/product/spu';
|
import type { MallSpuApi } from '#/api/mall/product/spu';
|
||||||
|
|
||||||
import { onMounted, ref } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
|
|
||||||
import { ContentWrap, Page } from '@vben/common-ui';
|
import { ContentWrap, Page, useVbenModal } from '@vben/common-ui';
|
||||||
import { convertToInteger, formatToFraction } from '@vben/utils';
|
import { convertToInteger, floatToFixed2, formatToFraction } from '@vben/utils';
|
||||||
|
|
||||||
import { Button, Tabs } from 'ant-design-vue';
|
import { Button, Tabs } from 'ant-design-vue';
|
||||||
|
|
||||||
@@ -19,11 +21,81 @@ import {
|
|||||||
useOtherFormSchema,
|
useOtherFormSchema,
|
||||||
useSkuFormSchema,
|
useSkuFormSchema,
|
||||||
} from './form-data';
|
} from './form-data';
|
||||||
|
import { getPropertyList } from './index';
|
||||||
|
import ProductAttributes from './product-attributes.vue';
|
||||||
|
import ProductPropertyAddForm from './product-property-add-form.vue';
|
||||||
|
|
||||||
const spuId = ref<number>();
|
const spuId = ref<number>();
|
||||||
const { params } = useRoute();
|
const { params, name } = useRoute();
|
||||||
|
|
||||||
const activeTabName = ref('info');
|
const activeTabName = ref('info');
|
||||||
|
// spu 表单数据
|
||||||
|
const formData = ref<MallSpuApi.Spu>({
|
||||||
|
name: '', // 商品名称
|
||||||
|
categoryId: undefined, // 商品分类
|
||||||
|
keyword: '', // 关键字
|
||||||
|
picUrl: '', // 商品封面图
|
||||||
|
sliderPicUrls: [], // 商品轮播图
|
||||||
|
introduction: '', // 商品简介
|
||||||
|
deliveryTypes: [], // 配送方式数组
|
||||||
|
deliveryTemplateId: undefined, // 运费模版
|
||||||
|
brandId: undefined, // 商品品牌
|
||||||
|
specType: false, // 商品规格
|
||||||
|
subCommissionType: false, // 分销类型
|
||||||
|
skus: [
|
||||||
|
{
|
||||||
|
price: 0, // 商品价格
|
||||||
|
marketPrice: 0, // 市场价
|
||||||
|
costPrice: 0, // 成本价
|
||||||
|
barCode: '', // 商品条码
|
||||||
|
picUrl: '', // 图片地址
|
||||||
|
stock: 0, // 库存
|
||||||
|
weight: 0, // 商品重量
|
||||||
|
volume: 0, // 商品体积
|
||||||
|
firstBrokeragePrice: 0, // 一级分销的佣金
|
||||||
|
secondBrokeragePrice: 0, // 二级分销的佣金
|
||||||
|
},
|
||||||
|
],
|
||||||
|
description: '', // 商品详情
|
||||||
|
sort: 0, // 商品排序
|
||||||
|
giveIntegral: 0, // 赠送积分
|
||||||
|
virtualSalesCount: 0, // 虚拟销量
|
||||||
|
});
|
||||||
|
const propertyList = ref<PropertyAndValues[]>([]); // 商品属性列表
|
||||||
|
const formLoading = ref(false); // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||||
|
const isDetail = ref(false); // 是否查看详情
|
||||||
|
|
||||||
|
// sku 相关属性校验规则
|
||||||
|
const ruleConfig: RuleConfig[] = [
|
||||||
|
{
|
||||||
|
name: 'stock',
|
||||||
|
rule: (arg) => arg >= 0,
|
||||||
|
message: '商品库存必须大于等于 1 !!!',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'price',
|
||||||
|
rule: (arg) => arg >= 0.01,
|
||||||
|
message: '商品销售价格必须大于等于 0.01 元!!!',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'marketPrice',
|
||||||
|
rule: (arg) => arg >= 0.01,
|
||||||
|
message: '商品市场价格必须大于等于 0.01 元!!!',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'costPrice',
|
||||||
|
rule: (arg) => arg >= 0.01,
|
||||||
|
message: '商品成本价格必须大于等于 0.00 元!!!',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 动态生成 sku form schema
|
||||||
|
const skuFormSchema = computed(() => {
|
||||||
|
return useSkuFormSchema(
|
||||||
|
propertyList.value,
|
||||||
|
ruleConfig,
|
||||||
|
isDetail.value || false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const [InfoForm, infoFormApi] = useVbenForm({
|
const [InfoForm, infoFormApi] = useVbenForm({
|
||||||
commonConfig: {
|
commonConfig: {
|
||||||
@@ -47,10 +119,15 @@ const [SkuForm, skuFormApi] = useVbenForm({
|
|||||||
labelWidth: 120,
|
labelWidth: 120,
|
||||||
},
|
},
|
||||||
layout: 'horizontal',
|
layout: 'horizontal',
|
||||||
schema: useSkuFormSchema(),
|
schema: skuFormSchema.value,
|
||||||
showDefaultActions: false,
|
showDefaultActions: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [ProductPropertyAddFormModal, productPropertyAddFormApi] = useVbenModal({
|
||||||
|
connectedComponent: ProductPropertyAddForm,
|
||||||
|
destroyOnClose: true,
|
||||||
|
});
|
||||||
|
|
||||||
const [DeliveryForm, deliveryFormApi] = useVbenForm({
|
const [DeliveryForm, deliveryFormApi] = useVbenForm({
|
||||||
commonConfig: {
|
commonConfig: {
|
||||||
componentProps: {
|
componentProps: {
|
||||||
@@ -121,35 +198,76 @@ async function onSubmit() {
|
|||||||
await (spuId.value ? updateSpu(values) : createSpu(values));
|
await (spuId.value ? updateSpu(values) : createSpu(values));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initDate() {
|
/** 获得详情 */
|
||||||
spuId.value = params.id as unknown as number;
|
const getDetail = async () => {
|
||||||
if (!spuId.value) {
|
if (name === 'ProductSpuDetail') {
|
||||||
return;
|
isDetail.value = true;
|
||||||
}
|
}
|
||||||
const res = await getSpu(spuId.value);
|
const id = params.id as unknown as number;
|
||||||
if (res.skus) {
|
if (id) {
|
||||||
res.skus.forEach((item) => {
|
formLoading.value = true;
|
||||||
|
try {
|
||||||
|
const res = await getSpu(spuId.value!);
|
||||||
|
res.skus?.forEach((item) => {
|
||||||
|
if (isDetail.value) {
|
||||||
|
item.price = floatToFixed2(item.price);
|
||||||
|
item.marketPrice = floatToFixed2(item.marketPrice);
|
||||||
|
item.costPrice = floatToFixed2(item.costPrice);
|
||||||
|
item.firstBrokeragePrice = floatToFixed2(item.firstBrokeragePrice);
|
||||||
|
item.secondBrokeragePrice = floatToFixed2(item.secondBrokeragePrice);
|
||||||
|
} else {
|
||||||
// 回显价格分转元
|
// 回显价格分转元
|
||||||
item.price = formatToFraction(item.price);
|
item.price = formatToFraction(item.price);
|
||||||
item.marketPrice = formatToFraction(item.marketPrice);
|
item.marketPrice = formatToFraction(item.marketPrice);
|
||||||
item.costPrice = formatToFraction(item.costPrice);
|
item.costPrice = formatToFraction(item.costPrice);
|
||||||
item.firstBrokeragePrice = formatToFraction(item.firstBrokeragePrice);
|
item.firstBrokeragePrice = formatToFraction(item.firstBrokeragePrice);
|
||||||
item.secondBrokeragePrice = formatToFraction(item.secondBrokeragePrice);
|
item.secondBrokeragePrice = formatToFraction(
|
||||||
});
|
item.secondBrokeragePrice,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
formData.value = res;
|
||||||
|
// 将 SKU 的属性,整理成 PropertyAndValues 数组
|
||||||
|
propertyList.value = getPropertyList(res);
|
||||||
|
// 初始化各表单值(异步)
|
||||||
infoFormApi.setValues(res);
|
infoFormApi.setValues(res);
|
||||||
|
// 特殊处理 SpuSkuFormData
|
||||||
skuFormApi.setValues(res);
|
skuFormApi.setValues(res);
|
||||||
deliveryFormApi.setValues(res);
|
deliveryFormApi.setValues(res);
|
||||||
descriptionFormApi.setValues(res);
|
descriptionFormApi.setValues(res);
|
||||||
otherFormApi.setValues(res);
|
otherFormApi.setValues(res);
|
||||||
|
} finally {
|
||||||
|
formLoading.value = false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 调用 SkuList generateTableData 方法*/
|
||||||
|
const generateSkus = (propertyList: any[]) => {
|
||||||
|
// skuListRef.value.generateTableData(propertyList)
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听 sku form schema 变化,更新表单
|
||||||
|
watch(
|
||||||
|
skuFormSchema,
|
||||||
|
(newSchema) => {
|
||||||
|
skuFormApi.updateSchema(newSchema);
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
);
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await initDate();
|
spuId.value = params.id as unknown as number;
|
||||||
|
if (!spuId.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await getDetail();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<ProductPropertyAddFormModal />
|
||||||
|
|
||||||
<Page auto-content-height>
|
<Page auto-content-height>
|
||||||
<ContentWrap class="h-full w-full pb-8">
|
<ContentWrap class="h-full w-full pb-8">
|
||||||
<template #extra>
|
<template #extra>
|
||||||
@@ -160,7 +278,23 @@ onMounted(async () => {
|
|||||||
<InfoForm class="w-3/5" />
|
<InfoForm class="w-3/5" />
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
<Tabs.TabPane tab="价格库存" key="sku">
|
<Tabs.TabPane tab="价格库存" key="sku">
|
||||||
<SkuForm class="w-3/5" />
|
<SkuForm class="w-3/5">
|
||||||
|
<template #productAttributes>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
class="mb-10px mr-15px"
|
||||||
|
@click="productPropertyAddFormApi.open"
|
||||||
|
>
|
||||||
|
添加属性
|
||||||
|
</Button>
|
||||||
|
<ProductAttributes
|
||||||
|
:is-detail="isDetail"
|
||||||
|
:property-list="propertyList"
|
||||||
|
@success="generateSkus"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</SkuForm>
|
||||||
</Tabs.TabPane>
|
</Tabs.TabPane>
|
||||||
<Tabs.TabPane tab="物流设置" key="delivery">
|
<Tabs.TabPane tab="物流设置" key="delivery">
|
||||||
<DeliveryForm class="w-3/5" />
|
<DeliveryForm class="w-3/5" />
|
||||||
|
|||||||
@@ -63,4 +63,5 @@ const getPropertyList = (spu: MallSpuApi.Spu): PropertyAndValues[] => {
|
|||||||
|
|
||||||
export { getPropertyList };
|
export { getPropertyList };
|
||||||
|
|
||||||
// export { default as SkuList } from './sku-list.vue';
|
// 导出组件
|
||||||
|
export { default as SkuList } from './sku-list.vue';
|
||||||
|
|||||||
@@ -73,6 +73,14 @@ const formSchema: VbenFormSchema[] = [
|
|||||||
|
|
||||||
// 初始化表单
|
// 初始化表单
|
||||||
const [Form, formApi] = useVbenForm({
|
const [Form, formApi] = useVbenForm({
|
||||||
|
commonConfig: {
|
||||||
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 120,
|
||||||
|
},
|
||||||
|
layout: 'horizontal',
|
||||||
schema: formSchema,
|
schema: formSchema,
|
||||||
showDefaultActions: false,
|
showDefaultActions: false,
|
||||||
});
|
});
|
||||||
|
|||||||
618
apps/web-antd/src/views/mall/product/spu/modules/sku-list.vue
Normal file
618
apps/web-antd/src/views/mall/product/spu/modules/sku-list.vue
Normal file
@@ -0,0 +1,618 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
|
||||||
|
import type { PropertyAndValues, RuleConfig } from './index';
|
||||||
|
|
||||||
|
import type { MallSpuApi } from '#/api/mall/product/spu';
|
||||||
|
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
|
||||||
|
import { formatToFraction, isEmpty } from '@vben/utils';
|
||||||
|
|
||||||
|
import { Button, Image, Input, InputNumber, message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { VxeColumn, VxeTable } from '#/adapter/vxe-table';
|
||||||
|
import ImageUpload from '#/components/upload/image-upload.vue';
|
||||||
|
|
||||||
|
defineOptions({ name: 'SkuList' });
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
isActivityComponent?: boolean; // 是否作为 sku 活动配置组件
|
||||||
|
isBatch?: boolean; // 是否作为批量操作组件
|
||||||
|
isComponent?: boolean; // 是否作为 sku 选择组件
|
||||||
|
isDetail?: boolean; // 是否作为 sku 详情组件
|
||||||
|
propertyList?: PropertyAndValues[];
|
||||||
|
propFormData?: MallSpuApi.Spu;
|
||||||
|
ruleConfig?: RuleConfig[];
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
propFormData: () => ({}) as MallSpuApi.Spu,
|
||||||
|
propertyList: () => [],
|
||||||
|
ruleConfig: () => [],
|
||||||
|
isBatch: false,
|
||||||
|
isDetail: false,
|
||||||
|
isComponent: false,
|
||||||
|
isActivityComponent: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'selectionChange', value: MallSpuApi.Sku[]): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const { isBatch, isDetail, isComponent, isActivityComponent } = props;
|
||||||
|
|
||||||
|
const formData: Ref<MallSpuApi.Spu | undefined> = ref<MallSpuApi.Spu>(); // 表单数据
|
||||||
|
const skuList = ref<MallSpuApi.Sku[]>([
|
||||||
|
{
|
||||||
|
price: 0, // 商品价格
|
||||||
|
marketPrice: 0, // 市场价
|
||||||
|
costPrice: 0, // 成本价
|
||||||
|
barCode: '', // 商品条码
|
||||||
|
picUrl: '', // 图片地址
|
||||||
|
stock: 0, // 库存
|
||||||
|
weight: 0, // 商品重量
|
||||||
|
volume: 0, // 商品体积
|
||||||
|
firstBrokeragePrice: 0, // 一级分销的佣金
|
||||||
|
secondBrokeragePrice: 0, // 二级分销的佣金
|
||||||
|
},
|
||||||
|
]); // 批量添加时的临时数据
|
||||||
|
|
||||||
|
/** 商品图预览 */
|
||||||
|
const imagePreview = (imgUrl: string) => {
|
||||||
|
// TODO @puhui999: 图片预览
|
||||||
|
// createImageViewer({
|
||||||
|
// zIndex: 9_999_999,
|
||||||
|
// urlList: [imgUrl],
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 批量添加 */
|
||||||
|
const batchAdd = () => {
|
||||||
|
validateProperty();
|
||||||
|
formData.value!.skus!.forEach((item: MallSpuApi.Sku) => {
|
||||||
|
// copyValueToTarget(item, skuList.value[0]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 校验商品属性属性值 */
|
||||||
|
const validateProperty = () => {
|
||||||
|
// 校验商品属性属性值是否为空,有一个为空都不给过
|
||||||
|
const warningInfo = '存在属性属性值为空,请先检查完善属性值后重试!!!';
|
||||||
|
for (const item of props.propertyList as PropertyAndValues[]) {
|
||||||
|
if (!item.values || isEmpty(item.values)) {
|
||||||
|
message.warning(warningInfo);
|
||||||
|
throw new Error(warningInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 删除 sku */
|
||||||
|
const deleteSku = (row: MallSpuApi.Sku) => {
|
||||||
|
const index = formData.value!.skus!.findIndex(
|
||||||
|
// 直接把列表转成字符串比较
|
||||||
|
(sku: MallSpuApi.Sku) =>
|
||||||
|
JSON.stringify(sku.properties) === JSON.stringify(row.properties),
|
||||||
|
);
|
||||||
|
formData.value!.skus!.splice(index, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const tableHeaders = ref<{ label: string; prop: string }[]>([]); // 多属性表头
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存时,每个商品规格的表单要校验下。例如说,销售金额最低是 0.01 这种。
|
||||||
|
*/
|
||||||
|
const validateSku = () => {
|
||||||
|
validateProperty();
|
||||||
|
let warningInfo = '请检查商品各行相关属性配置,';
|
||||||
|
let validate = true; // 默认通过
|
||||||
|
for (const sku of formData.value!.skus!) {
|
||||||
|
// 作为活动组件的校验
|
||||||
|
for (const rule of props?.ruleConfig as RuleConfig[]) {
|
||||||
|
const arg = getValue(sku, rule.name);
|
||||||
|
if (!rule.rule(arg)) {
|
||||||
|
validate = false; // 只要有一个不通过则直接不通过
|
||||||
|
warningInfo += rule.message;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 只要有一个不通过则结束后续的校验
|
||||||
|
if (!validate) {
|
||||||
|
message.warning(warningInfo);
|
||||||
|
throw new Error(warningInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getValue = (obj: any, arg: string): unknown => {
|
||||||
|
const keys = arg.split('.');
|
||||||
|
let value: any = obj;
|
||||||
|
for (const key of keys) {
|
||||||
|
if (value && typeof value === 'object' && key in value) {
|
||||||
|
value = value[key];
|
||||||
|
} else {
|
||||||
|
value = undefined;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择时触发
|
||||||
|
* @param records 传递过来的选中的 sku 是一个数组
|
||||||
|
*/
|
||||||
|
const handleSelectionChange = ({ records }: { records: MallSpuApi.Sku[] }) => {
|
||||||
|
emit('selectionChange', records);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将传进来的值赋值给 skuList
|
||||||
|
*/
|
||||||
|
watch(
|
||||||
|
() => props.propFormData,
|
||||||
|
(data) => {
|
||||||
|
if (!data) return;
|
||||||
|
formData.value = data;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deep: true,
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/** 生成表数据 */
|
||||||
|
const generateTableData = (propertyList: PropertyAndValues[]) => {
|
||||||
|
// 构建数据结构
|
||||||
|
const propertyValues = propertyList.map((item: PropertyAndValues) =>
|
||||||
|
(item.values || []).map((v: { id: number; name: string }) => ({
|
||||||
|
propertyId: item.id,
|
||||||
|
propertyName: item.name,
|
||||||
|
valueId: v.id,
|
||||||
|
valueName: v.name,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
const buildSkuList = build(propertyValues);
|
||||||
|
// 如果回显的 sku 属性和添加的属性不一致则重置 skus 列表
|
||||||
|
if (!validateData(propertyList)) {
|
||||||
|
// 如果不一致则重置表数据,默认添加新的属性重新生成 sku 列表
|
||||||
|
formData.value!.skus = [];
|
||||||
|
}
|
||||||
|
for (const item of buildSkuList) {
|
||||||
|
const row = {
|
||||||
|
properties: Array.isArray(item) ? item : [item], // 如果只有一个属性的话返回的是一个 property 对象
|
||||||
|
price: 0,
|
||||||
|
marketPrice: 0,
|
||||||
|
costPrice: 0,
|
||||||
|
barCode: '',
|
||||||
|
picUrl: '',
|
||||||
|
stock: 0,
|
||||||
|
weight: 0,
|
||||||
|
volume: 0,
|
||||||
|
firstBrokeragePrice: 0,
|
||||||
|
secondBrokeragePrice: 0,
|
||||||
|
};
|
||||||
|
// 如果存在属性相同的 sku 则不做处理
|
||||||
|
const index = formData.value!.skus!.findIndex(
|
||||||
|
(sku: MallSpuApi.Sku) =>
|
||||||
|
JSON.stringify(sku.properties) === JSON.stringify(row.properties),
|
||||||
|
);
|
||||||
|
if (index !== -1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
formData.value!.skus!.push(row);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成 skus 前置校验
|
||||||
|
*/
|
||||||
|
const validateData = (propertyList: PropertyAndValues[]): boolean => {
|
||||||
|
const skuPropertyIds: number[] = [];
|
||||||
|
formData.value!.skus!.forEach((sku: MallSpuApi.Sku) =>
|
||||||
|
sku.properties
|
||||||
|
?.map((property: MallSpuApi.Property) => property.propertyId)
|
||||||
|
?.forEach((propertyId?: number) => {
|
||||||
|
if (
|
||||||
|
propertyId !== undefined &&
|
||||||
|
!skuPropertyIds.indexOf(propertyId) === -1
|
||||||
|
) {
|
||||||
|
skuPropertyIds.push(propertyId);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const propertyIds = propertyList.map((item: PropertyAndValues) => item.id);
|
||||||
|
return skuPropertyIds.length === propertyIds.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 构建所有排列组合 */
|
||||||
|
const build = (
|
||||||
|
propertyValuesList: MallSpuApi.Property[][],
|
||||||
|
): (MallSpuApi.Property | MallSpuApi.Property[])[] => {
|
||||||
|
if (propertyValuesList.length === 0) {
|
||||||
|
return [];
|
||||||
|
} else if (propertyValuesList.length === 1) {
|
||||||
|
return propertyValuesList[0] || [];
|
||||||
|
} else {
|
||||||
|
const result: MallSpuApi.Property[][] = [];
|
||||||
|
const rest = build(propertyValuesList.slice(1));
|
||||||
|
const firstList = propertyValuesList[0];
|
||||||
|
if (!firstList) return [];
|
||||||
|
|
||||||
|
for (const element of firstList) {
|
||||||
|
for (const element_ of rest) {
|
||||||
|
// 第一次不是数组结构,后面的都是数组结构
|
||||||
|
if (Array.isArray(element_)) {
|
||||||
|
result.push([element!, ...(element_ as MallSpuApi.Property[])]);
|
||||||
|
} else {
|
||||||
|
result.push([element!, element_ as MallSpuApi.Property]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 监听属性列表,生成相关参数和表头 */
|
||||||
|
watch(
|
||||||
|
() => props.propertyList as PropertyAndValues[],
|
||||||
|
(propertyList: PropertyAndValues[]) => {
|
||||||
|
// 如果不是多规格则结束
|
||||||
|
if (!formData.value!.specType) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 如果当前组件作为批量添加数据使用,则重置表数据
|
||||||
|
if (props.isBatch) {
|
||||||
|
skuList.value = [
|
||||||
|
{
|
||||||
|
price: 0,
|
||||||
|
marketPrice: 0,
|
||||||
|
costPrice: 0,
|
||||||
|
barCode: '',
|
||||||
|
picUrl: '',
|
||||||
|
stock: 0,
|
||||||
|
weight: 0,
|
||||||
|
volume: 0,
|
||||||
|
firstBrokeragePrice: 0,
|
||||||
|
secondBrokeragePrice: 0,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断代理对象是否为空
|
||||||
|
if (JSON.stringify(propertyList) === '[]') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 重置表头
|
||||||
|
tableHeaders.value = [];
|
||||||
|
// 生成表头
|
||||||
|
propertyList.forEach((item, index) => {
|
||||||
|
// name加属性项index区分属性值
|
||||||
|
tableHeaders.value.push({ prop: `name${index}`, label: item.name });
|
||||||
|
});
|
||||||
|
// 如果回显的 sku 属性和添加的属性一致则不处理
|
||||||
|
if (validateData(propertyList)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 添加新属性没有属性值也不做处理
|
||||||
|
if (propertyList.some((item) => !item.values || isEmpty(item.values))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 生成 table 数据,即 sku 列表
|
||||||
|
generateTableData(propertyList);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deep: true,
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const activitySkuListRef = ref();
|
||||||
|
|
||||||
|
const getSkuTableRef = () => {
|
||||||
|
return activitySkuListRef.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 暴露出生成 sku 方法,给添加属性成功时调用
|
||||||
|
defineExpose({ generateTableData, validateSku, getSkuTableRef });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- 情况一:添加/修改 -->
|
||||||
|
<VxeTable
|
||||||
|
v-if="!isDetail && !isActivityComponent"
|
||||||
|
:data="isBatch ? skuList : formData?.skus || []"
|
||||||
|
border
|
||||||
|
max-height="500"
|
||||||
|
size="small"
|
||||||
|
class="w-full"
|
||||||
|
>
|
||||||
|
<VxeColumn align="center" title="图片" min-width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<ImageUpload
|
||||||
|
v-model:value="row.picUrl"
|
||||||
|
:max-number="1"
|
||||||
|
:max-size="2"
|
||||||
|
:show-description="false"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<template v-if="formData?.specType && !isBatch">
|
||||||
|
<!-- 根据商品属性动态添加 -->
|
||||||
|
<VxeColumn
|
||||||
|
v-for="(item, index) in tableHeaders"
|
||||||
|
:key="index"
|
||||||
|
:title="item.label"
|
||||||
|
align="center"
|
||||||
|
min-width="120"
|
||||||
|
>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="font-bold text-[#40aaff]">
|
||||||
|
{{ row.properties?.[index]?.valueName }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
</template>
|
||||||
|
<VxeColumn align="center" title="商品条码" min-width="168">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<Input v-model:value="row.barCode" class="w-full" />
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn align="center" title="销售价" min-width="168">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="row.price"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
:step="0.1"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn align="center" title="市场价" min-width="168">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="row.marketPrice"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
:step="0.1"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn align="center" title="成本价" min-width="168">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="row.costPrice"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
:step="0.1"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn align="center" title="库存" min-width="168">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<InputNumber v-model:value="row.stock" :min="0" class="w-full" />
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn align="center" title="重量(kg)" min-width="168">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="row.weight"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
:step="0.1"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn align="center" title="体积(m^3)" min-width="168">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="row.volume"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
:step="0.1"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<template v-if="formData?.subCommissionType">
|
||||||
|
<VxeColumn align="center" title="一级返佣(元)" min-width="168">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="row.firstBrokeragePrice"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
:step="0.1"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn align="center" title="二级返佣(元)" min-width="168">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="row.secondBrokeragePrice"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
:step="0.1"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
</template>
|
||||||
|
<VxeColumn
|
||||||
|
v-if="formData?.specType"
|
||||||
|
align="center"
|
||||||
|
fixed="right"
|
||||||
|
title="操作"
|
||||||
|
width="100"
|
||||||
|
>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<Button v-if="isBatch" type="link" size="small" @click="batchAdd">
|
||||||
|
批量添加
|
||||||
|
</Button>
|
||||||
|
<Button v-else type="link" size="small" danger @click="deleteSku(row)">
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
</VxeTable>
|
||||||
|
|
||||||
|
<!-- 情况二:详情 -->
|
||||||
|
<VxeTable
|
||||||
|
v-if="isDetail"
|
||||||
|
ref="activitySkuListRef"
|
||||||
|
:data="formData?.skus || []"
|
||||||
|
border
|
||||||
|
max-height="500"
|
||||||
|
size="small"
|
||||||
|
class="w-full"
|
||||||
|
:checkbox-config="isComponent ? { reserve: true } : undefined"
|
||||||
|
@checkbox-change="handleSelectionChange"
|
||||||
|
@checkbox-all="handleSelectionChange"
|
||||||
|
>
|
||||||
|
<VxeColumn v-if="isComponent" type="checkbox" width="45" />
|
||||||
|
<VxeColumn align="center" title="图片" min-width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<Image
|
||||||
|
v-if="row.picUrl"
|
||||||
|
:src="row.picUrl"
|
||||||
|
class="h-[50px] w-[50px] cursor-pointer"
|
||||||
|
:preview="false"
|
||||||
|
@click="imagePreview(row.picUrl)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<template v-if="formData?.specType && !isBatch">
|
||||||
|
<!-- 根据商品属性动态添加 -->
|
||||||
|
<VxeColumn
|
||||||
|
v-for="(item, index) in tableHeaders"
|
||||||
|
:key="index"
|
||||||
|
:title="item.label"
|
||||||
|
align="center"
|
||||||
|
min-width="80"
|
||||||
|
>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="font-bold text-[#40aaff]">
|
||||||
|
{{ row.properties?.[index]?.valueName }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
</template>
|
||||||
|
<VxeColumn align="center" title="商品条码" min-width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.barCode }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn align="center" title="销售价(元)" min-width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.price }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn align="center" title="市场价(元)" min-width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.marketPrice }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn align="center" title="成本价(元)" min-width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.costPrice }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn align="center" title="库存" min-width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.stock }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn align="center" title="重量(kg)" min-width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.weight }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn align="center" title="体积(m^3)" min-width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.volume }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<template v-if="formData?.subCommissionType">
|
||||||
|
<VxeColumn align="center" title="一级返佣(元)" min-width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.firstBrokeragePrice }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn align="center" title="二级返佣(元)" min-width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.secondBrokeragePrice }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
</template>
|
||||||
|
</VxeTable>
|
||||||
|
|
||||||
|
<!-- 情况三:作为活动组件 -->
|
||||||
|
<VxeTable
|
||||||
|
v-if="isActivityComponent"
|
||||||
|
:data="formData?.skus || []"
|
||||||
|
border
|
||||||
|
max-height="500"
|
||||||
|
size="small"
|
||||||
|
class="w-full"
|
||||||
|
>
|
||||||
|
<VxeColumn v-if="isComponent" type="checkbox" width="45" />
|
||||||
|
<VxeColumn align="center" title="图片" min-width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<Image
|
||||||
|
:src="row.picUrl"
|
||||||
|
class="h-[60px] w-[60px] cursor-pointer"
|
||||||
|
:preview="false"
|
||||||
|
@click="imagePreview(row.picUrl)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<template v-if="formData?.specType">
|
||||||
|
<!-- 根据商品属性动态添加 -->
|
||||||
|
<VxeColumn
|
||||||
|
v-for="(item, index) in tableHeaders"
|
||||||
|
:key="index"
|
||||||
|
:title="item.label"
|
||||||
|
align="center"
|
||||||
|
min-width="80"
|
||||||
|
>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="font-bold text-[#40aaff]">
|
||||||
|
{{ row.properties?.[index]?.valueName }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
</template>
|
||||||
|
<VxeColumn align="center" title="商品条码" min-width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.barCode }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn align="center" title="销售价(元)" min-width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatToFraction(row.price) }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn align="center" title="市场价(元)" min-width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatToFraction(row.marketPrice) }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn align="center" title="成本价(元)" min-width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatToFraction(row.costPrice) }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<VxeColumn align="center" title="库存" min-width="80">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ row.stock }}
|
||||||
|
</template>
|
||||||
|
</VxeColumn>
|
||||||
|
<!-- 方便扩展每个活动配置的属性不一样 -->
|
||||||
|
<slot name="extension"></slot>
|
||||||
|
</VxeTable>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user