!312 feat:【ele/antd】mall todo 优化
Merge pull request !312 from puhui999/dev-mall
This commit is contained in:
@@ -100,7 +100,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
toolbarConfig: {
|
toolbarConfig: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
} as VxeTableGridOptions<SystemSocialUserApi.SocialUser>,
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/** 解绑账号 */
|
/** 解绑账号 */
|
||||||
|
|||||||
@@ -73,7 +73,6 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
fieldName: 'spuIds',
|
fieldName: 'spuIds',
|
||||||
label: '活动商品',
|
label: '活动商品',
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
rules: 'required',
|
|
||||||
formItemClass: 'col-span-2',
|
formItemClass: 'col-span-2',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,30 +1,52 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { MallSpuApi } from '#/api/mall/product/spu';
|
||||||
import type { MallDiscountActivityApi } from '#/api/mall/promotion/discount/discountActivity';
|
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 { 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 {
|
import {
|
||||||
createDiscountActivity,
|
createDiscountActivity,
|
||||||
getDiscountActivity,
|
getDiscountActivity,
|
||||||
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 {
|
||||||
|
getPropertyList,
|
||||||
|
SpuAndSkuList,
|
||||||
|
SpuSkuSelect,
|
||||||
|
} 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<
|
|
||||||
Partial<MallDiscountActivityApi.DiscountActivity> & {
|
/** 折扣类型枚举 */
|
||||||
spuIds?: number[];
|
const PromotionDiscountTypeEnum = {
|
||||||
}
|
PRICE: { type: 1 }, // 满减
|
||||||
>({});
|
PERCENT: { type: 2 }, // 折扣
|
||||||
|
};
|
||||||
|
|
||||||
|
// ================= 表单相关 =================
|
||||||
|
const formData = ref<Partial<MallDiscountActivityApi.DiscountActivity>>({});
|
||||||
const getTitle = computed(() => {
|
const getTitle = computed(() => {
|
||||||
return formData.value?.id
|
return formData.value?.id
|
||||||
? $t('ui.actionTitle.edit', ['限时折扣活动'])
|
? $t('ui.actionTitle.edit', ['限时折扣活动'])
|
||||||
@@ -44,27 +66,203 @@ const [Form, formApi] = useVbenForm({
|
|||||||
showDefaultActions: false,
|
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({
|
const [Modal, modalApi] = useVbenModal({
|
||||||
async onConfirm() {
|
async onConfirm() {
|
||||||
const { valid } = await formApi.validate();
|
const { valid } = await formApi.validate();
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
modalApi.lock();
|
|
||||||
// 提交表单
|
|
||||||
const data =
|
|
||||||
(await formApi.getValues()) as MallDiscountActivityApi.DiscountActivity;
|
|
||||||
|
|
||||||
// 确保必要的默认值
|
// 校验是否选择了商品
|
||||||
if (!data.products) {
|
if (spuList.value.length === 0) {
|
||||||
data.products = [];
|
message.warning('请选择活动商品');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
modalApi.lock();
|
||||||
try {
|
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
|
await (formData.value?.id
|
||||||
? updateDiscountActivity(data)
|
? updateDiscountActivity(data)
|
||||||
: createDiscountActivity(data));
|
: createDiscountActivity(data));
|
||||||
// 关闭并提示
|
|
||||||
await modalApi.close();
|
await modalApi.close();
|
||||||
emit('success');
|
emit('success');
|
||||||
message.success($t('ui.actionMessage.operationSuccess'));
|
message.success($t('ui.actionMessage.operationSuccess'));
|
||||||
@@ -74,19 +272,45 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
},
|
},
|
||||||
async onOpenChange(isOpen: boolean) {
|
async onOpenChange(isOpen: boolean) {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
formData.value = {};
|
await resetForm();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载数据
|
// 加载数据
|
||||||
const data = modalApi.getData<MallDiscountActivityApi.DiscountActivity>();
|
const data = modalApi.getData<MallDiscountActivityApi.DiscountActivity>();
|
||||||
if (!data || !data.id) {
|
if (!data || !data.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
modalApi.lock();
|
modalApi.lock();
|
||||||
try {
|
try {
|
||||||
formData.value = await getDiscountActivity(data.id);
|
const activityData = await getDiscountActivity(data.id);
|
||||||
// 设置到 values
|
formData.value = activityData;
|
||||||
await formApi.setValues(formData.value);
|
|
||||||
|
// 加载商品详情
|
||||||
|
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 {
|
} finally {
|
||||||
modalApi.unlock();
|
modalApi.unlock();
|
||||||
}
|
}
|
||||||
@@ -95,12 +319,59 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Modal class="w-3/5" :title="getTitle">
|
<Modal class="w-[70%]" :title="getTitle">
|
||||||
<Form>
|
<Form>
|
||||||
<!-- 自定义插槽:商品选择 -->
|
<!-- 自定义插槽:商品选择 -->
|
||||||
<template #spuIds>
|
<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>
|
</template>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<!-- 商品选择弹窗 -->
|
||||||
|
<SpuSkuSelect
|
||||||
|
ref="spuSelectRef"
|
||||||
|
:is-select-sku="true"
|
||||||
|
@select="handleSpuSelected"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
componentProps: {
|
componentProps: {
|
||||||
showTime: true,
|
showTime: true,
|
||||||
format: 'YYYY-MM-DD HH:mm:ss',
|
format: 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
valueFormat: 'x',
|
||||||
placeholder: [
|
placeholder: [
|
||||||
$t('utils.rangePicker.beginTime'),
|
$t('utils.rangePicker.beginTime'),
|
||||||
$t('utils.rangePicker.endTime'),
|
$t('utils.rangePicker.endTime'),
|
||||||
@@ -217,13 +218,15 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
},
|
},
|
||||||
rules: 'required',
|
rules: 'required',
|
||||||
},
|
},
|
||||||
// TODO @puhui999:1)新增时:一直报:“请输入优惠设置”;2)修改老数据,出现报“请求参数类型错误:50.00”;
|
|
||||||
{
|
{
|
||||||
fieldName: 'rules',
|
fieldName: 'rules',
|
||||||
label: '优惠设置',
|
label: '优惠设置',
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
formItemClass: 'items-start',
|
formItemClass: 'items-start',
|
||||||
rules: 'required',
|
rules: z
|
||||||
|
.array(z.any())
|
||||||
|
.min(1, { message: '请添加至少一条优惠规则' })
|
||||||
|
.default([]),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'productScopeValues', // 隐藏字段:用于自动同步 productScopeValues
|
fieldName: 'productScopeValues', // 隐藏字段:用于自动同步 productScopeValues
|
||||||
|
|||||||
@@ -8,10 +8,9 @@ import {
|
|||||||
PromotionConditionTypeEnum,
|
PromotionConditionTypeEnum,
|
||||||
PromotionProductScopeEnum,
|
PromotionProductScopeEnum,
|
||||||
} from '@vben/constants';
|
} from '@vben/constants';
|
||||||
import { convertToInteger, formatToFraction } from '@vben/utils';
|
import { cloneDeep, convertToInteger, formatToFraction } from '@vben/utils';
|
||||||
|
|
||||||
import { message } from 'ant-design-vue';
|
import { message } from 'ant-design-vue';
|
||||||
import dayjs from 'dayjs';
|
|
||||||
|
|
||||||
import { useVbenForm } from '#/adapter/form';
|
import { useVbenForm } from '#/adapter/form';
|
||||||
import {
|
import {
|
||||||
@@ -53,6 +52,8 @@ const [Form, formApi] = useVbenForm({
|
|||||||
|
|
||||||
const [Modal, modalApi] = useVbenModal({
|
const [Modal, modalApi] = useVbenModal({
|
||||||
async onConfirm() {
|
async onConfirm() {
|
||||||
|
// 在验证前同步 formData.rules 到表单中
|
||||||
|
await formApi.setFieldValue('rules', formData.value.rules || []);
|
||||||
const { valid } = await formApi.validate();
|
const { valid } = await formApi.validate();
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
return;
|
return;
|
||||||
@@ -61,18 +62,24 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
// 提交表单
|
// 提交表单
|
||||||
try {
|
try {
|
||||||
const values = await formApi.getValues();
|
const values = await formApi.getValues();
|
||||||
const data = { ...formData.value, ...values };
|
// 使用 formData.value 作为基础,确保 rules 来自 formData
|
||||||
|
const data = { ...values, ...formData.value };
|
||||||
if (data.startAndEndTime && Array.isArray(data.startAndEndTime)) {
|
if (data.startAndEndTime && Array.isArray(data.startAndEndTime)) {
|
||||||
data.startTime = data.startAndEndTime[0];
|
data.startTime = data.startAndEndTime[0];
|
||||||
data.endTime = data.startAndEndTime[1];
|
data.endTime = data.startAndEndTime[1];
|
||||||
delete data.startAndEndTime;
|
delete data.startAndEndTime;
|
||||||
}
|
}
|
||||||
data.rules?.forEach((item: any) => {
|
// 深拷贝 rules 避免修改原始数据
|
||||||
|
const rules = cloneDeep(
|
||||||
|
data.rules,
|
||||||
|
) as unknown as MallRewardActivityApi.RewardRule[];
|
||||||
|
rules.forEach((item: any) => {
|
||||||
item.discountPrice = convertToInteger(item.discountPrice || 0);
|
item.discountPrice = convertToInteger(item.discountPrice || 0);
|
||||||
if (data.conditionType === PromotionConditionTypeEnum.PRICE.type) {
|
if (data.conditionType === PromotionConditionTypeEnum.PRICE.type) {
|
||||||
item.limit = convertToInteger(item.limit || 0);
|
item.limit = convertToInteger(item.limit || 0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
data.rules = rules;
|
||||||
await (data.id
|
await (data.id
|
||||||
? updateRewardActivity(data as MallRewardActivityApi.RewardActivity)
|
? updateRewardActivity(data as MallRewardActivityApi.RewardActivity)
|
||||||
: createRewardActivity(data as MallRewardActivityApi.RewardActivity));
|
: createRewardActivity(data as MallRewardActivityApi.RewardActivity));
|
||||||
@@ -97,9 +104,10 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
modalApi.lock();
|
modalApi.lock();
|
||||||
try {
|
try {
|
||||||
const result = await getReward(data.id);
|
const result = await getReward(data.id);
|
||||||
|
// valueFormat: 'x' 配置下,直接使用时间戳
|
||||||
result.startAndEndTime = [
|
result.startAndEndTime = [
|
||||||
result.startTime ? dayjs(result.startTime) : undefined,
|
result.startTime ? String(result.startTime) : undefined,
|
||||||
result.endTime ? dayjs(result.endTime) : undefined,
|
result.endTime ? String(result.endTime) : undefined,
|
||||||
] as any[];
|
] as any[];
|
||||||
result.rules?.forEach((item: any) => {
|
result.rules?.forEach((item: any) => {
|
||||||
item.discountPrice = formatToFraction(item.discountPrice || 0);
|
item.discountPrice = formatToFraction(item.discountPrice || 0);
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const props = withDefaults(defineProps<CropperAvatarProps>(), {
|
|||||||
width: 200,
|
width: 200,
|
||||||
value: '',
|
value: '',
|
||||||
showBtn: true,
|
showBtn: true,
|
||||||
btnProps: () => ({}),
|
btnProps: () => ({}) as any,
|
||||||
btnText: '',
|
btnText: '',
|
||||||
uploadApi: () => Promise.resolve(),
|
uploadApi: () => Promise.resolve(),
|
||||||
size: 5,
|
size: 5,
|
||||||
@@ -27,14 +27,10 @@ const props = withDefaults(defineProps<CropperAvatarProps>(), {
|
|||||||
const emit = defineEmits(['update:value', 'change']);
|
const emit = defineEmits(['update:value', 'change']);
|
||||||
|
|
||||||
const sourceValue = ref(props.value || '');
|
const sourceValue = ref(props.value || '');
|
||||||
// TODO @puhui999:这个有办法去掉么?
|
|
||||||
const prefixCls = 'cropper-avatar';
|
|
||||||
const [CropperModal, modalApi] = useVbenModal({
|
const [CropperModal, modalApi] = useVbenModal({
|
||||||
connectedComponent: cropperModal,
|
connectedComponent: cropperModal,
|
||||||
});
|
});
|
||||||
|
|
||||||
const getClass = computed(() => [prefixCls]);
|
|
||||||
|
|
||||||
const getWidth = computed(() => `${`${props.width}`.replace(/px/, '')}px`);
|
const getWidth = computed(() => `${`${props.width}`.replace(/px/, '')}px`);
|
||||||
|
|
||||||
const getIconWidth = computed(
|
const getIconWidth = computed(
|
||||||
@@ -74,34 +70,41 @@ defineExpose({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- TODO @puhui999:html 部分,看看有没办法和 web-antd/src/components/cropper/cropper-avatar.vue 风格更接近 -->
|
|
||||||
<!-- 头像容器 -->
|
<!-- 头像容器 -->
|
||||||
<div :class="getClass" :style="getStyle">
|
<div class="inline-block text-center" :style="getStyle">
|
||||||
<!-- 图片包装器 -->
|
<!-- 图片包装器 -->
|
||||||
<div
|
<div
|
||||||
:class="`${prefixCls}-image-wrapper`"
|
class="group relative cursor-pointer overflow-hidden rounded-full border border-gray-200 bg-white"
|
||||||
:style="getImageWrapperStyle"
|
:style="getImageWrapperStyle"
|
||||||
@click="openModal"
|
@click="openModal"
|
||||||
>
|
>
|
||||||
<!-- 遮罩层 -->
|
<!-- 遮罩层 -->
|
||||||
<div :class="`${prefixCls}-image-mask`" :style="getImageWrapperStyle">
|
<div
|
||||||
|
class="duration-400 absolute inset-0 flex cursor-pointer items-center justify-center rounded-full bg-black bg-opacity-40 opacity-0 transition-opacity group-hover:opacity-100"
|
||||||
|
:style="getImageWrapperStyle"
|
||||||
|
>
|
||||||
<span
|
<span
|
||||||
:style="{
|
:style="{
|
||||||
...getImageWrapperStyle,
|
...getImageWrapperStyle,
|
||||||
width: `${getIconWidth}`,
|
width: getIconWidth,
|
||||||
height: `${getIconWidth}`,
|
height: getIconWidth,
|
||||||
lineHeight: `${getIconWidth}`,
|
lineHeight: getIconWidth,
|
||||||
}"
|
}"
|
||||||
class="icon-[ant-design--cloud-upload-outlined] text-[#d6d6d6]"
|
class="icon-[ant-design--cloud-upload-outlined] text-gray-400"
|
||||||
></span>
|
></span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 头像图片 -->
|
<!-- 头像图片 -->
|
||||||
<img v-if="sourceValue" :src="sourceValue" alt="avatar" />
|
<img
|
||||||
|
v-if="sourceValue"
|
||||||
|
:src="sourceValue"
|
||||||
|
alt="avatar"
|
||||||
|
class="h-full w-full object-cover"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- 上传按钮 -->
|
<!-- 上传按钮 -->
|
||||||
<ElButton
|
<ElButton
|
||||||
v-if="showBtn"
|
v-if="showBtn"
|
||||||
:class="`${prefixCls}-upload-btn`"
|
class="mx-auto mt-2"
|
||||||
@click="openModal"
|
@click="openModal"
|
||||||
v-bind="btnProps"
|
v-bind="btnProps"
|
||||||
>
|
>
|
||||||
@@ -116,50 +119,3 @@ defineExpose({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
/* TODO @puhui999:要类似 web-antd/src/components/cropper/cropper-avatar.vue 减少 scss,通过 tindwind 么? */
|
|
||||||
.cropper-avatar {
|
|
||||||
display: inline-block;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
&-image-wrapper {
|
|
||||||
overflow: hidden;
|
|
||||||
cursor: pointer;
|
|
||||||
background: #fff;
|
|
||||||
border: 1px solid #eee;
|
|
||||||
border-radius: 50%;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-image-mask {
|
|
||||||
position: absolute;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: inherit;
|
|
||||||
height: inherit;
|
|
||||||
cursor: pointer;
|
|
||||||
background: rgb(0 0 0 / 40%);
|
|
||||||
border: inherit;
|
|
||||||
border-radius: inherit;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.4s;
|
|
||||||
|
|
||||||
::v-deep(svg) {
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-image-mask:hover {
|
|
||||||
opacity: 40;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-upload-btn {
|
|
||||||
margin: 10px auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ const cropper = ref<CropperType>();
|
|||||||
let scaleX = 1;
|
let scaleX = 1;
|
||||||
let scaleY = 1;
|
let scaleY = 1;
|
||||||
|
|
||||||
const prefixCls = 'cropper-am';
|
|
||||||
const [Modal, modalApi] = useVbenModal({
|
const [Modal, modalApi] = useVbenModal({
|
||||||
onConfirm: handleOk,
|
onConfirm: handleOk,
|
||||||
onOpenChange(isOpen) {
|
onOpenChange(isOpen) {
|
||||||
@@ -120,11 +119,34 @@ async function handleOk() {
|
|||||||
:title="$t('ui.cropper.modalTitle')"
|
:title="$t('ui.cropper.modalTitle')"
|
||||||
class="w-[800px]"
|
class="w-[800px]"
|
||||||
>
|
>
|
||||||
<div :class="prefixCls">
|
<div class="flex">
|
||||||
<!-- 左侧区域 -->
|
<!-- 左侧区域 -->
|
||||||
<div :class="`${prefixCls}-left`" class="w-full">
|
<div class="h-[340px] w-[55%]">
|
||||||
<!-- 裁剪器容器 -->
|
<!-- 裁剪器容器 -->
|
||||||
<div :class="`${prefixCls}-cropper`">
|
<div
|
||||||
|
class="h-[300px] bg-[#eee]"
|
||||||
|
style="
|
||||||
|
background-image:
|
||||||
|
linear-gradient(
|
||||||
|
45deg,
|
||||||
|
rgb(0 0 0 / 25%) 25%,
|
||||||
|
transparent 0,
|
||||||
|
transparent 75%,
|
||||||
|
rgb(0 0 0 / 25%) 0
|
||||||
|
),
|
||||||
|
linear-gradient(
|
||||||
|
45deg,
|
||||||
|
rgb(0 0 0 / 25%) 25%,
|
||||||
|
transparent 0,
|
||||||
|
transparent 75%,
|
||||||
|
rgb(0 0 0 / 25%) 0
|
||||||
|
);
|
||||||
|
background-position:
|
||||||
|
0 0,
|
||||||
|
12px 12px;
|
||||||
|
background-size: 24px 24px;
|
||||||
|
"
|
||||||
|
>
|
||||||
<CropperImage
|
<CropperImage
|
||||||
v-if="src"
|
v-if="src"
|
||||||
:circled="circled"
|
:circled="circled"
|
||||||
@@ -136,7 +158,7 @@ async function handleOk() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 工具栏 -->
|
<!-- 工具栏 -->
|
||||||
<div :class="`${prefixCls}-toolbar`">
|
<div class="mt-2.5 flex items-center justify-between">
|
||||||
<ElUpload
|
<ElUpload
|
||||||
:before-upload="handleBeforeUpload"
|
:before-upload="handleBeforeUpload"
|
||||||
:file-list="[]"
|
:file-list="[]"
|
||||||
@@ -281,18 +303,23 @@ async function handleOk() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右侧区域 -->
|
<!-- 右侧区域 -->
|
||||||
<div :class="`${prefixCls}-right`">
|
<div class="h-[340px] w-[45%]">
|
||||||
<!-- 预览区域 -->
|
<!-- 预览区域 -->
|
||||||
<div :class="`${prefixCls}-preview`">
|
<div
|
||||||
|
class="mx-auto h-[220px] w-[220px] overflow-hidden rounded-full border border-gray-200"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
v-if="previewSource"
|
v-if="previewSource"
|
||||||
:alt="$t('ui.cropper.preview')"
|
:alt="$t('ui.cropper.preview')"
|
||||||
:src="previewSource"
|
:src="previewSource"
|
||||||
|
class="h-full w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<!-- 头像组合预览 -->
|
<!-- 头像组合预览 -->
|
||||||
<template v-if="previewSource">
|
<template v-if="previewSource">
|
||||||
<div :class="`${prefixCls}-group`">
|
<div
|
||||||
|
class="mt-2 flex items-center justify-around border-t border-gray-200 pt-2"
|
||||||
|
>
|
||||||
<ElAvatar :src="previewSource" size="large" />
|
<ElAvatar :src="previewSource" size="large" />
|
||||||
<ElAvatar :size="48" :src="previewSource" />
|
<ElAvatar :size="48" :src="previewSource" />
|
||||||
<ElAvatar :size="64" :src="previewSource" />
|
<ElAvatar :size="64" :src="previewSource" />
|
||||||
@@ -303,77 +330,3 @@ async function handleOk() {
|
|||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
/* TODO @puhui999:要类似 web-antd/src/components/cropper/cropper-avatar.vue 减少 scss,通过 tindwind 么? */
|
|
||||||
.cropper-am {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
&-left,
|
|
||||||
&-right {
|
|
||||||
height: 340px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-left {
|
|
||||||
width: 55%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-right {
|
|
||||||
width: 45%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-cropper {
|
|
||||||
height: 300px;
|
|
||||||
background: #eee;
|
|
||||||
background-image:
|
|
||||||
linear-gradient(
|
|
||||||
45deg,
|
|
||||||
rgb(0 0 0 / 25%) 25%,
|
|
||||||
transparent 0,
|
|
||||||
transparent 75%,
|
|
||||||
rgb(0 0 0 / 25%) 0
|
|
||||||
),
|
|
||||||
linear-gradient(
|
|
||||||
45deg,
|
|
||||||
rgb(0 0 0 / 25%) 25%,
|
|
||||||
transparent 0,
|
|
||||||
transparent 75%,
|
|
||||||
rgb(0 0 0 / 25%) 0
|
|
||||||
);
|
|
||||||
background-position:
|
|
||||||
0 0,
|
|
||||||
12px 12px;
|
|
||||||
background-size: 24px 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-toolbar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-preview {
|
|
||||||
width: 220px;
|
|
||||||
height: 220px;
|
|
||||||
margin: 0 auto;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid #eee;
|
|
||||||
border-radius: 50%;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-group {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-around;
|
|
||||||
padding-top: 8px;
|
|
||||||
margin-top: 8px;
|
|
||||||
border-top: 1px solid #eee;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -33,8 +33,6 @@ const imgElRef = ref<ElRef<HTMLImageElement>>();
|
|||||||
const cropper = ref<Cropper | null>();
|
const cropper = ref<Cropper | null>();
|
||||||
const isReady = ref(false);
|
const isReady = ref(false);
|
||||||
|
|
||||||
// TODO @puhui999:这个有办法去掉么?
|
|
||||||
const prefixCls = 'cropper-image';
|
|
||||||
const debounceRealTimeCropped = useDebounceFn(realTimeCropped, 80);
|
const debounceRealTimeCropped = useDebounceFn(realTimeCropped, 80);
|
||||||
|
|
||||||
const getImageStyle = computed((): CSSProperties => {
|
const getImageStyle = computed((): CSSProperties => {
|
||||||
@@ -47,10 +45,9 @@ const getImageStyle = computed((): CSSProperties => {
|
|||||||
|
|
||||||
const getClass = computed(() => {
|
const getClass = computed(() => {
|
||||||
return [
|
return [
|
||||||
prefixCls,
|
|
||||||
attrs.class,
|
attrs.class,
|
||||||
{
|
{
|
||||||
[`${prefixCls}--circled`]: props.circled,
|
'cropper-image--circled': props.circled,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
@@ -158,6 +155,7 @@ function getRoundedCanvas() {
|
|||||||
:crossorigin="crossorigin"
|
:crossorigin="crossorigin"
|
||||||
:src="src"
|
:src="src"
|
||||||
:style="getImageStyle"
|
:style="getImageStyle"
|
||||||
|
class="h-auto max-w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ const apiSelectRule = [
|
|||||||
field: 'data',
|
field: 'data',
|
||||||
title: '请求参数 JSON 格式',
|
title: '请求参数 JSON 格式',
|
||||||
props: {
|
props: {
|
||||||
autosize: true, // TODO @puhui999:这里时 autoSize 还是 autosize 哈?和 antd 不同
|
autosize: true,
|
||||||
type: 'textarea',
|
type: 'textarea',
|
||||||
placeholder: '{"type": 1}',
|
placeholder: '{"type": 1}',
|
||||||
},
|
},
|
||||||
@@ -155,7 +155,7 @@ const apiSelectRule = [
|
|||||||
info: `data 为接口返回值,需要写一个匿名函数解析返回值为选择器 options 列表
|
info: `data 为接口返回值,需要写一个匿名函数解析返回值为选择器 options 列表
|
||||||
(data: any)=>{ label: string; value: any }[]`,
|
(data: any)=>{ label: string; value: any }[]`,
|
||||||
props: {
|
props: {
|
||||||
autosize: true, // TODO @puhui999:这里时 autoSize 还是 autosize 哈?和 antd 不同
|
autosize: true,
|
||||||
rows: { minRows: 2, maxRows: 6 },
|
rows: { minRows: 2, maxRows: 6 },
|
||||||
type: 'textarea',
|
type: 'textarea',
|
||||||
placeholder: `
|
placeholder: `
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export function useDictSelectRule() {
|
|||||||
title: label,
|
title: label,
|
||||||
info: '',
|
info: '',
|
||||||
$required: false,
|
$required: false,
|
||||||
// TODO @puhui999:vben 版本里,这里有个 modelField: 'value', 需要添加么?
|
modelField: 'model-value',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
props(_: any, { t }: any) {
|
props(_: any, { t }: any) {
|
||||||
|
|||||||
@@ -21,10 +21,13 @@ const emit = defineEmits<{
|
|||||||
(e: 'success'): void;
|
(e: 'success'): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// TODO @puhui999:展示貌似不太对;应该是左右,不是上下哈
|
|
||||||
const [Form, formApi] = useVbenForm({
|
const [Form, formApi] = useVbenForm({
|
||||||
commonConfig: {
|
commonConfig: {
|
||||||
labelWidth: 70,
|
componentProps: {
|
||||||
|
class: 'w-full',
|
||||||
|
},
|
||||||
|
formItemClass: 'col-span-2',
|
||||||
|
labelWidth: 80,
|
||||||
},
|
},
|
||||||
schema: [
|
schema: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ const avatar = computed(
|
|||||||
() => props.profile?.avatar || preferences.app.defaultAvatar,
|
() => props.profile?.avatar || preferences.app.defaultAvatar,
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO @puhui999:头像上传没跑通
|
|
||||||
async function handelUpload({
|
async function handelUpload({
|
||||||
file,
|
file,
|
||||||
filename,
|
filename,
|
||||||
@@ -37,9 +36,9 @@ async function handelUpload({
|
|||||||
const { httpRequest } = useUpload();
|
const { httpRequest } = useUpload();
|
||||||
// 将 Blob 转换为 File
|
// 将 Blob 转换为 File
|
||||||
const fileObj = new File([file], filename, { type: file.type });
|
const fileObj = new File([file], filename, { type: file.type });
|
||||||
const avatar = await httpRequest(fileObj);
|
const res = await httpRequest(fileObj);
|
||||||
// 2. 更新用户头像
|
// 2. 更新用户头像(httpRequest 返回 { url: string })
|
||||||
await updateUserProfile({ avatar });
|
await updateUserProfile({ avatar: res.url });
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -57,8 +56,8 @@ async function handelUpload({
|
|||||||
</ElTooltip>
|
</ElTooltip>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-8">
|
<div class="mt-8">
|
||||||
<ElDescriptions :column="2">
|
<ElDescriptions :column="2" border>
|
||||||
<ElDescriptionsItem>
|
<ElDescriptionsItem label="用户账号">
|
||||||
<template #label>
|
<template #label>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<IconifyIcon icon="ant-design:user-outlined" class="mr-1" />
|
<IconifyIcon icon="ant-design:user-outlined" class="mr-1" />
|
||||||
@@ -116,7 +115,11 @@ async function handelUpload({
|
|||||||
所属岗位
|
所属岗位
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
{{ profile.posts.map((post) => post.name).join(',') }}
|
{{
|
||||||
|
profile.posts && profile.posts.length > 0
|
||||||
|
? profile.posts.map((post) => post.name).join(',')
|
||||||
|
: '-'
|
||||||
|
}}
|
||||||
</ElDescriptionsItem>
|
</ElDescriptionsItem>
|
||||||
<ElDescriptionsItem>
|
<ElDescriptionsItem>
|
||||||
<template #label>
|
<template #label>
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
|
|||||||
toolbarConfig: {
|
toolbarConfig: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
} as VxeTableGridOptions<SystemSocialUserApi.SocialUser>,
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/** 解绑账号 */
|
/** 解绑账号 */
|
||||||
@@ -167,13 +167,12 @@ onMounted(() => {
|
|||||||
>
|
>
|
||||||
<ElCard v-for="item in allBindList" :key="item.type" class="!mb-2">
|
<ElCard v-for="item in allBindList" :key="item.type" class="!mb-2">
|
||||||
<div class="flex w-full items-center gap-4">
|
<div class="flex w-full items-center gap-4">
|
||||||
<!-- TODO @puhui999:图片大小不太对 -->
|
|
||||||
<ElImage
|
<ElImage
|
||||||
:src="item.img"
|
:src="item.img"
|
||||||
:width="40"
|
style="width: 40px; height: 40px"
|
||||||
:height="40"
|
|
||||||
:alt="item.title"
|
:alt="item.title"
|
||||||
:preview="false"
|
:preview-disabled="true"
|
||||||
|
fit="contain"
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-1 items-center justify-between">
|
<div class="flex flex-1 items-center justify-between">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
|
|||||||
@@ -490,16 +490,25 @@ defineExpose({
|
|||||||
@checkbox-all="handleSelectionChange"
|
@checkbox-all="handleSelectionChange"
|
||||||
>
|
>
|
||||||
<VxeColumn v-if="isComponent" type="checkbox" width="45" fixed="left" />
|
<VxeColumn v-if="isComponent" type="checkbox" width="45" fixed="left" />
|
||||||
<!-- TODO @puhui999:这里的宽度貌似有点问题,图片会寄出来; -->
|
<VxeColumn
|
||||||
<VxeColumn align="center" title="图片" max-width="140" fixed="left">
|
align="center"
|
||||||
|
title="图片"
|
||||||
|
width="80"
|
||||||
|
min-width="80"
|
||||||
|
fixed="left"
|
||||||
|
>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<ElImage
|
<div class="flex items-center justify-center overflow-hidden">
|
||||||
v-if="row.picUrl"
|
<ElImage
|
||||||
:src="row.picUrl"
|
v-if="row.picUrl"
|
||||||
class="h-[50px] w-[50px] cursor-pointer"
|
:src="row.picUrl"
|
||||||
:preview-src-list="[row.picUrl]"
|
class="h-[50px] w-[50px] cursor-pointer"
|
||||||
fit="cover"
|
:preview-src-list="[row.picUrl]"
|
||||||
/>
|
:preview-teleported="true"
|
||||||
|
:z-index="3000"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</VxeColumn>
|
</VxeColumn>
|
||||||
<template v-if="formData?.specType && !isBatch">
|
<template v-if="formData?.specType && !isBatch">
|
||||||
@@ -583,15 +592,24 @@ defineExpose({
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<VxeColumn v-if="isComponent" type="checkbox" width="45" fixed="left" />
|
<VxeColumn v-if="isComponent" type="checkbox" width="45" fixed="left" />
|
||||||
<!-- TODO @puhui999:这里的宽度貌似有点问题,图片会寄出来; -->
|
<VxeColumn
|
||||||
<VxeColumn align="center" title="图片" max-width="140" fixed="left">
|
align="center"
|
||||||
|
title="图片"
|
||||||
|
width="80"
|
||||||
|
min-width="80"
|
||||||
|
fixed="left"
|
||||||
|
>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<ElImage
|
<div class="flex items-center justify-center overflow-hidden">
|
||||||
:src="row.picUrl"
|
<ElImage
|
||||||
class="h-[60px] w-[60px] cursor-pointer"
|
:src="row.picUrl"
|
||||||
:preview-src-list="[row.picUrl]"
|
class="h-[60px] w-[60px] cursor-pointer"
|
||||||
fit="cover"
|
:preview-src-list="[row.picUrl]"
|
||||||
/>
|
:preview-teleported="true"
|
||||||
|
:z-index="3000"
|
||||||
|
fit="cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</VxeColumn>
|
</VxeColumn>
|
||||||
<template v-if="formData?.specType">
|
<template v-if="formData?.specType">
|
||||||
|
|||||||
@@ -130,12 +130,13 @@ watch(
|
|||||||
<VxeColumn field="id" align="center" title="商品编号" min-width="30" />
|
<VxeColumn field="id" align="center" title="商品编号" min-width="30" />
|
||||||
<VxeColumn title="商品图" min-width="80">
|
<VxeColumn title="商品图" min-width="80">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<!-- TODO @puhui999:它的 preview 貌似展示有点奇怪,不像 antd 是全屏的。。。 -->
|
|
||||||
<ElImage
|
<ElImage
|
||||||
v-if="row.picUrl"
|
v-if="row.picUrl"
|
||||||
:src="row.picUrl"
|
:src="row.picUrl"
|
||||||
class="h-[30px] w-[30px] cursor-pointer"
|
class="h-[30px] w-[30px] cursor-pointer"
|
||||||
:preview-src-list="[row.picUrl]"
|
:preview-src-list="[row.picUrl]"
|
||||||
|
:preview-teleported="true"
|
||||||
|
:z-index="3000"
|
||||||
fit="cover"
|
fit="cover"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -320,7 +320,6 @@ onMounted(async () => {
|
|||||||
<ElCard class="h-full w-full" v-loading="formLoading">
|
<ElCard class="h-full w-full" v-loading="formLoading">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<!-- TODO @puhui999:这里有告警,需要修复下。 -->
|
|
||||||
<ElTabs v-model="activeTabName" @tab-change="handleTabChange">
|
<ElTabs v-model="activeTabName" @tab-change="handleTabChange">
|
||||||
<ElTabPane label="基础设置" name="info" />
|
<ElTabPane label="基础设置" name="info" />
|
||||||
<ElTabPane label="价格库存" name="sku" />
|
<ElTabPane label="价格库存" name="sku" />
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
fieldName: 'spuIds',
|
fieldName: 'spuIds',
|
||||||
label: '活动商品',
|
label: '活动商品',
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
rules: 'required',
|
|
||||||
formItemClass: 'col-span-2',
|
formItemClass: 'col-span-2',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,30 +1,52 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { MallSpuApi } from '#/api/mall/product/spu';
|
||||||
import type { MallDiscountActivityApi } from '#/api/mall/promotion/discount/discountActivity';
|
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 { 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 {
|
import {
|
||||||
createDiscountActivity,
|
createDiscountActivity,
|
||||||
getDiscountActivity,
|
getDiscountActivity,
|
||||||
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 {
|
||||||
|
getPropertyList,
|
||||||
|
SpuAndSkuList,
|
||||||
|
SpuSkuSelect,
|
||||||
|
} from '#/views/mall/product/spu/components';
|
||||||
|
|
||||||
import { useFormSchema } from '../data';
|
import { useFormSchema } from '../data';
|
||||||
|
|
||||||
defineOptions({ name: 'DiscountActivityForm' });
|
defineOptions({ name: 'DiscountActivityForm' });
|
||||||
|
|
||||||
|
/** 折扣类型枚举 */
|
||||||
|
const PromotionDiscountTypeEnum = {
|
||||||
|
PRICE: { type: 1 }, // 满减
|
||||||
|
PERCENT: { type: 2 }, // 折扣
|
||||||
|
};
|
||||||
|
|
||||||
const emit = defineEmits(['success']);
|
const emit = defineEmits(['success']);
|
||||||
const formData = ref<
|
|
||||||
Partial<MallDiscountActivityApi.DiscountActivity> & {
|
// ================= 表单相关 =================
|
||||||
spuIds?: number[];
|
const formData = ref<Partial<MallDiscountActivityApi.DiscountActivity>>({});
|
||||||
}
|
|
||||||
>({});
|
|
||||||
const getTitle = computed(() => {
|
const getTitle = computed(() => {
|
||||||
return formData.value?.id
|
return formData.value?.id
|
||||||
? $t('ui.actionTitle.edit', ['限时折扣活动'])
|
? $t('ui.actionTitle.edit', ['限时折扣活动'])
|
||||||
@@ -44,28 +66,203 @@ const [Form, formApi] = useVbenForm({
|
|||||||
showDefaultActions: false,
|
showDefaultActions: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO @puhui999:antd 和 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({
|
const [Modal, modalApi] = useVbenModal({
|
||||||
async onConfirm() {
|
async onConfirm() {
|
||||||
const { valid } = await formApi.validate();
|
const { valid } = await formApi.validate();
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
modalApi.lock();
|
|
||||||
// 提交表单
|
|
||||||
const data =
|
|
||||||
(await formApi.getValues()) as MallDiscountActivityApi.DiscountActivity;
|
|
||||||
|
|
||||||
// 确保必要的默认值
|
// 校验是否选择了商品
|
||||||
if (!data.products) {
|
if (spuList.value.length === 0) {
|
||||||
data.products = [];
|
ElMessage.warning('请选择活动商品');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
modalApi.lock();
|
||||||
try {
|
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
|
await (formData.value?.id
|
||||||
? updateDiscountActivity(data)
|
? updateDiscountActivity(data)
|
||||||
: createDiscountActivity(data));
|
: createDiscountActivity(data));
|
||||||
// 关闭并提示
|
|
||||||
await modalApi.close();
|
await modalApi.close();
|
||||||
emit('success');
|
emit('success');
|
||||||
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
ElMessage.success($t('ui.actionMessage.operationSuccess'));
|
||||||
@@ -75,19 +272,45 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
},
|
},
|
||||||
async onOpenChange(isOpen: boolean) {
|
async onOpenChange(isOpen: boolean) {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
formData.value = {};
|
await resetForm();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载数据
|
// 加载数据
|
||||||
const data = modalApi.getData<MallDiscountActivityApi.DiscountActivity>();
|
const data = modalApi.getData<MallDiscountActivityApi.DiscountActivity>();
|
||||||
if (!data || !data.id) {
|
if (!data || !data.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
modalApi.lock();
|
modalApi.lock();
|
||||||
try {
|
try {
|
||||||
formData.value = await getDiscountActivity(data.id);
|
const activityData = await getDiscountActivity(data.id);
|
||||||
// 设置到 values
|
formData.value = activityData;
|
||||||
await formApi.setValues(formData.value);
|
|
||||||
|
// 加载商品详情
|
||||||
|
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 {
|
} finally {
|
||||||
modalApi.unlock();
|
modalApi.unlock();
|
||||||
}
|
}
|
||||||
@@ -96,12 +319,59 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Modal class="w-3/5" :title="getTitle">
|
<Modal class="w-[70%]" :title="getTitle">
|
||||||
<Form>
|
<Form>
|
||||||
<!-- 自定义插槽:商品选择 -->
|
<!-- 自定义插槽:商品选择 -->
|
||||||
<template #spuIds>
|
<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>
|
</template>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<!-- 商品选择弹窗 -->
|
||||||
|
<SpuSkuSelect
|
||||||
|
ref="spuSelectRef"
|
||||||
|
:is-select-sku="true"
|
||||||
|
@select="handleSpuSelected"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
componentProps: {
|
componentProps: {
|
||||||
showTime: true,
|
showTime: true,
|
||||||
format: 'YYYY-MM-DD HH:mm:ss',
|
format: 'YYYY-MM-DD HH:mm:ss',
|
||||||
|
valueFormat: 'x',
|
||||||
placeholder: [
|
placeholder: [
|
||||||
$t('utils.rangePicker.beginTime'),
|
$t('utils.rangePicker.beginTime'),
|
||||||
$t('utils.rangePicker.endTime'),
|
$t('utils.rangePicker.endTime'),
|
||||||
@@ -222,7 +223,10 @@ export function useFormSchema(): VbenFormSchema[] {
|
|||||||
label: '优惠设置',
|
label: '优惠设置',
|
||||||
component: 'Input',
|
component: 'Input',
|
||||||
formItemClass: 'items-start',
|
formItemClass: 'items-start',
|
||||||
rules: 'required',
|
rules: z
|
||||||
|
.array(z.any())
|
||||||
|
.min(1, { message: '请添加至少一条优惠规则' })
|
||||||
|
.default([]),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'productScopeValues', // 隐藏字段:用于自动同步 productScopeValues
|
fieldName: 'productScopeValues', // 隐藏字段:用于自动同步 productScopeValues
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
PromotionConditionTypeEnum,
|
PromotionConditionTypeEnum,
|
||||||
PromotionProductScopeEnum,
|
PromotionProductScopeEnum,
|
||||||
} from '@vben/constants';
|
} from '@vben/constants';
|
||||||
import { convertToInteger, formatToFraction } from '@vben/utils';
|
import { cloneDeep, convertToInteger, formatToFraction } from '@vben/utils';
|
||||||
|
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
|
|
||||||
@@ -52,6 +52,8 @@ const [Form, formApi] = useVbenForm({
|
|||||||
|
|
||||||
const [Modal, modalApi] = useVbenModal({
|
const [Modal, modalApi] = useVbenModal({
|
||||||
async onConfirm() {
|
async onConfirm() {
|
||||||
|
// 在验证前同步 formData.rules 到表单中
|
||||||
|
await formApi.setFieldValue('rules', formData.value.rules || []);
|
||||||
const { valid } = await formApi.validate();
|
const { valid } = await formApi.validate();
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
return;
|
return;
|
||||||
@@ -60,18 +62,24 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
// 提交表单
|
// 提交表单
|
||||||
try {
|
try {
|
||||||
const values = await formApi.getValues();
|
const values = await formApi.getValues();
|
||||||
const data = { ...formData.value, ...values };
|
// 使用 formData.value 作为基础,确保 rules 来自 formData
|
||||||
|
const data = { ...values, ...formData.value };
|
||||||
if (data.startAndEndTime && Array.isArray(data.startAndEndTime)) {
|
if (data.startAndEndTime && Array.isArray(data.startAndEndTime)) {
|
||||||
data.startTime = data.startAndEndTime[0];
|
data.startTime = Number(data.startAndEndTime[0]);
|
||||||
data.endTime = data.startAndEndTime[1];
|
data.endTime = Number(data.startAndEndTime[1]);
|
||||||
delete data.startAndEndTime;
|
delete data.startAndEndTime;
|
||||||
}
|
}
|
||||||
data.rules?.forEach((item: any) => {
|
// 深拷贝 rules 避免修改原始数据
|
||||||
|
const rules = cloneDeep(
|
||||||
|
data.rules,
|
||||||
|
) as unknown as MallRewardActivityApi.RewardRule[];
|
||||||
|
rules.forEach((item: any) => {
|
||||||
item.discountPrice = convertToInteger(item.discountPrice || 0);
|
item.discountPrice = convertToInteger(item.discountPrice || 0);
|
||||||
if (data.conditionType === PromotionConditionTypeEnum.PRICE.type) {
|
if (data.conditionType === PromotionConditionTypeEnum.PRICE.type) {
|
||||||
item.limit = convertToInteger(item.limit || 0);
|
item.limit = convertToInteger(item.limit || 0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
data.rules = rules;
|
||||||
await (data.id
|
await (data.id
|
||||||
? updateRewardActivity(data as MallRewardActivityApi.RewardActivity)
|
? updateRewardActivity(data as MallRewardActivityApi.RewardActivity)
|
||||||
: createRewardActivity(data as MallRewardActivityApi.RewardActivity));
|
: createRewardActivity(data as MallRewardActivityApi.RewardActivity));
|
||||||
@@ -96,7 +104,11 @@ const [Modal, modalApi] = useVbenModal({
|
|||||||
modalApi.lock();
|
modalApi.lock();
|
||||||
try {
|
try {
|
||||||
const result = await getReward(data.id);
|
const result = await getReward(data.id);
|
||||||
result.startAndEndTime = [result.startTime, result.endTime] as any[];
|
// valueFormat: 'x' 配置下,直接使用时间戳字符串
|
||||||
|
result.startAndEndTime = [
|
||||||
|
result.startTime ? String(result.startTime) : undefined,
|
||||||
|
result.endTime ? String(result.endTime) : undefined,
|
||||||
|
] as any[];
|
||||||
result.rules?.forEach((item: any) => {
|
result.rules?.forEach((item: any) => {
|
||||||
item.discountPrice = formatToFraction(item.discountPrice || 0);
|
item.discountPrice = formatToFraction(item.discountPrice || 0);
|
||||||
if (result.conditionType === PromotionConditionTypeEnum.PRICE.type) {
|
if (result.conditionType === PromotionConditionTypeEnum.PRICE.type) {
|
||||||
|
|||||||
Reference in New Issue
Block a user