feat:【antd】商品积分活动的 review

This commit is contained in:
YunaiV
2025-10-23 12:44:19 +08:00
parent d14177a4c0
commit 74bf240f52
5 changed files with 150 additions and 158 deletions

View File

@@ -1,13 +1,11 @@
import type { VbenFormSchema } from '#/adapter/form'; import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants'; import { DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks'; import { getDictOptions } from '@vben/hooks';
import { $t } from '@vben/locales'; import { fenToYuan } from '@vben/utils';
import { z } from '#/adapter/form'; /** 列表的搜索表单 */
// TODO @AI注释
export function useGridFormSchema(): VbenFormSchema[] { export function useGridFormSchema(): VbenFormSchema[] {
return [ return [
{ {
@@ -23,7 +21,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
]; ];
} }
// TODO @AI注释 /** 列表的表格列 */
export function useGridColumns(): VxeTableGridOptions['columns'] { export function useGridColumns(): VxeTableGridOptions['columns'] {
return [ return [
{ {
@@ -51,19 +49,15 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
field: 'marketPrice', field: 'marketPrice',
title: '原价', title: '原价',
minWidth: 100, minWidth: 100,
formatter: 'formatAmount', formatter: ({ row }) => `${fenToYuan(row.marketPrice)}`,
}, },
{ {
field: 'status', field: 'status',
title: '活动状态', title: '活动状态',
minWidth: 100, minWidth: 100,
align: 'center',
cellRender: { cellRender: {
name: 'CellDict', name: 'CellDict',
props: { props: { type: DICT_TYPE.COMMON_STATUS },
type: DICT_TYPE.COMMON_STATUS,
value: CommonStatusEnum.ENABLE,
},
}, },
}, },
{ {
@@ -80,20 +74,18 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
field: 'redeemedQuantity', field: 'redeemedQuantity',
title: '已兑换数量', title: '已兑换数量',
minWidth: 100, minWidth: 100,
formatter: ({ row }) => { formatter: ({ row }) => {
return (row.totalStock || 0) - (row.stock || 0); return (row.totalStock || 0) - (row.stock || 0);
}, },
}, },
{ {
field: 'createTime', field: 'createTime',
title: $t('common.createTime'), title: '创建时间',
minWidth: 180, minWidth: 180,
formatter: 'formatDateTime', formatter: 'formatDateTime',
}, },
{ {
title: $t('common.action'), title: '操作',
width: 150, width: 150,
fixed: 'right', fixed: 'right',
slots: { default: 'actions' }, slots: { default: 'actions' },
@@ -101,7 +93,7 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
]; ];
} }
// TODO @AI注释下 /** 新增/修改的表单 */
export function useFormSchema(): VbenFormSchema[] { export function useFormSchema(): VbenFormSchema[] {
return [ return [
{ {
@@ -112,9 +104,30 @@ export function useFormSchema(): VbenFormSchema[] {
show: () => false, show: () => false,
}, },
}, },
{
fieldName: 'sort',
label: '排序',
component: 'InputNumber',
componentProps: {
min: 0,
placeholder: '请输入排序',
class: '!w-full',
},
rules: 'required',
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
componentProps: {
placeholder: '请输入备注',
rows: 4,
},
formItemClass: 'col-span-2',
},
{ {
fieldName: 'spuId', fieldName: 'spuId',
label: '积分商城活动商品', label: '活动商品',
component: 'Input', component: 'Input',
rules: 'required', rules: 'required',
formItemClass: 'col-span-2', formItemClass: 'col-span-2',
@@ -122,23 +135,5 @@ export function useFormSchema(): VbenFormSchema[] {
default: () => null, default: () => null,
}), }),
}, },
{
fieldName: 'sort',
label: '排序',
component: 'InputNumber',
componentProps: {
min: 0,
},
rules: z.number().default(0),
},
{
fieldName: 'remark',
label: '备注',
component: 'Textarea',
componentProps: {
rows: 4,
},
formItemClass: 'col-span-2',
},
]; ];
} }

View File

@@ -1,9 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import { computed } from 'vue'; import { Page, useVbenModal } from '@vben/common-ui';
import { confirm, Page, useVbenModal } from '@vben/common-ui';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
@@ -25,112 +23,92 @@ const [FormModal, formModalApi] = useVbenModal({
destroyOnClose: true, destroyOnClose: true,
}); });
// TODO @AI增加注释 /** 刷新表格 */
function handleCreate() {
formModalApi.setData(null).open();
}
// TODO @AI增加注释
function handleEdit(row: any) {
formModalApi.setData(row).open();
}
// TODO @AI增加注释
async function handleClose(row: any) {
await confirm({
title: '提示',
content: '确认关闭该积分商城活动吗?',
});
await closePointActivity(row.id);
// TODO @AI增加 loading
message.success('关闭成功');
gridApi.query();
}
async function handleDelete(row: any) {
await deletePointActivity(row.id);
message.success($t('common.delSuccess'));
gridApi.query();
}
// TODO @AI增加注释
function handleRefresh() { function handleRefresh() {
gridApi.query(); gridApi.query();
} }
// 计算操作按钮 /** 创建积分商城活动 */
// TODO @AI不用方法直接 vue 标签里,写条件 ifShow function handleCreate() {
const getActions = computed(() => (row: any) => { formModalApi.setData(null).open();
const actions: any[] = [ }
{
label: $t('common.edit'), /** 编辑积分商城活动 */
icon: ACTION_ICON.EDIT, function handleEdit(row: any) {
onClick: handleEdit.bind(null, row), formModalApi.setData(row).open();
}, }
];
if (row.status === 0) { /** 关闭积分商城活动 */
actions.push({ async function handleClose(row: any) {
label: '关闭', const hideLoading = message.loading({
icon: ACTION_ICON.CLOSE, content: '正在关闭中...',
danger: true, duration: 0,
popConfirm: { });
title: '确认关闭该积分商城活动吗?', try {
confirm: handleClose.bind(null, row), await closePointActivity(row.id);
}, message.success('关闭成功');
}); handleRefresh();
} else { } finally {
actions.push({ hideLoading();
label: $t('common.delete'), }
icon: ACTION_ICON.DELETE, }
danger: true,
popConfirm: { /** 删除积分商城活动 */
title: $t('ui.actionMessage.deleteConfirm', [row.spuName]), async function handleDelete(row: any) {
confirm: handleDelete.bind(null, row), const hideLoading = message.loading({
}, content: $t('ui.actionMessage.deleting', [row.spuName]),
}); duration: 0,
} });
return actions; try {
}); await deletePointActivity(row.id);
message.success($t('ui.actionMessage.deleteSuccess', [row.spuName]));
handleRefresh();
} finally {
hideLoading();
}
}
// TODO @AI参考 system/notice 补全,简化;
const [Grid, gridApi] = useVbenVxeGrid({ const [Grid, gridApi] = useVbenVxeGrid({
formOptions: { formOptions: {
schema: useGridFormSchema(), schema: useGridFormSchema(),
}, },
gridOptions: { gridOptions: {
columns: useGridColumns(), columns: useGridColumns(),
height: 'auto',
keepSource: true,
proxyConfig: { proxyConfig: {
ajax: { ajax: {
query: async ({ page }, formValues) => { query: async ({ page }, formValues) => {
const params = { return await getPointActivityPage({
pageNo: page.currentPage, pageNo: page.currentPage,
pageSize: page.pageSize, pageSize: page.pageSize,
...formValues, ...formValues,
}; });
const data = await getPointActivityPage(params);
return { items: data.list, total: data.total };
}, },
}, },
}, },
rowConfig: {
keyField: 'id',
isHover: true,
},
toolbarConfig: {
refresh: true,
search: true,
},
} as VxeTableGridOptions, } as VxeTableGridOptions,
}); });
</script> </script>
<template> <template>
<!-- TODO @puhui999不用 description --> <Page auto-content-height>
<Page
description="积分商城活动,用于管理积分兑换商品的配置"
doc-link="https://doc.iocoder.cn/mall/promotion-point/"
title="积分商城活动"
>
<FormModal @success="handleRefresh" /> <FormModal @success="handleRefresh" />
<Grid table-title="积分商城活动列表"> <Grid table-title="积分商城活动列表">
<template #toolbar-tools> <template #toolbar-tools>
<TableAction <TableAction
:actions="[ :actions="[
{ {
label: $t('ui.actionTitle.create', ['积分活动']), label: $t('ui.actionTitle.create', ['积分活动']),
type: 'primary',
icon: ACTION_ICON.ADD, icon: ACTION_ICON.ADD,
onClick: handleCreate, onClick: handleCreate,
}, },
@@ -138,7 +116,38 @@ const [Grid, gridApi] = useVbenVxeGrid({
/> />
</template> </template>
<template #actions="{ row }"> <template #actions="{ row }">
<TableAction :actions="getActions(row)" /> <TableAction
:actions="[
{
label: $t('common.edit'),
type: 'link',
icon: ACTION_ICON.EDIT,
onClick: handleEdit.bind(null, row),
},
{
label: '关闭',
type: 'link',
danger: true,
icon: ACTION_ICON.CLOSE,
ifShow: row.status === 0,
popConfirm: {
title: '确认关闭该积分商城活动吗?',
confirm: handleClose.bind(null, row),
},
},
{
label: $t('common.delete'),
type: 'link',
danger: true,
icon: ACTION_ICON.DELETE,
ifShow: row.status !== 0,
popConfirm: {
title: $t('ui.actionMessage.deleteConfirm', [row.spuName]),
confirm: handleDelete.bind(null, row),
},
},
]"
/>
</template> </template>
</Grid> </Grid>
</Page> </Page>

View File

@@ -25,15 +25,22 @@ import { SpuAndSkuList, SpuSkuSelect } from '../../../components';
import { useFormSchema } from '../data'; import { useFormSchema } from '../data';
const emit = defineEmits(['success']); const emit = defineEmits(['success']);
const formData = ref<MallPointActivityApi.PointActivity>();
const formData = ref<MallPointActivityApi.PointActivity>(); // 用于存储当前编辑的数据 const getTitle = computed(() => {
const isFormUpdate = ref(false); // 是否为编辑模式 return formData.value?.id
? $t('ui.actionTitle.edit', ['积分活动'])
const getTitle = computed(() => : $t('ui.actionTitle.create', ['积分活动']);
isFormUpdate.value ? '编辑积分活动' : '新增积分活动', });
);
const [Form, formApi] = useVbenForm({ const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 100,
},
layout: 'horizontal',
schema: useFormSchema(), schema: useFormSchema(),
showDefaultActions: false, showDefaultActions: false,
}); });
@@ -43,7 +50,6 @@ const [Form, formApi] = useVbenForm({
const spuSkuSelectRef = ref(); // 商品和属性选择 Ref const spuSkuSelectRef = ref(); // 商品和属性选择 Ref
const spuAndSkuListRef = ref(); // SPU 和 SKU 列表组件 Ref const spuAndSkuListRef = ref(); // SPU 和 SKU 列表组件 Ref
// SKU 规则配置
const ruleConfig: RuleConfig[] = [ const ruleConfig: RuleConfig[] = [
{ {
name: 'productConfig.stock', name: 'productConfig.stock',
@@ -60,38 +66,30 @@ const ruleConfig: RuleConfig[] = [
rule: (arg) => arg >= 1, rule: (arg) => arg >= 1,
message: '商品可兑换次数必须大于等于 1 ', message: '商品可兑换次数必须大于等于 1 ',
}, },
]; ]; // SKU 规则配置
const spuList = ref<any[]>([]); // 选择的 SPU 列表 const spuList = ref<any[]>([]); // 选择的 SPU 列表
const spuPropertyList = ref<SpuProperty<any>[]>([]); // SPU 属性列表 const spuPropertyList = ref<SpuProperty<any>[]>([]); // SPU 属性列表
/** /** 打开商品选择器 */
* 打开商品选择器
*/
// TODO @puhui999spuSkuSelectRef.value.open is not a function // TODO @puhui999spuSkuSelectRef.value.open is not a function
function openSpuSelect() { function openSpuSelect() {
spuSkuSelectRef.value.open(); spuSkuSelectRef.value.open();
} }
/** /** 选择商品后的回调 */
* 选择商品后的回调
*/
async function handleSpuSelected(spuId: number, skuIds?: number[]) { async function handleSpuSelected(spuId: number, skuIds?: number[]) {
await formApi.setFieldValue('spuId', spuId); await formApi.setFieldValue('spuId', spuId);
await getSpuDetails(spuId, skuIds); await getSpuDetails(spuId, skuIds);
} }
/** /** 获取 SPU 详情 */
* 获取 SPU 详情
*/
async function getSpuDetails( async function getSpuDetails(
spuId: number, spuId: number,
skuIds?: number[], skuIds?: number[],
products?: MallPointActivityApi.PointProduct[], products?: MallPointActivityApi.PointProduct[],
) { ) {
const spuProperties: SpuProperty<any>[] = [];
const res = await getSpu(spuId); const res = await getSpu(spuId);
if (!res) { if (!res) {
return; return;
} }
@@ -103,7 +101,6 @@ async function getSpuDetails(
skuIds === undefined skuIds === undefined
? res.skus ? res.skus
: res.skus?.filter((sku) => skuIds.includes(sku.id!)); : res.skus?.filter((sku) => skuIds.includes(sku.id!));
// 为每个 SKU 配置积分商城相关的配置 // 为每个 SKU 配置积分商城相关的配置
selectSkus?.forEach((sku: any) => { selectSkus?.forEach((sku: any) => {
let config: MallPointActivityApi.PointProduct = { let config: MallPointActivityApi.PointProduct = {
@@ -113,7 +110,6 @@ async function getSpuDetails(
point: 0, point: 0,
count: 0, count: 0,
}; };
// 如果是编辑模式,回填已有配置 // 如果是编辑模式,回填已有配置
if (products !== undefined) { if (products !== undefined) {
const product = products.find((item) => item.skuId === sku.id); const product = products.find((item) => item.skuId === sku.id);
@@ -122,19 +118,20 @@ async function getSpuDetails(
} }
config = product || config; config = product || config;
} }
sku.productConfig = config; sku.productConfig = config;
}); });
res.skus = selectSkus; res.skus = selectSkus;
const spuProperties: SpuProperty[] = [];
spuProperties.push({ spuProperties.push({
spuId: res.id!, spuId: res.id!,
spuDetail: res, spuDetail: res,
propertyList: getPropertyList(res), propertyList: getPropertyList(res),
}); });
// TODO @puhui999貌似直接 = 下面的,不用 push
spuList.value.push(res); spuList.value.push(res);
// TODO @puhui999貌似直接 = 下面的,不用 push
spuPropertyList.value = spuProperties; spuPropertyList.value = spuProperties;
} }
@@ -146,55 +143,43 @@ const [Modal, modalApi] = useVbenModal({
if (!valid) { if (!valid) {
return; return;
} }
modalApi.lock(); modalApi.lock();
try { try {
// 获取积分商城商品配置 // 获取积分商城商品配置
const products: MallPointActivityApi.PointProduct[] = const products: MallPointActivityApi.PointProduct[] =
spuAndSkuListRef.value?.getSkuConfigs('productConfig') || []; spuAndSkuListRef.value?.getSkuConfigs('productConfig') || [];
// 价格需要转为分 // 价格需要转为分
products.forEach((item) => { products.forEach((item) => {
item.price = convertToInteger(item.price); item.price = convertToInteger(item.price);
}); });
// 提交表单
const data = const data =
(await formApi.getValues()) as MallPointActivityApi.PointActivity; (await formApi.getValues()) as MallPointActivityApi.PointActivity;
data.products = products; data.products = products;
await (formData.value?.id
// 真正提交
await (isFormUpdate.value
? updatePointActivity(data) ? updatePointActivity(data)
: createPointActivity(data)); : createPointActivity(data));
// 关闭并提示
message.success($t('ui.actionMessage.operationSuccess'));
await modalApi.close(); await modalApi.close();
emit('success'); emit('success');
message.success($t('ui.actionMessage.operationSuccess'));
} finally { } finally {
modalApi.unlock(); modalApi.unlock();
} }
}, },
async onOpenChange(isOpen: boolean) { async onOpenChange(isOpen: boolean) {
if (!isOpen) { if (!isOpen) {
// 关闭时清理状态
formData.value = undefined; formData.value = undefined;
isFormUpdate.value = false;
spuList.value = []; spuList.value = [];
spuPropertyList.value = []; spuPropertyList.value = [];
return; return;
} }
// 加载数据
const data = modalApi.getData(); const data = modalApi.getData<MallPointActivityApi.PointActivity>();
if (!data || !data.id) { if (!data || !data.id) {
// 新增模式
isFormUpdate.value = false;
return; return;
} }
// 编辑模式
isFormUpdate.value = true;
modalApi.lock(); modalApi.lock();
try { try {
formData.value = await getPointActivity(data.id); formData.value = await getPointActivity(data.id);
await getSpuDetails( await getSpuDetails(
@@ -202,6 +187,7 @@ const [Modal, modalApi] = useVbenModal({
formData.value.products?.map((sku) => sku.skuId), formData.value.products?.map((sku) => sku.skuId),
formData.value.products, formData.value.products,
); );
// 设置到 values
await formApi.setValues(formData.value); await formApi.setValues(formData.value);
} finally { } finally {
modalApi.unlock(); modalApi.unlock();
@@ -212,11 +198,11 @@ const [Modal, modalApi] = useVbenModal({
<template> <template>
<Modal :title="getTitle" class="w-[70%]"> <Modal :title="getTitle" class="w-[70%]">
<Form> <Form class="mx-4">
<!-- 商品选择 --> <!-- 商品选择 -->
<template #spuId> <template #spuId>
<div class="w-full"> <div class="w-full">
<Button v-if="!isFormUpdate" type="primary" @click="openSpuSelect"> <Button v-if="!formData?.id" type="primary" @click="openSpuSelect">
选择商品 选择商品
</Button> </Button>

View File

@@ -1,5 +1,6 @@
<!-- 积分活动橱窗组件 - 用于装修时展示和选择积分活动 --> <!-- 积分活动橱窗组件 - 用于装修时展示和选择积分活动 -->
<script lang="ts" setup> <script lang="ts" setup>
// TODO @puhui999看看是不是整体优化下代码风格参考别的模块
import type { MallPointActivityApi } from '#/api/mall/promotion/point'; import type { MallPointActivityApi } from '#/api/mall/promotion/point';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';

View File

@@ -1,5 +1,6 @@
<!-- 积分活动表格选择器 --> <!-- 积分活动表格选择器 -->
<script lang="ts" setup> <script lang="ts" setup>
// TODO @puhui999看看是不是整体优化下代码风格参考别的模块
import type { VbenFormSchema } from '#/adapter/form'; import type { VbenFormSchema } from '#/adapter/form';
import type { VxeGridProps } from '#/adapter/vxe-table'; import type { VxeGridProps } from '#/adapter/vxe-table';
import type { MallPointActivityApi } from '#/api/mall/promotion/point'; import type { MallPointActivityApi } from '#/api/mall/promotion/point';