feat:【ele/antd】discountActivity todo 优化

pull/312/head
puhui999 2025-12-28 18:34:28 +08:00
parent e6327ae9da
commit 6353f0a8e9
4 changed files with 586 additions and 47 deletions

View File

@ -73,7 +73,6 @@ export function useFormSchema(): VbenFormSchema[] {
fieldName: 'spuIds',
label: '活动商品',
component: 'Input',
rules: 'required',
formItemClass: 'col-span-2',
},
];

View File

@ -1,30 +1,52 @@
<script lang="ts" setup>
import type { MallSpuApi } from '#/api/mall/product/spu';
import type { MallDiscountActivityApi } from '#/api/mall/promotion/discount/discountActivity';
import type {
PropertyAndValues,
RuleConfig,
SpuProperty,
} from '#/views/mall/product/spu/components';
import { computed, ref } from 'vue';
import { computed, nextTick, ref } from 'vue';
import { useVbenForm, useVbenModal } from '@vben/common-ui';
import {
convertToInteger,
erpCalculatePercentage,
formatToFraction,
yuanToFen,
} from '@vben/utils';
import { message } from 'ant-design-vue';
import { Button, InputNumber, message } from 'ant-design-vue';
import { VxeColumn } from '#/adapter/vxe-table';
import { getSpuDetailList } from '#/api/mall/product/spu';
import {
createDiscountActivity,
getDiscountActivity,
updateDiscountActivity,
} from '#/api/mall/promotion/discount/discountActivity';
import { $t } from '#/locales';
import { SpuShowcase } from '#/views/mall/product/spu/components';
import {
getPropertyList,
SpuAndSkuList,
SpuSkuSelect,
} from '#/views/mall/product/spu/components';
import { useFormSchema } from '../data';
defineOptions({ name: 'DiscountActivityForm' });
const emit = defineEmits(['success']);
const formData = ref<
Partial<MallDiscountActivityApi.DiscountActivity> & {
spuIds?: number[];
}
>({});
/** 折扣类型枚举 */
const PromotionDiscountTypeEnum = {
PRICE: { type: 1 }, //
PERCENT: { type: 2 }, //
};
// ================= =================
const formData = ref<Partial<MallDiscountActivityApi.DiscountActivity>>({});
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['限时折扣活动'])
@ -44,27 +66,203 @@ const [Form, formApi] = useVbenForm({
showDefaultActions: false,
});
// ================= =================
/** SKU 扩展类型 */
interface SkuExtension extends MallSpuApi.Sku {
productConfig: MallDiscountActivityApi.DiscountProduct;
}
/** SPU 扩展类型 */
interface SpuExtension extends MallSpuApi.Spu {
skus?: SkuExtension[];
}
const spuSelectRef = ref(); // Ref
const spuAndSkuListRef = ref(); // SKU Ref
const spuList = ref<SpuExtension[]>([]); // SPU
const spuPropertyList = ref<SpuProperty<SpuExtension>[]>([]); // SPU
const spuIdList = ref<number[]>([]); // SPU ID
/** SKU 校验规则配置 */
const ruleConfig: RuleConfig[] = [
{
name: 'productConfig.discountPrice',
rule: (arg) => arg > 0,
message: '商品优惠金额不能为 0 ',
},
];
/** 打开商品选择弹窗 */
function openSpuSelect() {
spuSelectRef.value?.open();
}
/** 选择商品后的回调 */
function handleSpuSelected(spuId: number, skuIds?: number[]) {
getSpuDetails(spuId, skuIds);
}
/** 获取 SPU 详情 */
async function getSpuDetails(
spuId: number,
skuIdArr?: number[],
products?: MallDiscountActivityApi.DiscountProduct[],
type?: string,
) {
// SPU
if (spuIdList.value.includes(spuId)) {
if (type !== 'load') {
message.error('数据重复选择!');
}
return;
}
spuIdList.value.push(spuId);
const res = (await getSpuDetailList([spuId])) as SpuExtension[];
if (res.length === 0) {
return;
}
const spu = res[0]!;
// SKU
const selectSkus =
skuIdArr === undefined
? spu.skus
: spu.skus?.filter((sku) => skuIdArr.includes(sku.id!));
// SKU
selectSkus?.forEach((sku) => {
let config: MallDiscountActivityApi.DiscountProduct = {
skuId: sku.id!,
spuId: spu.id!,
discountType: 1,
discountPercent: 0,
discountPrice: 0,
};
// 使
if (products !== undefined) {
const product = products.find((item) => item.skuId === sku.id);
if (product) {
//
config = {
...product,
discountPercent: Number(formatToFraction(product.discountPercent)),
discountPrice: Number(formatToFraction(product.discountPrice)),
};
}
}
(sku as SkuExtension).productConfig = config;
});
spu.skus = selectSkus as SkuExtension[];
spuPropertyList.value.push({
spuId: spu.id!,
spuDetail: spu,
propertyList: getPropertyList(spu) as PropertyAndValues[],
});
spuList.value.push(spu);
}
/** 删除 SPU */
function handleDeleteSpu(spuId: number) {
const spuIndex = spuIdList.value.indexOf(spuId);
if (spuIndex !== -1) {
spuIdList.value.splice(spuIndex, 1);
}
const propertyIndex = spuPropertyList.value.findIndex(
(item) => item.spuId === spuId,
);
if (propertyIndex !== -1) {
spuPropertyList.value.splice(propertyIndex, 1);
}
const listIndex = spuList.value.findIndex((item) => item.id === spuId);
if (listIndex !== -1) {
spuList.value.splice(listIndex, 1);
}
}
/** 处理 SKU 优惠金额变动 */
function handleSkuDiscountPriceChange(row: SkuExtension) {
if (row.productConfig.discountPrice <= 0) {
return;
}
//
row.productConfig.discountType = PromotionDiscountTypeEnum.PRICE.type;
//
const price = typeof row.price === 'number' ? row.price : Number(row.price);
const percent = erpCalculatePercentage(
price - yuanToFen(row.productConfig.discountPrice),
price,
);
row.productConfig.discountPercent =
typeof percent === 'number' ? percent : Number(percent);
}
/** 处理 SKU 折扣百分比变动 */
function handleSkuDiscountPercentChange(row: SkuExtension) {
if (
row.productConfig.discountPercent <= 0 ||
row.productConfig.discountPercent >= 100
) {
return;
}
//
row.productConfig.discountType = PromotionDiscountTypeEnum.PERCENT.type;
//
const price = typeof row.price === 'number' ? row.price : Number(row.price);
row.productConfig.discountPrice = Number(
formatToFraction(price - price * (row.productConfig.discountPercent / 100)),
);
}
/** 重置表单 */
async function resetForm() {
spuList.value = [];
spuPropertyList.value = [];
spuIdList.value = [];
formData.value = {};
await nextTick();
await formApi.resetForm();
}
// ================= =================
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
//
const data =
(await formApi.getValues()) as MallDiscountActivityApi.DiscountActivity;
//
if (!data.products) {
data.products = [];
//
if (spuList.value.length === 0) {
message.warning('请选择活动商品');
return;
}
modalApi.lock();
try {
//
const products = structuredClone(
spuAndSkuListRef.value?.getSkuConfigs('productConfig') || [],
) as MallDiscountActivityApi.DiscountProduct[];
//
products.forEach((item) => {
item.discountPercent = convertToInteger(item.discountPercent);
item.discountPrice = convertToInteger(item.discountPrice);
});
const data = structuredClone(
await formApi.getValues(),
) as MallDiscountActivityApi.DiscountActivity;
data.products = products;
//
await (formData.value?.id
? updateDiscountActivity(data)
: createDiscountActivity(data));
//
await modalApi.close();
emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
@ -74,19 +272,45 @@ const [Modal, modalApi] = useVbenModal({
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = {};
await resetForm();
return;
}
//
const data = modalApi.getData<MallDiscountActivityApi.DiscountActivity>();
if (!data || !data.id) {
return;
}
modalApi.lock();
try {
formData.value = await getDiscountActivity(data.id);
// values
await formApi.setValues(formData.value);
const activityData = await getDiscountActivity(data.id);
formData.value = activityData;
//
if (activityData.products && activityData.products.length > 0) {
// spuId
const spuProductsMap = new Map<
number,
MallDiscountActivityApi.DiscountProduct[]
>();
for (const product of activityData.products) {
const spuId = product.spuId;
if (!spuProductsMap.has(spuId)) {
spuProductsMap.set(spuId, []);
}
spuProductsMap.get(spuId)!.push(product);
}
// SPU
for (const [spuId, products] of spuProductsMap) {
const skuIdArr = products.map((p) => p.skuId);
await getSpuDetails(spuId, skuIdArr, products, 'load');
}
}
//
await formApi.setValues(activityData);
} finally {
modalApi.unlock();
}
@ -95,12 +319,59 @@ const [Modal, modalApi] = useVbenModal({
</script>
<template>
<Modal class="w-3/5" :title="getTitle">
<Modal class="w-[70%]" :title="getTitle">
<Form>
<!-- 自定义插槽商品选择 -->
<template #spuIds>
<SpuShowcase v-model="formData.spuIds" />
<div class="w-full">
<Button class="mb-4" @click="openSpuSelect"></Button>
<SpuAndSkuList
ref="spuAndSkuListRef"
:deletable="true"
:rule-config="ruleConfig"
:spu-list="spuList"
:spu-property-list-p="spuPropertyList"
@delete="handleDeleteSpu"
>
<!-- 扩展列限时折扣活动特有配置 -->
<template #default>
<VxeColumn align="center" min-width="168" title="优惠金额(元)">
<template #default="{ row }">
<InputNumber
v-model:value="row.productConfig.discountPrice"
:max="Number(formatToFraction(row.price))"
:min="0"
:precision="2"
:step="0.1"
class="w-full"
@change="handleSkuDiscountPriceChange(row)"
/>
</template>
</VxeColumn>
<VxeColumn align="center" min-width="168" title="折扣百分比(%)">
<template #default="{ row }">
<InputNumber
v-model:value="row.productConfig.discountPercent"
:max="100"
:min="0"
:precision="2"
:step="0.1"
class="w-full"
@change="handleSkuDiscountPercentChange(row)"
/>
</template>
</VxeColumn>
</template>
</SpuAndSkuList>
</div>
</template>
</Form>
</Modal>
<!-- 商品选择弹窗 -->
<SpuSkuSelect
ref="spuSelectRef"
:is-select-sku="true"
@select="handleSpuSelected"
/>
</template>

View File

@ -75,7 +75,6 @@ export function useFormSchema(): VbenFormSchema[] {
fieldName: 'spuIds',
label: '活动商品',
component: 'Input',
rules: 'required',
formItemClass: 'col-span-2',
},
];

View File

@ -1,30 +1,52 @@
<script lang="ts" setup>
import type { MallSpuApi } from '#/api/mall/product/spu';
import type { MallDiscountActivityApi } from '#/api/mall/promotion/discount/discountActivity';
import type {
PropertyAndValues,
RuleConfig,
SpuProperty,
} from '#/views/mall/product/spu/components';
import { computed, ref } from 'vue';
import { computed, nextTick, ref } from 'vue';
import { useVbenForm, useVbenModal } from '@vben/common-ui';
import {
convertToInteger,
erpCalculatePercentage,
formatToFraction,
yuanToFen,
} from '@vben/utils';
import { ElMessage } from 'element-plus';
import { ElButton, ElInputNumber, ElMessage } from 'element-plus';
import { VxeColumn } from '#/adapter/vxe-table';
import { getSpuDetailList } from '#/api/mall/product/spu';
import {
createDiscountActivity,
getDiscountActivity,
updateDiscountActivity,
} from '#/api/mall/promotion/discount/discountActivity';
import { $t } from '#/locales';
import { SpuShowcase } from '#/views/mall/product/spu/components';
import {
getPropertyList,
SpuAndSkuList,
SpuSkuSelect,
} from '#/views/mall/product/spu/components';
import { useFormSchema } from '../data';
defineOptions({ name: 'DiscountActivityForm' });
/** 折扣类型枚举 */
const PromotionDiscountTypeEnum = {
PRICE: { type: 1 }, //
PERCENT: { type: 2 }, //
};
const emit = defineEmits(['success']);
const formData = ref<
Partial<MallDiscountActivityApi.DiscountActivity> & {
spuIds?: number[];
}
>({});
// ================= =================
const formData = ref<Partial<MallDiscountActivityApi.DiscountActivity>>({});
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['限时折扣活动'])
@ -44,28 +66,203 @@ const [Form, formApi] = useVbenForm({
showDefaultActions: false,
});
// TODO @puhui999antd ele
// ================= =================
/** SKU 扩展类型 */
interface SkuExtension extends MallSpuApi.Sku {
productConfig: MallDiscountActivityApi.DiscountProduct;
}
/** SPU 扩展类型 */
interface SpuExtension extends MallSpuApi.Spu {
skus?: SkuExtension[];
}
const spuSelectRef = ref(); // Ref
const spuAndSkuListRef = ref(); // SKU Ref
const spuList = ref<SpuExtension[]>([]); // SPU
const spuPropertyList = ref<SpuProperty<SpuExtension>[]>([]); // SPU
const spuIdList = ref<number[]>([]); // SPU ID
/** SKU 校验规则配置 */
const ruleConfig: RuleConfig[] = [
{
name: 'productConfig.discountPrice',
rule: (arg) => arg > 0,
message: '商品优惠金额不能为 0 ',
},
];
/** 打开商品选择弹窗 */
function openSpuSelect() {
spuSelectRef.value?.open();
}
/** 选择商品后的回调 */
function handleSpuSelected(spuId: number, skuIds?: number[]) {
getSpuDetails(spuId, skuIds);
}
/** 获取 SPU 详情 */
async function getSpuDetails(
spuId: number,
skuIdArr?: number[],
products?: MallDiscountActivityApi.DiscountProduct[],
type?: string,
) {
// SPU
if (spuIdList.value.includes(spuId)) {
if (type !== 'load') {
ElMessage.error('数据重复选择!');
}
return;
}
spuIdList.value.push(spuId);
const res = (await getSpuDetailList([spuId])) as SpuExtension[];
if (res.length === 0) {
return;
}
const spu = res[0]!;
// SKU
const selectSkus =
skuIdArr === undefined
? spu.skus
: spu.skus?.filter((sku) => skuIdArr.includes(sku.id!));
// SKU
selectSkus?.forEach((sku) => {
let config: MallDiscountActivityApi.DiscountProduct = {
skuId: sku.id!,
spuId: spu.id!,
discountType: 1,
discountPercent: 0,
discountPrice: 0,
};
// 使
if (products !== undefined) {
const product = products.find((item) => item.skuId === sku.id);
if (product) {
//
config = {
...product,
discountPercent: Number(formatToFraction(product.discountPercent)),
discountPrice: Number(formatToFraction(product.discountPrice)),
};
}
}
(sku as SkuExtension).productConfig = config;
});
spu.skus = selectSkus as SkuExtension[];
spuPropertyList.value.push({
spuId: spu.id!,
spuDetail: spu,
propertyList: getPropertyList(spu) as PropertyAndValues[],
});
spuList.value.push(spu);
}
/** 删除 SPU */
function handleDeleteSpu(spuId: number) {
const spuIndex = spuIdList.value.indexOf(spuId);
if (spuIndex !== -1) {
spuIdList.value.splice(spuIndex, 1);
}
const propertyIndex = spuPropertyList.value.findIndex(
(item) => item.spuId === spuId,
);
if (propertyIndex !== -1) {
spuPropertyList.value.splice(propertyIndex, 1);
}
const listIndex = spuList.value.findIndex((item) => item.id === spuId);
if (listIndex !== -1) {
spuList.value.splice(listIndex, 1);
}
}
/** 处理 SKU 优惠金额变动 */
function handleSkuDiscountPriceChange(row: SkuExtension) {
if (row.productConfig.discountPrice <= 0) {
return;
}
//
row.productConfig.discountType = PromotionDiscountTypeEnum.PRICE.type;
//
const price = typeof row.price === 'number' ? row.price : Number(row.price);
const percent = erpCalculatePercentage(
price - yuanToFen(row.productConfig.discountPrice),
price,
);
row.productConfig.discountPercent =
typeof percent === 'number' ? percent : Number(percent);
}
/** 处理 SKU 折扣百分比变动 */
function handleSkuDiscountPercentChange(row: SkuExtension) {
if (
row.productConfig.discountPercent <= 0 ||
row.productConfig.discountPercent >= 100
) {
return;
}
//
row.productConfig.discountType = PromotionDiscountTypeEnum.PERCENT.type;
//
const price = typeof row.price === 'number' ? row.price : Number(row.price);
row.productConfig.discountPrice = Number(
formatToFraction(price - price * (row.productConfig.discountPercent / 100)),
);
}
/** 重置表单 */
async function resetForm() {
spuList.value = [];
spuPropertyList.value = [];
spuIdList.value = [];
formData.value = {};
await nextTick();
await formApi.resetForm();
}
// ================= =================
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
//
const data =
(await formApi.getValues()) as MallDiscountActivityApi.DiscountActivity;
//
if (!data.products) {
data.products = [];
//
if (spuList.value.length === 0) {
ElMessage.warning('请选择活动商品');
return;
}
modalApi.lock();
try {
//
const products = structuredClone(
spuAndSkuListRef.value?.getSkuConfigs('productConfig') || [],
) as MallDiscountActivityApi.DiscountProduct[];
//
products.forEach((item) => {
item.discountPercent = convertToInteger(item.discountPercent);
item.discountPrice = convertToInteger(item.discountPrice);
});
const data = structuredClone(
await formApi.getValues(),
) as MallDiscountActivityApi.DiscountActivity;
data.products = products;
//
await (formData.value?.id
? updateDiscountActivity(data)
: createDiscountActivity(data));
//
await modalApi.close();
emit('success');
ElMessage.success($t('ui.actionMessage.operationSuccess'));
@ -75,19 +272,45 @@ const [Modal, modalApi] = useVbenModal({
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
formData.value = {};
await resetForm();
return;
}
//
const data = modalApi.getData<MallDiscountActivityApi.DiscountActivity>();
if (!data || !data.id) {
return;
}
modalApi.lock();
try {
formData.value = await getDiscountActivity(data.id);
// values
await formApi.setValues(formData.value);
const activityData = await getDiscountActivity(data.id);
formData.value = activityData;
//
if (activityData.products && activityData.products.length > 0) {
// spuId
const spuProductsMap = new Map<
number,
MallDiscountActivityApi.DiscountProduct[]
>();
for (const product of activityData.products) {
const spuId = product.spuId;
if (!spuProductsMap.has(spuId)) {
spuProductsMap.set(spuId, []);
}
spuProductsMap.get(spuId)!.push(product);
}
// SPU
for (const [spuId, products] of spuProductsMap) {
const skuIdArr = products.map((p) => p.skuId);
await getSpuDetails(spuId, skuIdArr, products, 'load');
}
}
//
await formApi.setValues(activityData);
} finally {
modalApi.unlock();
}
@ -96,12 +319,59 @@ const [Modal, modalApi] = useVbenModal({
</script>
<template>
<Modal class="w-3/5" :title="getTitle">
<Modal class="w-[70%]" :title="getTitle">
<Form>
<!-- 自定义插槽商品选择 -->
<template #spuIds>
<SpuShowcase v-model="formData.spuIds" />
<div class="w-full">
<ElButton class="mb-4" @click="openSpuSelect"></ElButton>
<SpuAndSkuList
ref="spuAndSkuListRef"
:deletable="true"
:rule-config="ruleConfig"
:spu-list="spuList"
:spu-property-list-p="spuPropertyList"
@delete="handleDeleteSpu"
>
<!-- 扩展列限时折扣活动特有配置 -->
<template #default>
<VxeColumn align="center" min-width="168" title="优惠金额(元)">
<template #default="{ row }">
<ElInputNumber
v-model="row.productConfig.discountPrice"
:max="Number(formatToFraction(row.price))"
:min="0"
:precision="2"
:step="0.1"
class="w-full"
@change="handleSkuDiscountPriceChange(row)"
/>
</template>
</VxeColumn>
<VxeColumn align="center" min-width="168" title="折扣百分比(%)">
<template #default="{ row }">
<ElInputNumber
v-model="row.productConfig.discountPercent"
:max="100"
:min="0"
:precision="2"
:step="0.1"
class="w-full"
@change="handleSkuDiscountPercentChange(row)"
/>
</template>
</VxeColumn>
</template>
</SpuAndSkuList>
</div>
</template>
</Form>
</Modal>
<!-- 商品选择弹窗 -->
<SpuSkuSelect
ref="spuSelectRef"
:is-select-sku="true"
@select="handleSpuSelected"
/>
</template>