!288 feat:【antd】【mall】商城活动优化

Merge pull request !288 from puhui999/dev-mall
This commit is contained in:
芋道源码
2025-12-01 10:54:59 +00:00
committed by Gitee
18 changed files with 149 additions and 59 deletions

View File

@@ -21,6 +21,7 @@ export namespace MallCombinationActivityApi {
limitDuration?: number; // 限制时长 limitDuration?: number; // 限制时长
combinationPrice?: number; // 拼团价格 combinationPrice?: number; // 拼团价格
products: CombinationProduct[]; // 商品列表 products: CombinationProduct[]; // 商品列表
picUrl?: any;
} }
/** 拼团活动所需属性 */ /** 拼团活动所需属性 */

View File

@@ -31,6 +31,7 @@ export namespace MallSeckillActivityApi {
totalStock?: number; // 秒杀总库存 totalStock?: number; // 秒杀总库存
seckillPrice?: number; // 秒杀价格 seckillPrice?: number; // 秒杀价格
products?: SeckillProduct[]; // 秒杀商品列表 products?: SeckillProduct[]; // 秒杀商品列表
picUrl?: any;
} }
} }

View File

@@ -133,6 +133,12 @@ export function useFormSchema(): VbenFormSchema[] {
placeholder: '请输入最大砍价金额', placeholder: '请输入最大砍价金额',
}, },
}, },
{
fieldName: 'spuId',
label: '砍价商品',
component: 'Input',
rules: 'required',
},
]; ];
} }

View File

@@ -14,6 +14,7 @@ import {
updateBargainActivity, updateBargainActivity,
} from '#/api/mall/promotion/bargain/bargainActivity'; } from '#/api/mall/promotion/bargain/bargainActivity';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { SpuShowcase } from '#/views/mall/product/spu/components';
import { useFormSchema } from '../data'; import { useFormSchema } from '../data';
@@ -21,7 +22,7 @@ defineOptions({ name: 'PromotionBargainActivityForm' });
const emit = defineEmits(['success']); const emit = defineEmits(['success']);
const formData = ref<MallBargainActivityApi.BargainActivity>(); const formData = ref<Partial<MallBargainActivityApi.BargainActivity>>({});
const getTitle = computed(() => { const getTitle = computed(() => {
return formData.value?.id return formData.value?.id
? $t('ui.actionTitle.edit', ['砍价活动']) ? $t('ui.actionTitle.edit', ['砍价活动'])
@@ -49,8 +50,11 @@ const [Modal, modalApi] = useVbenModal({
} }
modalApi.lock(); modalApi.lock();
// 提交表单 // 提交表单
const data = const values = await formApi.getValues();
(await formApi.getValues()) as MallBargainActivityApi.BargainActivity; const data = {
...values,
spuId: formData.value.spuId,
} as MallBargainActivityApi.BargainActivity;
try { try {
await (formData.value?.id await (formData.value?.id
? updateBargainActivity(data) ? updateBargainActivity(data)
@@ -65,7 +69,7 @@ const [Modal, modalApi] = useVbenModal({
}, },
async onOpenChange(isOpen: boolean) { async onOpenChange(isOpen: boolean) {
if (!isOpen) { if (!isOpen) {
formData.value = undefined; formData.value = {};
return; return;
} }
// 加载数据 // 加载数据
@@ -86,8 +90,12 @@ const [Modal, modalApi] = useVbenModal({
</script> </script>
<template> <template>
<!-- TODO @puhui999这里缺少商品的选择 -->
<Modal class="w-2/5" :title="getTitle"> <Modal class="w-2/5" :title="getTitle">
<Form class="mx-4" /> <Form class="mx-4">
<!-- 自定义插槽商品选择 -->
<template #spuId>
<SpuShowcase v-model="formData.spuId" :limit="1" />
</template>
</Form>
</Modal> </Modal>
</template> </template>

View File

@@ -106,10 +106,10 @@ export function useFormSchema(): VbenFormSchema[] {
}, },
}, },
{ {
// TODO <!-- TODO @puhui999这里缺少商品的选择 -->
fieldName: 'spuId', fieldName: 'spuId',
label: '拼团商品', label: '拼团商品',
component: 'Input', component: 'Input',
rules: 'required',
}, },
]; ];
} }

View File

@@ -13,13 +13,16 @@ import {
updateCombinationActivity, updateCombinationActivity,
} from '#/api/mall/promotion/combination/combinationActivity'; } from '#/api/mall/promotion/combination/combinationActivity';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { SpuShowcase } from '#/views/mall/product/spu/components';
import { useFormSchema } from '../data'; import { useFormSchema } from '../data';
defineOptions({ name: 'CombinationActivityForm' }); defineOptions({ name: 'CombinationActivityForm' });
const emit = defineEmits(['success']); const emit = defineEmits(['success']);
const formData = ref<MallCombinationActivityApi.CombinationActivity>(); const formData = ref<Partial<MallCombinationActivityApi.CombinationActivity>>(
{},
);
const getTitle = computed(() => { const getTitle = computed(() => {
return formData.value?.id return formData.value?.id
? $t('ui.actionTitle.edit', ['拼团活动']) ? $t('ui.actionTitle.edit', ['拼团活动'])
@@ -47,8 +50,11 @@ const [Modal, modalApi] = useVbenModal({
} }
modalApi.lock(); modalApi.lock();
// 提交表单 // 提交表单
const data = const values = await formApi.getValues();
(await formApi.getValues()) as MallCombinationActivityApi.CombinationActivity; const data = {
...values,
spuId: formData.value.spuId,
} as MallCombinationActivityApi.CombinationActivity;
try { try {
await (formData.value?.id await (formData.value?.id
? updateCombinationActivity(data) ? updateCombinationActivity(data)
@@ -63,7 +69,7 @@ const [Modal, modalApi] = useVbenModal({
}, },
async onOpenChange(isOpen: boolean) { async onOpenChange(isOpen: boolean) {
if (!isOpen) { if (!isOpen) {
formData.value = undefined; formData.value = {};
return; return;
} }
// 加载数据 // 加载数据
@@ -86,6 +92,11 @@ const [Modal, modalApi] = useVbenModal({
<template> <template>
<Modal class="w-3/5" :title="getTitle"> <Modal class="w-3/5" :title="getTitle">
<Form /> <Form>
<!-- 自定义插槽商品选择 -->
<template #spuId>
<SpuShowcase v-model="formData.spuId" :limit="1" />
</template>
</Form>
</Modal> </Modal>
</template> </template>

View File

@@ -111,7 +111,6 @@ function emitActivityChange() {
> >
<Tooltip :title="activity.name"> <Tooltip :title="activity.name">
<div class="relative h-full w-full"> <div class="relative h-full w-full">
<!-- TODO @芋艿 -->
<Image <Image
:src="activity.picUrl" :src="activity.picUrl"
class="h-full w-full rounded-lg object-cover" class="h-full w-full rounded-lg object-cover"

View File

@@ -60,14 +60,10 @@ export function useFormSchema(): VbenFormSchema[] {
rules: 'required', rules: 'required',
defaultValue: PromotionProductScopeEnum.ALL.scope, defaultValue: PromotionProductScopeEnum.ALL.scope,
}, },
// TODO @puhui999 商品选择器优化
{ {
fieldName: 'productSpuIds', fieldName: 'productSpuIds',
label: '商品', label: '商品',
component: 'Input', component: 'Input',
componentProps: {
placeholder: '请选择商品',
},
dependencies: { dependencies: {
triggerFields: ['productScope', 'productScopeValues'], triggerFields: ['productScope', 'productScopeValues'],
show: (model) => show: (model) =>
@@ -84,14 +80,10 @@ export function useFormSchema(): VbenFormSchema[] {
}, },
rules: 'required', rules: 'required',
}, },
// TODO @puhui999 商品分类选择器优化
{ {
fieldName: 'productCategoryIds', fieldName: 'productCategoryIds',
label: '商品分类', label: '商品分类',
component: 'Input', component: 'Input',
componentProps: {
placeholder: '请选择商品分类',
},
dependencies: { dependencies: {
triggerFields: ['productScope', 'productScopeValues'], triggerFields: ['productScope', 'productScopeValues'],
show: (model) => show: (model) =>

View File

@@ -16,11 +16,18 @@ import {
updateCouponTemplate, updateCouponTemplate,
} from '#/api/mall/promotion/coupon/couponTemplate'; } from '#/api/mall/promotion/coupon/couponTemplate';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { ProductCategorySelect } from '#/views/mall/product/category/components';
import { SpuShowcase } from '#/views/mall/product/spu/components';
import { useFormSchema } from '../data'; import { useFormSchema } from '../data';
const emit = defineEmits(['success']); const emit = defineEmits(['success']);
const formData = ref<MallCouponTemplateApi.CouponTemplate>(); const formData = ref<
Partial<MallCouponTemplateApi.CouponTemplate> & {
productCategoryIds?: number | number[];
productSpuIds?: number[];
}
>({});
const getTitle = computed(() => { const getTitle = computed(() => {
return formData.value?.id return formData.value?.id
? $t('ui.actionTitle.edit', ['优惠券模板']) ? $t('ui.actionTitle.edit', ['优惠券模板'])
@@ -64,7 +71,7 @@ const [Modal, modalApi] = useVbenModal({
}, },
async onOpenChange(isOpen: boolean) { async onOpenChange(isOpen: boolean) {
if (!isOpen) { if (!isOpen) {
formData.value = undefined; formData.value = {};
return; return;
} }
// 加载数据 // 加载数据
@@ -75,7 +82,7 @@ const [Modal, modalApi] = useVbenModal({
modalApi.lock(); modalApi.lock();
try { try {
formData.value = await getCouponTemplate(data.id); formData.value = await getCouponTemplate(data.id);
const processedData = await processLoadData(formData.value); const processedData = await processLoadData(formData.value as any);
// 设置到表单 // 设置到表单
await formApi.setValues(processedData); await formApi.setValues(processedData);
} finally { } finally {
@@ -144,6 +151,15 @@ async function processLoadData(
<template> <template>
<Modal :title="getTitle" class="w-2/5"> <Modal :title="getTitle" class="w-2/5">
<Form class="mx-4" /> <Form class="mx-4">
<!-- 自定义插槽商品选择 -->
<template #productSpuIds>
<SpuShowcase v-model="formData.productSpuIds" />
</template>
<!-- 自定义插槽分类选择 -->
<template #productCategoryIds>
<ProductCategorySelect v-model="formData.productCategoryIds" />
</template>
</Form>
</Modal> </Modal>
</template> </template>

View File

@@ -67,8 +67,15 @@ export function useFormSchema(): VbenFormSchema[] {
placeholder: '请输入备注', placeholder: '请输入备注',
rows: 4, rows: 4,
}, },
formItemClass: 'col-span-2',
},
{
fieldName: 'spuIds',
label: '活动商品',
component: 'Input',
rules: 'required',
formItemClass: 'col-span-2',
}, },
// TODO @puhui999少了商品选择~
]; ];
} }

View File

@@ -13,13 +13,18 @@ import {
updateDiscountActivity, updateDiscountActivity,
} from '#/api/mall/promotion/discount/discountActivity'; } from '#/api/mall/promotion/discount/discountActivity';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { SpuShowcase } from '#/views/mall/product/spu/components';
import { useFormSchema } from '../data'; import { useFormSchema } from '../data';
defineOptions({ name: 'DiscountActivityForm' }); defineOptions({ name: 'DiscountActivityForm' });
const emit = defineEmits(['success']); const emit = defineEmits(['success']);
const formData = ref<MallDiscountActivityApi.DiscountActivity>(); const formData = ref<
Partial<MallDiscountActivityApi.DiscountActivity> & {
spuIds?: number[];
}
>({});
const getTitle = computed(() => { const getTitle = computed(() => {
return formData.value?.id return formData.value?.id
? $t('ui.actionTitle.edit', ['限时折扣活动']) ? $t('ui.actionTitle.edit', ['限时折扣活动'])
@@ -69,7 +74,7 @@ const [Modal, modalApi] = useVbenModal({
}, },
async onOpenChange(isOpen: boolean) { async onOpenChange(isOpen: boolean) {
if (!isOpen) { if (!isOpen) {
formData.value = undefined; formData.value = {};
return; return;
} }
// 加载数据 // 加载数据
@@ -91,6 +96,11 @@ const [Modal, modalApi] = useVbenModal({
<template> <template>
<Modal class="w-3/5" :title="getTitle"> <Modal class="w-3/5" :title="getTitle">
<Form /> <Form>
<!-- 自定义插槽商品选择 -->
<template #spuIds>
<SpuShowcase v-model="formData.spuIds" />
</template>
</Form>
</Modal> </Modal>
</template> </template>

View File

@@ -133,10 +133,6 @@ export function useFormSchema(): VbenFormSchema[] {
component: 'Input', component: 'Input',
rules: 'required', rules: 'required',
formItemClass: 'col-span-2', formItemClass: 'col-span-2',
// TODO @puhui999貌似 renderComponentContent 不需要哇?
renderComponentContent: () => ({
default: () => null,
}),
}, },
]; ];
} }

View File

@@ -42,7 +42,6 @@ function handleEdit(row: MallRewardActivityApi.RewardActivity) {
/** 关闭满减送活动 */ /** 关闭满减送活动 */
async function handleClose(row: MallRewardActivityApi.RewardActivity) { async function handleClose(row: MallRewardActivityApi.RewardActivity) {
// TODO @puhui999这个国际化需要加下哈closing、closeSuccess
const hideLoading = message.loading({ const hideLoading = message.loading({
content: $t('ui.actionMessage.closing', [row.name]), content: $t('ui.actionMessage.closing', [row.name]),
duration: 0, duration: 0,

View File

@@ -29,10 +29,10 @@ const emit = defineEmits(['success']);
const formData = ref<Partial<MallRewardActivityApi.RewardActivity>>({ const formData = ref<Partial<MallRewardActivityApi.RewardActivity>>({
conditionType: PromotionConditionTypeEnum.PRICE.type, conditionType: PromotionConditionTypeEnum.PRICE.type,
productScope: PromotionProductScopeEnum.ALL.scope,
rules: [], rules: [],
}); });
// TODO @puhui999点击“编辑”后会出现 Cannot read properties of null (reading 'type') 报错;
const getTitle = computed(() => { const getTitle = computed(() => {
return formData.value?.id return formData.value?.id
? $t('ui.actionTitle.edit', ['满减送']) ? $t('ui.actionTitle.edit', ['满减送'])
@@ -78,6 +78,7 @@ const [Modal, modalApi] = useVbenModal({
switch (data.productScope) { switch (data.productScope) {
case PromotionProductScopeEnum.CATEGORY.scope: { case PromotionProductScopeEnum.CATEGORY.scope: {
const categoryIds = data.productCategoryIds; const categoryIds = data.productCategoryIds;
data.productScopeValues = Array.isArray(categoryIds) data.productScopeValues = Array.isArray(categoryIds)
? categoryIds ? categoryIds
: categoryIds : categoryIds

View File

@@ -8,12 +8,14 @@ import { useVbenModal } from '@vben/common-ui';
import { Button, message } from 'ant-design-vue'; import { Button, message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form'; import { useVbenForm } from '#/adapter/form';
import { getSpu } from '#/api/mall/product/spu';
import { import {
createSeckillActivity, createSeckillActivity,
getSeckillActivity, getSeckillActivity,
updateSeckillActivity, updateSeckillActivity,
} from '#/api/mall/promotion/seckill/seckillActivity'; } from '#/api/mall/promotion/seckill/seckillActivity';
import { $t } from '#/locales'; import { $t } from '#/locales';
import { SpuSkuSelect } from '#/views/mall/product/spu/components';
import { useFormSchema } from '../data'; import { useFormSchema } from '../data';
@@ -31,25 +33,37 @@ const spuId = ref<number>();
const spuName = ref<string>(''); const spuName = ref<string>('');
const skuTableData = ref<any[]>([]); const skuTableData = ref<any[]>([]);
// 选择商品(占位函数,实际需要对接商品选择组件) const spuSkuSelectRef = ref(); // 商品选择弹窗 Ref
/** 打开商品选择弹窗 */
const handleSelectProduct = () => { const handleSelectProduct = () => {
message.info('商品选择功能需要对接商品选择组件'); spuSkuSelectRef.value?.open();
// TODO: 打开商品选择弹窗
// 实际使用时需要:
// 1. 打开商品选择弹窗
// 2. 选择商品后调用以下逻辑设置数据:
// spuId.value = selectedSpu.id;
// spuName.value = selectedSpu.name;
// skuTableData.value = selectedSkus.map(sku => ({
// skuId: sku.id,
// skuName: sku.name || '',
// picUrl: sku.picUrl || selectedSpu.picUrl || '',
// price: sku.price || 0,
// stock: 0,
// seckillPrice: 0,
// }));
}; };
/** 选择商品后的回调 */
async function handleSpuSelected(selectedSpuId: number, skuIds?: number[]) {
const spu = await getSpu(selectedSpuId);
if (!spu) return;
spuId.value = spu.id;
spuName.value = spu.name || '';
// 筛选指定的 SKU
const selectedSkus = skuIds
? spu.skus?.filter((sku) => skuIds.includes(sku.id!))
: spu.skus;
skuTableData.value =
selectedSkus?.map((sku) => ({
skuId: sku.id!,
skuName: sku.name || '',
picUrl: sku.picUrl || spu.picUrl || '',
price: sku.price || 0,
stock: 0,
seckillPrice: 0,
})) || [];
}
// ================= end ================= // ================= end =================
const [Form, formApi] = useVbenForm({ const [Form, formApi] = useVbenForm({
@@ -137,10 +151,30 @@ const [Modal, modalApi] = useVbenModal({
await nextTick(); await nextTick();
await formApi.setValues(formData.value); await formApi.setValues(formData.value);
// TODO: 加载商品和 SKU 信息 // 加载商品和 SKU 信息
// 需要调用商品 API 获取 SPU 详情 if (formData.value.spuId) {
// spuId.value = formData.value.spuId; const spu = await getSpu(formData.value.spuId);
// await loadProductDetails(formData.value.spuId, formData.value.products); if (spu) {
spuId.value = spu.id;
spuName.value = spu.name || '';
// 回填 SKU 配置
const products = formData.value.products || [];
skuTableData.value =
spu.skus
?.filter((sku) => products.some((p) => p.skuId === sku.id))
.map((sku) => {
const product = products.find((p) => p.skuId === sku.id);
return {
skuId: sku.id!,
skuName: sku.name || '',
picUrl: sku.picUrl || spu.picUrl || '',
price: sku.price || 0,
stock: product?.stock || 0,
seckillPrice: (product?.seckillPrice || 0) / 100, // 分转元
};
}) || [];
}
}
} finally { } finally {
modalApi.unlock(); modalApi.unlock();
} }
@@ -154,7 +188,6 @@ const [Modal, modalApi] = useVbenModal({
<Form /> <Form />
<!-- 商品选择区域 --> <!-- 商品选择区域 -->
<!-- TODO @puhui999这里缺少商品的选择 -->
<div class="mt-4"> <div class="mt-4">
<div class="mb-2 flex items-center"> <div class="mb-2 flex items-center">
<span class="text-sm font-medium">秒杀活动商品:</span> <span class="text-sm font-medium">秒杀活动商品:</span>
@@ -218,4 +251,11 @@ const [Modal, modalApi] = useVbenModal({
</div> </div>
</div> </div>
</Modal> </Modal>
<!-- 商品选择器弹窗 -->
<SpuSkuSelect
ref="spuSkuSelectRef"
:is-select-sku="true"
@select="handleSpuSelected"
/>
</template> </template>

View File

@@ -133,7 +133,6 @@ function emitActivityChange() {
class="flex h-[60px] w-[60px] cursor-pointer items-center justify-center rounded-lg border border-dashed border-gray-300 hover:border-blue-400" class="flex h-[60px] w-[60px] cursor-pointer items-center justify-center rounded-lg border border-dashed border-gray-300 hover:border-blue-400"
@click="handleOpenActivitySelect" @click="handleOpenActivitySelect"
> >
<!-- TODO @芋艿等待和 /Users/yunai/Java/yudao-ui-admin-vben-v5/apps/web-antd/src/views/mall/product/spu/components/spu-showcase.vue 进一步统一 -->
<IconifyIcon icon="lucide:plus" class="text-xl text-gray-400" /> <IconifyIcon icon="lucide:plus" class="text-xl text-gray-400" />
</div> </div>
</Tooltip> </Tooltip>

View File

@@ -36,7 +36,9 @@
"downloadTemplateFail": "Download template failed", "downloadTemplateFail": "Download template failed",
"updating": "Updating {0}...", "updating": "Updating {0}...",
"updateSuccess": "Update {0} successfully", "updateSuccess": "Update {0} successfully",
"updateFailed": "Update {0} failed" "updateFailed": "Update {0} failed",
"closing": "Closing {0} ...",
"closeSuccess": "{0} closed successfully"
}, },
"placeholder": { "placeholder": {
"input": "Please enter", "input": "Please enter",

View File

@@ -36,7 +36,9 @@
"downloadTemplateFail": "下载模板失败", "downloadTemplateFail": "下载模板失败",
"updating": "正在更新 {0}...", "updating": "正在更新 {0}...",
"updateSuccess": "更新 {0} 成功", "updateSuccess": "更新 {0} 成功",
"updateFailed": "更新 {0} 失败" "updateFailed": "更新 {0} 失败",
"closing": "正在关闭 {0} ...",
"closeSuccess": "{0} 关闭成功"
}, },
"placeholder": { "placeholder": {
"input": "请输入", "input": "请输入",