feat:【mall 商城】优惠劵-模版(100% ele)

This commit is contained in:
YunaiV
2025-10-18 11:45:57 +08:00
parent 97e692d2fc
commit 2288748ca8
8 changed files with 458 additions and 463 deletions

View File

@@ -1,223 +0,0 @@
<script lang="ts" setup>
import type { MallCouponTemplateApi } from '#/api/mall/promotion/coupon/couponTemplate';
import { reactive, ref } from 'vue';
import { ContentWrap } from '@vben/common-ui';
import { CouponTemplateTakeTypeEnum, DICT_TYPE } from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
import { IconifyIcon } from '@vben/icons';
import * as CouponTemplateApi from '#/api/mall/promotion/coupon/couponTemplate';
import {
discountFormat,
remainedCountFormat,
takeLimitCountFormat,
validityTypeFormat,
} from '#/views/mall/promotion/coupon/formatter';
defineOptions({ name: 'CouponSelect' });
const props = defineProps<{
multipleSelection?: MallCouponTemplateApi.CouponTemplate[];
takeType: number; // 领取方式
}>();
const emit = defineEmits<{
(
e: 'update:multipleSelection',
v: MallCouponTemplateApi.CouponTemplate[],
): void;
(e: 'change', v: MallCouponTemplateApi.CouponTemplate[]): void;
}>();
const dialogVisible = ref(false); // 弹窗的是否展示
const dialogTitle = ref('选择优惠劵'); // 弹窗的标题
const formLoading = ref(false); // 表单的加载中1修改时的数据加载2提交的按钮禁用
const loading = ref(true); // 列表的加载中
const total = ref(0); // 列表的总页数
const list = ref<MallCouponTemplateApi.CouponTemplate[]>([]); // 字典表格数据
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
name: null,
discountType: null,
canTakeTypes: [CouponTemplateTakeTypeEnum.USER.type], // 只获得直接领取的券
});
const queryFormRef = ref(); // 搜索的表单
const selectedCouponList = ref<MallCouponTemplateApi.CouponTemplate[]>([]); // 选择的数据
/** 查询列表 */
const getList = async () => {
loading.value = true;
try {
// 执行查询
queryParams.canTakeTypes = [props.takeType] as any;
const data = await CouponTemplateApi.getCouponTemplatePage(queryParams);
list.value = data.list;
total.value = data.total;
} finally {
loading.value = false;
}
};
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1;
getList();
};
/** 重置按钮操作 */
const resetQuery = () => {
queryFormRef?.value?.resetFields();
handleQuery();
};
/** 打开弹窗 */
const open = async () => {
dialogVisible.value = true;
resetQuery();
};
defineExpose({ open }); // 提供 open 方法,用于打开弹窗
const handleSelectionChange = (val: MallCouponTemplateApi.CouponTemplate[]) => {
if (props.multipleSelection) {
emit('update:multipleSelection', val);
return;
}
selectedCouponList.value = val;
};
const submitForm = () => {
dialogVisible.value = false;
emit('change', selectedCouponList.value);
};
</script>
<template>
<Dialog v-model="dialogVisible" :title="dialogTitle" width="65%">
<!-- 搜索工作栏 -->
<ContentWrap>
<el-form
ref="queryFormRef"
:inline="true"
:model="queryParams"
class="-mb-15px"
label-width="82px"
>
<el-form-item label="优惠券名称" prop="name">
<el-input
v-model="queryParams.name"
class="!w-240px"
clearable
placeholder="请输入优惠劵名"
@keyup="handleQuery"
/>
</el-form-item>
<el-form-item label="优惠类型" prop="discountType">
<el-select
v-model="queryParams.discountType"
class="!w-240px"
clearable
placeholder="请选择优惠券类型"
>
<el-option
v-for="dict in getDictOptions(
DICT_TYPE.PROMOTION_DISCOUNT_TYPE,
'number',
)"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">
<IconifyIcon class="mr-5px" icon="ep:search" />
搜索
</el-button>
<el-button @click="resetQuery">
<IconifyIcon class="mr-5px" icon="ep:refresh" />
重置
</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 列表 -->
<ContentWrap>
<el-table
v-loading="loading"
:data="list"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="优惠券名称" min-width="140" prop="name" />
<el-table-column label="类型" min-width="80" prop="productScope">
<template #default="scope">
<dict-tag
:type="DICT_TYPE.PROMOTION_PRODUCT_SCOPE"
:value="scope.row.productScope"
/>
</template>
</el-table-column>
<el-table-column label="优惠" min-width="100" prop="discount">
<template #default="scope">
<dict-tag
:type="DICT_TYPE.PROMOTION_DISCOUNT_TYPE"
:value="scope.row.discountType"
/>
{{ discountFormat(scope.row) }}
</template>
</el-table-column>
<el-table-column label="领取方式" min-width="100" prop="takeType">
<template #default="scope">
<dict-tag
:type="DICT_TYPE.PROMOTION_COUPON_TAKE_TYPE"
:value="scope.row.takeType"
/>
</template>
</el-table-column>
<el-table-column
:formatter="validityTypeFormat"
align="center"
label="使用时间"
prop="validityType"
width="185"
/>
<el-table-column align="center" label="发放数量" prop="totalCount" />
<el-table-column
:formatter="remainedCountFormat"
align="center"
label="剩余数量"
prop="totalCount"
/>
<el-table-column
:formatter="takeLimitCountFormat"
align="center"
label="领取上限"
prop="takeLimitCount"
/>
<el-table-column align="center" label="状态" prop="status">
<template #default="scope">
<dict-tag
:type="DICT_TYPE.COMMON_STATUS"
:value="scope.row.status"
/>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<Pagination
v-model:limit="queryParams.pageSize"
v-model:page="queryParams.pageNo"
:total="total"
@pagination="getList"
/>
</ContentWrap>
<template #footer>
<el-button :disabled="formLoading" type="primary" @click="submitForm">
确 定
</el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</Dialog>
</template>

View File

@@ -1,11 +1,17 @@
import type { VbenFormSchema } from '#/adapter/form';
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MallCouponTemplateApi } from '#/api/mall/promotion/coupon/couponTemplate';
import { CommonStatusEnum, DICT_TYPE } from '@vben/constants';
import {
CommonStatusEnum,
CouponTemplateTakeTypeEnum,
CouponTemplateValidityTypeEnum,
DICT_TYPE,
PromotionDiscountTypeEnum,
PromotionProductScopeEnum,
} from '@vben/constants';
import { getDictOptions } from '@vben/hooks';
// 格式化函数移到组件内部实现
import { z } from '#/adapter/form';
import { getRangePickerDefaultProps } from '#/utils';
import {
@@ -40,69 +46,289 @@ export function useFormSchema(): VbenFormSchema[] {
fieldName: 'description',
label: '优惠券描述',
component: 'Textarea',
componentProps: {
placeholder: '请输入优惠券描述',
},
},
// TODO @霖:不同的优惠,不同的选择
{
fieldName: 'productScope',
label: '优惠类型',
label: '优惠类型',
component: 'RadioGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.PROMOTION_PRODUCT_SCOPE, 'number'),
},
rules: 'required',
defaultValue: PromotionProductScopeEnum.ALL.scope,
},
// TODO @puhui999 商品选择器优化
{
fieldName: 'productSpuIds',
label: '商品',
component: 'Input',
componentProps: {
placeholder: '请选择商品',
},
dependencies: {
triggerFields: ['productScope', 'productScopeValues'],
show: (model) =>
model.productScope === PromotionProductScopeEnum.SPU.scope,
trigger(values, form) {
// 当加载已有数据时,根据 productScopeValues 设置 productSpuIds
if (
values.productScope === PromotionProductScopeEnum.SPU.scope &&
values.productScopeValues
) {
form.setFieldValue('productSpuIds', values.productScopeValues);
}
},
},
rules: 'required',
},
// TODO @puhui999 商品分类选择器优化
{
fieldName: 'productCategoryIds',
label: '商品分类',
component: 'Input',
componentProps: {
placeholder: '请选择商品分类',
},
dependencies: {
triggerFields: ['productScope', 'productScopeValues'],
show: (model) =>
model.productScope === PromotionProductScopeEnum.CATEGORY.scope,
trigger(values, form) {
// 当加载已有数据时,根据 productScopeValues 设置 productCategoryIds
if (
values.productScope === PromotionProductScopeEnum.CATEGORY.scope &&
values.productScopeValues
) {
const categoryIds = values.productScopeValues;
// 单选时使用数组不能反显,取第一个元素
form.setFieldValue(
'productCategoryIds',
Array.isArray(categoryIds) && categoryIds.length > 0
? categoryIds[0]
: categoryIds,
);
}
},
},
rules: 'required',
},
{
fieldName: 'discountType',
label: '优惠类型',
component: 'RadioGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.PROMOTION_DISCOUNT_TYPE, 'number'),
},
rules: 'required',
defaultValue: PromotionDiscountTypeEnum.PRICE.type,
},
{
fieldName: 'discountPrice',
label: '优惠券面额',
component: 'InputNumber',
componentProps: {
min: 0,
precision: 2,
placeholder: '请输入优惠金额,单位:元',
addonAfter: '元',
controlsPosition: 'right',
class: '!w-full',
},
dependencies: {
triggerFields: ['discountType'],
show: (model) =>
model.discountType === PromotionDiscountTypeEnum.PRICE.type,
},
rules: 'required',
},
{
fieldName: 'discountPercent',
label: '优惠券折扣',
component: 'InputNumber',
componentProps: {
min: 1,
max: 9.9,
precision: 1,
placeholder: '优惠券折扣不能小于 1 折,且不可大于 9.9 折',
addonAfter: '折',
controlsPosition: 'right',
class: '!w-full',
},
dependencies: {
triggerFields: ['discountType'],
show: (model) =>
model.discountType === PromotionDiscountTypeEnum.PERCENT.type,
},
rules: 'required',
},
{
fieldName: 'discountLimitPrice',
label: '最多优惠',
component: 'InputNumber',
componentProps: {
min: 0,
precision: 2,
placeholder: '请输入最多优惠',
addonAfter: '元',
controlsPosition: 'right',
class: '!w-full',
},
dependencies: {
triggerFields: ['discountType'],
show: (model) =>
model.discountType === PromotionDiscountTypeEnum.PERCENT.type,
},
rules: 'required',
},
{
fieldName: 'usePrice',
label: '满多少元可以使用',
component: 'InputNumber',
componentProps: {
min: 0,
precision: 2,
placeholder: '无门槛请设为 0',
addonAfter: '元',
controlsPosition: 'right',
class: '!w-full',
},
rules: 'required',
},
{
fieldName: 'takeType',
label: '领取方式',
component: 'Select',
component: 'RadioGroup',
componentProps: {
placeholder: '请选择领取方式',
options: getDictOptions(DICT_TYPE.PROMOTION_COUPON_TAKE_TYPE, 'number'),
},
rules: 'required',
},
// TODO @xingu不同的有效期不同的类型
{
fieldName: 'validityType',
label: '有效期类型',
component: 'Select',
componentProps: {
placeholder: '请选择有效期类型',
options: getDictOptions(
DICT_TYPE.PROMOTION_COUPON_TEMPLATE_VALIDITY_TYPE,
'number',
),
},
rules: 'required',
defaultValue: CouponTemplateTakeTypeEnum.USER.type,
},
{
fieldName: 'totalCount',
label: '发放数量',
component: 'InputNumber',
componentProps: {
min: 0,
placeholder: '请输入发放数量',
min: -1,
placeholder: '发放数量,没有之后不能领取或发放,-1 为不限制',
addonAfter: '张',
controlsPosition: 'right',
class: '!w-full',
},
dependencies: {
triggerFields: ['takeType'],
show: (model) =>
model.takeType === CouponTemplateTakeTypeEnum.USER.type,
},
rules: 'required',
},
{
fieldName: 'takeLimitCount',
label: '领取上限',
label: '每人限领个数',
component: 'InputNumber',
componentProps: {
min: 0,
placeholder: '请输入领取上限',
min: -1,
placeholder: '设置为 -1 时,可无限领取',
addonAfter: '张',
controlsPosition: 'right',
class: '!w-full',
},
dependencies: {
triggerFields: ['takeType'],
show: (model) => model.takeType === 1,
},
rules: 'required',
},
{
fieldName: 'status',
label: '优惠券状态',
fieldName: 'validityType',
label: '有效期类型',
component: 'RadioGroup',
componentProps: {
options: getDictOptions(DICT_TYPE.COMMON_STATUS, 'number'),
options: getDictOptions(
DICT_TYPE.PROMOTION_COUPON_TEMPLATE_VALIDITY_TYPE,
'number',
),
},
defaultValue: CouponTemplateValidityTypeEnum.DATE.type,
rules: 'required',
},
{
fieldName: 'validTimes',
label: '固定日期',
component: 'RangePicker',
componentProps: {
...getRangePickerDefaultProps(),
valueFormat: 'x',
},
dependencies: {
triggerFields: ['validityType'],
show: (model) =>
model.validityType === CouponTemplateValidityTypeEnum.DATE.type,
},
rules: 'required',
},
{
fieldName: 'fixedStartTerm',
label: '领取日期',
component: 'InputNumber',
componentProps: {
min: 0,
placeholder: '第 0 为今天生效',
addonBefore: '第',
addonAfter: '天',
controlsPosition: 'right',
class: '!w-full',
},
dependencies: {
triggerFields: ['validityType'],
show: (model) =>
model.validityType === CouponTemplateValidityTypeEnum.TERM.type,
},
rules: 'required',
},
{
fieldName: 'fixedEndTerm',
component: 'InputNumber',
componentProps: {
min: 0,
placeholder: '请输入结束天数',
addonBefore: '至',
addonAfter: '天有效',
controlsPosition: 'right',
class: '!w-full',
},
dependencies: {
triggerFields: ['validityType'],
show: (model) =>
model.validityType === CouponTemplateValidityTypeEnum.TERM.type,
},
rules: 'required',
},
{
fieldName: 'productScopeValues',
component: 'Input',
dependencies: {
triggerFields: ['productScope', 'productSpuIds', 'productCategoryIds'],
show: () => false,
trigger(values, form) {
switch (values.productScope) {
case PromotionProductScopeEnum.CATEGORY.scope: {
const categoryIds = Array.isArray(values.productCategoryIds)
? values.productCategoryIds
: [values.productCategoryIds];
form.setFieldValue('productScopeValues', categoryIds);
break;
}
case PromotionProductScopeEnum.SPU.scope: {
form.setFieldValue('productScopeValues', values.productSpuIds);
break;
}
}
},
},
rules: z.number().default(CommonStatusEnum.ENABLE),
},
];
}
@@ -115,7 +341,7 @@ export function useGridFormSchema(): VbenFormSchema[] {
label: '优惠券名称',
component: 'Input',
componentProps: {
placeholder: '请输入优惠券名称',
placeholder: '请输入优惠劵名',
clearable: true,
},
},
@@ -152,9 +378,13 @@ export function useGridFormSchema(): VbenFormSchema[] {
}
/** 列表的字段 */
export function useGridColumns(): VxeTableGridOptions['columns'] {
export function useGridColumns(
onStatusChange?: (
newStatus: number,
row: MallCouponTemplateApi.CouponTemplate,
) => PromiseLike<boolean | undefined>,
): VxeTableGridOptions['columns'] {
return [
{ type: 'checkbox', width: 40 },
{
field: 'name',
title: '优惠券名称',
@@ -231,7 +461,15 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
field: 'status',
title: '状态',
minWidth: 100,
slots: { default: 'status' },
align: 'center',
cellRender: {
attrs: { beforeChange: onStatusChange },
name: 'CellSwitch',
props: {
activeValue: CommonStatusEnum.ENABLE,
inactiveValue: CommonStatusEnum.DISABLE,
},
},
},
{
field: 'createTime',

View File

@@ -2,13 +2,11 @@
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
import type { MallCouponTemplateApi } from '#/api/mall/promotion/coupon/couponTemplate';
import { ref } from 'vue';
import { DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { confirm, DocAlert, Page, useVbenModal } from '@vben/common-ui';
import { CommonStatusEnum } from '@vben/constants';
import { $t } from '@vben/locales';
import { ElLoading, ElMessage, ElSwitch } from 'element-plus';
import { ElLoading, ElMessage } from 'element-plus';
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
import {
@@ -48,7 +46,7 @@ async function handleDelete(row: MallCouponTemplateApi.CouponTemplate) {
text: $t('ui.actionMessage.deleting', [row.name]),
});
try {
await deleteCouponTemplate(row.id as number);
await deleteCouponTemplate(row.id!);
ElMessage.success($t('ui.actionMessage.deleteSuccess', [row.name]));
handleRefresh();
} finally {
@@ -56,33 +54,30 @@ async function handleDelete(row: MallCouponTemplateApi.CouponTemplate) {
}
}
const checkedIds = ref<number[]>([]);
function handleRowCheckboxChange({
records,
}: {
records: MallCouponTemplateApi.CouponTemplate[];
}) {
checkedIds.value = records.map((item) => item.id!);
}
/** 优惠券模板状态修改 */
async function handleStatusChange(row: MallCouponTemplateApi.CouponTemplate) {
const text = row.status === CommonStatusEnum.ENABLE ? '启用' : '停用';
const loadingInstance = ElLoading.service({
text: `正在${text}优惠券模板...`,
async function handleStatusChange(
newStatus: number,
row: MallCouponTemplateApi.CouponTemplate,
): Promise<boolean | undefined> {
return new Promise((resolve, reject) => {
confirm({
content: `你要将${row.name}的状态切换为【${newStatus === CommonStatusEnum.ENABLE ? '启用' : '停用'}】吗?`,
})
.then(async () => {
// 更新优惠券模板状态
const res = await updateCouponTemplateStatus(row.id!, newStatus);
if (res) {
// 提示并返回成功
ElMessage.success($t('ui.actionMessage.operationSuccess'));
resolve(true);
} else {
reject(new Error('更新失败'));
}
})
.catch(() => {
reject(new Error('取消操作'));
});
});
try {
await updateCouponTemplateStatus(row.id as number, row.status as 0 | 1);
ElMessage.success(`${text}成功`);
} catch {
// 异常时,需要将 row.status 状态重置回之前的
row.status =
row.status === CommonStatusEnum.ENABLE
? CommonStatusEnum.DISABLE
: CommonStatusEnum.ENABLE;
} finally {
loadingInstance.close();
}
}
const [Grid, gridApi] = useVbenVxeGrid({
@@ -90,7 +85,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
schema: useGridFormSchema(),
},
gridOptions: {
columns: useGridColumns(),
columns: useGridColumns(handleStatusChange),
height: 'auto',
keepSource: true,
proxyConfig: {
@@ -113,10 +108,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
search: true,
},
} as VxeTableGridOptions<MallCouponTemplateApi.CouponTemplate>,
gridEvents: {
checkboxAll: handleRowCheckboxChange,
checkboxChange: handleRowCheckboxChange,
},
});
</script>
@@ -144,15 +135,6 @@ const [Grid, gridApi] = useVbenVxeGrid({
]"
/>
</template>
<template #status="{ row }">
<ElSwitch
v-model:checked="row.status"
:checked-value="CommonStatusEnum.ENABLE"
:un-checked-value="CommonStatusEnum.DISABLE"
@change="handleStatusChange(row)"
/>
</template>
<template #actions="{ row }">
<TableAction
:actions="[

View File

@@ -4,6 +4,8 @@ import type { MallCouponTemplateApi } from '#/api/mall/promotion/coupon/couponTe
import { computed, ref } from 'vue';
import { useVbenModal } from '@vben/common-ui';
import { CouponTemplateTakeTypeEnum } from '@vben/constants';
import { convertToInteger, formatToFraction } from '@vben/utils';
import { ElMessage } from 'element-plus';
@@ -31,7 +33,7 @@ const [Form, formApi] = useVbenForm({
class: 'w-full',
},
formItemClass: 'col-span-2',
labelWidth: 80,
labelWidth: 120,
},
layout: 'horizontal',
schema: useFormSchema(),
@@ -46,8 +48,8 @@ const [Modal, modalApi] = useVbenModal({
}
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as MallCouponTemplateApi.CouponTemplate;
const formValues = (await formApi.getValues()) as any;
const data = await processSubmitData(formValues);
try {
await (formData.value?.id
? updateCouponTemplate(data)
@@ -73,17 +75,75 @@ const [Modal, modalApi] = useVbenModal({
modalApi.lock();
try {
formData.value = await getCouponTemplate(data.id);
// 设置到 values
await formApi.setValues(formData.value);
const processedData = await processLoadData(formData.value);
// 设置到表单
await formApi.setValues(processedData);
} finally {
modalApi.unlock();
}
},
});
/** 处理提交数据 */
async function processSubmitData(
formValues: any,
): Promise<MallCouponTemplateApi.CouponTemplate> {
return {
...formValues,
// 金额转换:元转分
discountPrice: convertToInteger(formValues.discountPrice),
discountPercent:
formValues.discountPercent === undefined
? undefined
: formValues.discountPercent * 10,
discountLimitPrice: convertToInteger(formValues.discountLimitPrice),
usePrice: convertToInteger(formValues.usePrice),
// 处理有效期时间
validStartTime:
formValues.validTimes && formValues.validTimes.length === 2
? formValues.validTimes[0]
: undefined,
validEndTime:
formValues.validTimes && formValues.validTimes.length === 2
? formValues.validTimes[1]
: undefined,
// 处理发放数量和限领数量
totalCount:
formValues.takeType === CouponTemplateTakeTypeEnum.USER.type
? formValues.totalCount
: -1,
takeLimitCount:
formValues.takeType === CouponTemplateTakeTypeEnum.USER.type
? formValues.takeLimitCount
: -1,
};
}
/** 处理加载的数据 */
async function processLoadData(
data: MallCouponTemplateApi.CouponTemplate,
): Promise<any> {
return {
...data,
// 金额转换:分转元
discountPrice: formatToFraction(data.discountPrice),
discountPercent:
data.discountPercent === undefined
? undefined
: data.discountPercent / 10,
discountLimitPrice: formatToFraction(data.discountLimitPrice),
usePrice: formatToFraction(data.usePrice),
// 处理有效期时间
validTimes:
data.validStartTime && data.validEndTime
? [data.validStartTime, data.validEndTime]
: [],
};
}
</script>
<template>
<Modal class="w-2/5" :title="getTitle">
<Modal :title="getTitle" class="w-2/5">
<Form class="mx-4" />
</Modal>
</template>