4 Commits
dev ... master

Author SHA1 Message Date
芋道源码
9be7bb5065 !311 (〃'▽'〃) v2025.12 发布:极大极大完善 vben5 的 antd、vben 版本的功能,新增 admin uniapp vue3 版本
Merge pull request !311 from 芋道源码/dev
2025-12-28 03:08:53 +00:00
芋道源码
70f075b003 !310 chore: new tag
Merge pull request !310 from xingyu/dev
2025-12-27 03:40:30 +00:00
芋道源码
3908f03418 !309 feat:【antd】【ele】大量更新同步
Merge pull request !309 from 芋道源码/dev
2025-12-26 01:03:05 +00:00
xingyu
65b210faf6 !285 !283 Merge remote-tracking branch 'yudao/dev' into dev
Merge pull request !285 from xingyu/dev
2025-11-28 10:12:18 +00:00
39 changed files with 318 additions and 814 deletions

View File

@@ -43,7 +43,6 @@ const [Modal, modalApi] = useVbenModal({
if (isOpen) {
// 打开时,进行 loading 加载。后续 CropperImage 组件加载完毕,会自动关闭 loading通过 handleReady
modalLoading(true);
// TODO @puhui999这里比 ele 多了,是符合预期的哇?
const img = new Image();
img.src = src.value;
img.addEventListener('load', () => {

View File

@@ -121,7 +121,7 @@ const apiSelectRule = [
field: 'data',
title: '请求参数 JSON 格式',
props: {
autoSize: true, // 特殊ele 里是 autosizeantd 里是 autoSize
autoSize: true,
type: 'textarea',
placeholder: '{"type": 1}',
},
@@ -155,7 +155,7 @@ const apiSelectRule = [
info: `data 为接口返回值,需要写一个匿名函数解析返回值为选择器 options 列表
(data: any)=>{ label: string; value: any }[]`,
props: {
autoSize: true, // 特殊ele 里是 autosizeantd 里是 autoSize
autoSize: true,
rows: { minRows: 2, maxRows: 6 },
type: 'textarea',
placeholder: `

View File

@@ -39,7 +39,7 @@ export function useDictSelectRule() {
title: label,
info: '',
$required: false,
modelField: 'value', // 特殊ele 里是 model-valueantd 里是 value
modelField: 'value',
};
},
props(_: any, { t }: any) {

View File

@@ -100,7 +100,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
toolbarConfig: {
enabled: false,
},
},
} as VxeTableGridOptions<SystemSocialUserApi.SocialUser>,
});
/** 解绑账号 */

View File

@@ -158,7 +158,7 @@ function changeNodeName() {
defineExpose({ open }); // 提供 open 方法,用于打开弹窗
</script>
<template>
<Drawer class="w-2/5">
<Drawer class="w-1/3">
<template #title>
<div class="flex items-center">
<Input

View File

@@ -383,7 +383,7 @@ onMounted(() => {
});
</script>
<template>
<Drawer class="w-2/5">
<Drawer class="w-1/3">
<template #title>
<div class="config-header">
<Input

View File

@@ -497,7 +497,7 @@ onBeforeUnmount(() => {
/>
<!-- 第四步更多设置 -->
<div v-show="currentStep === 3" class="mx-auto w-4/6">
<div v-if="currentStep === 3" class="mx-auto w-4/6">
<ExtraSetting v-model="formData" ref="extraSettingRef" />
</div>
</div>

View File

@@ -29,8 +29,10 @@ import {
import dayjs from 'dayjs';
import { getForm } from '#/api/bpm/form';
import { parseFormFields } from '#/components/form-create';
import { HttpRequestSetting } from '#/views/bpm/components/simple-process-design';
import {
HttpRequestSetting,
parseFormFields,
} from '#/views/bpm/components/simple-process-design';
import PrintTemplate from './custom-print-template.vue';

View File

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

View File

@@ -1,53 +1,30 @@
<script lang="ts" setup>
import type { MallSpuApi } from '#/api/mall/product/spu';
import type { MallDiscountActivityApi } from '#/api/mall/promotion/discount/discountActivity';
import type {
PropertyAndValues,
RuleConfig,
SpuProperty,
} from '#/views/mall/product/spu/components';
import { computed, nextTick, ref } from 'vue';
import { computed, ref } from 'vue';
import { useVbenForm, useVbenModal } from '@vben/common-ui';
import {
convertToInteger,
erpCalculatePercentage,
formatToFraction,
yuanToFen,
} from '@vben/utils';
import { Button, InputNumber, message } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import { VxeColumn } from '#/adapter/vxe-table';
import { getSpuDetailList } from '#/api/mall/product/spu';
import {
createDiscountActivity,
getDiscountActivity,
updateDiscountActivity,
} from '#/api/mall/promotion/discount/discountActivity';
import { $t } from '#/locales';
import {
getPropertyList,
SpuAndSkuList,
SpuSkuSelect,
} from '#/views/mall/product/spu/components';
import { SpuShowcase } from '#/views/mall/product/spu/components';
import { useFormSchema } from '../data';
defineOptions({ name: 'DiscountActivityForm' });
const emit = defineEmits(['success']);
/** 折扣类型枚举 */
// TODO @puhui999这里可以使用 biz-mall 里的枚举噢;
const PromotionDiscountTypeEnum = {
PRICE: { type: 1 }, // 满减
PERCENT: { type: 2 }, // 折扣
};
// ================= 表单相关 =================
const formData = ref<Partial<MallDiscountActivityApi.DiscountActivity>>({});
const formData = ref<
Partial<MallDiscountActivityApi.DiscountActivity> & {
spuIds?: number[];
}
>({});
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['限时折扣活动'])
@@ -67,196 +44,23 @@ const [Form, formApi] = useVbenForm({
showDefaultActions: false,
});
// ================= 商品选择相关 =================
/** SKU 扩展类型 */
interface SkuExtension extends MallSpuApi.Sku {
productConfig: MallDiscountActivityApi.DiscountProduct;
}
/** SPU 扩展类型 */
interface SpuExtension extends MallSpuApi.Spu {
skus?: SkuExtension[];
}
const spuSelectRef = ref(); // 商品选择组件 Ref
const spuAndSkuListRef = ref(); // SKU 列表组件 Ref
const spuList = ref<SpuExtension[]>([]); // 选择的 SPU 列表
const spuPropertyList = ref<SpuProperty<SpuExtension>[]>([]); // SPU 属性列表
const spuIdList = ref<number[]>([]); // 已选择的 SPU ID 列表
/** SKU 校验规则配置 */
const ruleConfig: RuleConfig[] = [
{
name: 'productConfig.discountPrice',
rule: (arg) => arg > 0,
message: '商品优惠金额不能为 0 ',
},
];
/** 打开商品选择弹窗 */
function openSpuSelect() {
spuSelectRef.value?.open();
}
/** 选择商品后的回调 */
function handleSpuSelected(spuId: number, skuIds?: number[]) {
getSpuDetails(spuId, skuIds);
}
/** 获取 SPU 详情 */
async function getSpuDetails(
spuId: number,
skuIdArr?: number[],
products?: MallDiscountActivityApi.DiscountProduct[],
type?: string,
) {
// 如果已经包含该 SPU 则跳过
if (spuIdList.value.includes(spuId)) {
if (type !== 'load') {
message.error('数据重复选择!');
}
return;
}
spuIdList.value.push(spuId);
const res = (await getSpuDetailList([spuId])) as SpuExtension[];
if (res.length === 0) {
return;
}
const spu = res[0]!;
// 筛选 SKU
const selectSkus =
skuIdArr === undefined
? spu.skus
: spu.skus?.filter((sku) => skuIdArr.includes(sku.id!));
// 为每个 SKU 添加折扣配置
selectSkus?.forEach((sku) => {
let config: MallDiscountActivityApi.DiscountProduct = {
skuId: sku.id!,
spuId: spu.id!,
discountType: 1,
discountPercent: 0,
discountPrice: 0,
};
// 编辑时,使用已有的配置
if (products !== undefined) {
const product = products.find((item) => item.skuId === sku.id);
if (product) {
// 转换为元显示
config = {
...product,
discountPercent: Number(formatToFraction(product.discountPercent)),
discountPrice: Number(formatToFraction(product.discountPrice)),
};
}
}
(sku as SkuExtension).productConfig = config;
});
spu.skus = selectSkus as SkuExtension[];
spuPropertyList.value.push({
spuId: spu.id!,
spuDetail: spu,
propertyList: getPropertyList(spu) as PropertyAndValues[],
});
spuList.value.push(spu);
}
/** 删除 SPU */
function handleDeleteSpu(spuId: number) {
const spuIndex = spuIdList.value.indexOf(spuId);
if (spuIndex !== -1) {
spuIdList.value.splice(spuIndex, 1);
}
const propertyIndex = spuPropertyList.value.findIndex(
(item) => item.spuId === spuId,
);
if (propertyIndex !== -1) {
spuPropertyList.value.splice(propertyIndex, 1);
}
const listIndex = spuList.value.findIndex((item) => item.id === spuId);
if (listIndex !== -1) {
spuList.value.splice(listIndex, 1);
}
}
/** 处理 SKU 优惠金额变动 */
function handleSkuDiscountPriceChange(row: SkuExtension) {
if (row.productConfig.discountPrice <= 0) {
return;
}
// 设置优惠类型:满减
row.productConfig.discountType = PromotionDiscountTypeEnum.PRICE.type;
// 计算折扣百分比
const price = typeof row.price === 'number' ? row.price : Number(row.price);
const percent = erpCalculatePercentage(
price - yuanToFen(row.productConfig.discountPrice),
price,
);
row.productConfig.discountPercent =
typeof percent === 'number' ? percent : Number(percent);
}
/** 处理 SKU 折扣百分比变动 */
function handleSkuDiscountPercentChange(row: SkuExtension) {
if (
row.productConfig.discountPercent <= 0 ||
row.productConfig.discountPercent >= 100
) {
return;
}
// 设置优惠类型:折扣
row.productConfig.discountType = PromotionDiscountTypeEnum.PERCENT.type;
// 计算优惠金额
const price = typeof row.price === 'number' ? row.price : Number(row.price);
row.productConfig.discountPrice = Number(
formatToFraction(price - price * (row.productConfig.discountPercent / 100)),
);
}
/** 重置表单 */
async function resetForm() {
spuList.value = [];
spuPropertyList.value = [];
spuIdList.value = [];
formData.value = {};
await nextTick();
await formApi.resetForm();
}
// ================= 弹窗相关 =================
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
// 校验是否选择了商品
if (spuList.value.length === 0) {
message.warning('请选择活动商品');
return;
}
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as MallDiscountActivityApi.DiscountActivity;
// 确保必要的默认值
if (!data.products) {
data.products = [];
}
try {
// 获取折扣商品配置
// TODO @puhui999structuredClone 执行会报错;
const products = structuredClone(
spuAndSkuListRef.value?.getSkuConfigs('productConfig') || [],
) as MallDiscountActivityApi.DiscountProduct[];
// 转换金额为分
products.forEach((item) => {
item.discountPercent = convertToInteger(item.discountPercent);
item.discountPrice = convertToInteger(item.discountPrice);
});
const data = structuredClone(
await formApi.getValues(),
) as MallDiscountActivityApi.DiscountActivity;
data.products = products;
// 提交请求
await (formData.value?.id
? updateDiscountActivity(data)
: createDiscountActivity(data));
@@ -270,7 +74,7 @@ const [Modal, modalApi] = useVbenModal({
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
await resetForm();
formData.value = {};
return;
}
// 加载数据
@@ -280,30 +84,9 @@ const [Modal, modalApi] = useVbenModal({
}
modalApi.lock();
try {
const activityData = await getDiscountActivity(data.id);
formData.value = activityData;
// 加载商品详情
if (activityData.products && activityData.products.length > 0) {
// 按 spuId 分组
const spuProductsMap = new Map<
number,
MallDiscountActivityApi.DiscountProduct[]
>();
for (const product of activityData.products) {
const spuId = product.spuId;
if (!spuProductsMap.has(spuId)) {
spuProductsMap.set(spuId, []);
}
spuProductsMap.get(spuId)!.push(product);
}
// 加载每个 SPU 的详情
for (const [spuId, products] of spuProductsMap) {
const skuIdArr = products.map((p) => p.skuId);
await getSpuDetails(spuId, skuIdArr, products, 'load');
}
}
formData.value = await getDiscountActivity(data.id);
// 设置到 values
await formApi.setValues(activityData);
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
@@ -312,59 +95,12 @@ const [Modal, modalApi] = useVbenModal({
</script>
<template>
<Modal class="w-[70%]" :title="getTitle">
<Modal class="w-3/5" :title="getTitle">
<Form>
<!-- 自定义插槽商品选择 -->
<template #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>
<SpuShowcase v-model="formData.spuIds" />
</template>
</Form>
</Modal>
<!-- 商品选择弹窗 -->
<SpuSkuSelect
ref="spuSelectRef"
:is-select-sku="true"
@select="handleSpuSelected"
/>
</template>

View File

@@ -138,7 +138,6 @@ export function useFormSchema(): VbenFormSchema[] {
componentProps: {
showTime: true,
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'x',
placeholder: [
$t('utils.rangePicker.beginTime'),
$t('utils.rangePicker.endTime'),
@@ -218,15 +217,13 @@ export function useFormSchema(): VbenFormSchema[] {
},
rules: 'required',
},
// TODO @puhui9991新增时一直报“请输入优惠设置”2修改老数据出现报“请求参数类型错误:50.00”;
{
fieldName: 'rules',
label: '优惠设置',
component: 'Input',
formItemClass: 'items-start',
rules: z
.array(z.any())
.min(1, { message: '请添加至少一条优惠规则' })
.default([]),
rules: 'required',
},
{
fieldName: 'productScopeValues', // 隐藏字段:用于自动同步 productScopeValues

View File

@@ -8,9 +8,10 @@ import {
PromotionConditionTypeEnum,
PromotionProductScopeEnum,
} from '@vben/constants';
import { cloneDeep, convertToInteger, formatToFraction } from '@vben/utils';
import { convertToInteger, formatToFraction } from '@vben/utils';
import { message } from 'ant-design-vue';
import dayjs from 'dayjs';
import { useVbenForm } from '#/adapter/form';
import {
@@ -52,9 +53,6 @@ const [Form, formApi] = useVbenForm({
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
// 在验证前同步 formData.rules 到表单中
// TODO @puhui999选择了分类、或者商品还是报没选择
await formApi.setFieldValue('rules', formData.value.rules || []);
const { valid } = await formApi.validate();
if (!valid) {
return;
@@ -63,24 +61,18 @@ const [Modal, modalApi] = useVbenModal({
// 提交表单
try {
const values = await formApi.getValues();
// 使用 formData.value 作为基础,确保 rules 来自 formData
const data = { ...values, ...formData.value };
const data = { ...formData.value, ...values };
if (data.startAndEndTime && Array.isArray(data.startAndEndTime)) {
data.startTime = data.startAndEndTime[0];
data.endTime = data.startAndEndTime[1];
delete data.startAndEndTime;
}
// 深拷贝 rules 避免修改原始数据
const rules = cloneDeep(
data.rules,
) as unknown as MallRewardActivityApi.RewardRule[];
rules.forEach((item: any) => {
data.rules?.forEach((item: any) => {
item.discountPrice = convertToInteger(item.discountPrice || 0);
if (data.conditionType === PromotionConditionTypeEnum.PRICE.type) {
item.limit = convertToInteger(item.limit || 0);
}
});
data.rules = rules;
await (data.id
? updateRewardActivity(data as MallRewardActivityApi.RewardActivity)
: createRewardActivity(data as MallRewardActivityApi.RewardActivity));
@@ -106,8 +98,8 @@ const [Modal, modalApi] = useVbenModal({
try {
const result = await getReward(data.id);
result.startAndEndTime = [
result.startTime ? String(result.startTime) : undefined,
result.endTime ? String(result.endTime) : undefined,
result.startTime ? dayjs(result.startTime) : undefined,
result.endTime ? dayjs(result.endTime) : undefined,
] as any[];
result.rules?.forEach((item: any) => {
item.discountPrice = formatToFraction(item.discountPrice || 0);

View File

@@ -18,7 +18,7 @@ const props = withDefaults(defineProps<CropperAvatarProps>(), {
width: 200,
value: '',
showBtn: true,
btnProps: () => ({}) as any,
btnProps: () => ({}),
btnText: '',
uploadApi: () => Promise.resolve(),
size: 5,
@@ -27,10 +27,14 @@ const props = withDefaults(defineProps<CropperAvatarProps>(), {
const emit = defineEmits(['update:value', 'change']);
const sourceValue = ref(props.value || '');
// TODO @puhui999这个有办法去掉么
const prefixCls = 'cropper-avatar';
const [CropperModal, modalApi] = useVbenModal({
connectedComponent: cropperModal,
});
const getClass = computed(() => [prefixCls]);
const getWidth = computed(() => `${`${props.width}`.replace(/px/, '')}px`);
const getIconWidth = computed(
@@ -70,42 +74,34 @@ defineExpose({
</script>
<template>
<!-- TODO @puhui999html 部分看看有没办法和 web-antd/src/components/cropper/cropper-avatar.vue 风格更接近 -->
<!-- 头像容器 -->
<div class="inline-block text-center" :style="getStyle">
<div :class="getClass" :style="getStyle">
<!-- 图片包装器 -->
<div
class="group relative cursor-pointer overflow-hidden rounded-full border border-gray-200 bg-white"
:class="`${prefixCls}-image-wrapper`"
:style="getImageWrapperStyle"
@click="openModal"
>
<!-- 遮罩层 -->
<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"
>
<!-- TODO @puhui999可以改成类似 /Users/yunai/Java/yudao-ui-admin-vben-v5/apps/web-antd/src/components/cropper/cropper-avatar.vue 里的 Icon -->
<div :class="`${prefixCls}-image-mask`" :style="getImageWrapperStyle">
<span
:style="{
...getImageWrapperStyle,
width: getIconWidth,
height: getIconWidth,
lineHeight: getIconWidth,
width: `${getIconWidth}`,
height: `${getIconWidth}`,
lineHeight: `${getIconWidth}`,
}"
class="icon-[ant-design--cloud-upload-outlined] text-gray-400"
class="icon-[ant-design--cloud-upload-outlined] text-[#d6d6d6]"
></span>
</div>
<!-- 头像图片 -->
<img
v-if="sourceValue"
:src="sourceValue"
alt="avatar"
class="h-full w-full object-cover"
/>
<img v-if="sourceValue" :src="sourceValue" alt="avatar" />
</div>
<!-- 上传按钮 -->
<ElButton
v-if="showBtn"
class="mx-auto mt-2"
:class="`${prefixCls}-upload-btn`"
@click="openModal"
v-bind="btnProps"
>
@@ -120,3 +116,50 @@ defineExpose({
/>
</div>
</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>

View File

@@ -36,6 +36,7 @@ const cropper = ref<CropperType>();
let scaleX = 1;
let scaleY = 1;
const prefixCls = 'cropper-am';
const [Modal, modalApi] = useVbenModal({
onConfirm: handleOk,
onOpenChange(isOpen) {
@@ -112,7 +113,6 @@ async function handleOk() {
</script>
<template>
<!-- TODO @puhui999antd 版本里是 2/3 宽度两边要对齐么 -->
<Modal
v-bind="$attrs"
:confirm-text="$t('ui.cropper.okText')"
@@ -120,37 +120,11 @@ async function handleOk() {
:title="$t('ui.cropper.modalTitle')"
class="w-[800px]"
>
<!-- TODO @puhui999antd 版本有个 h-96两边要对齐么 -->
<div class="flex">
<div :class="prefixCls">
<!-- 左侧区域 -->
<!-- TODO @puhui999antd 版本是 h-full w-3/5 两边要对齐么 -->
<div class="h-[340px] w-[55%]">
<div :class="`${prefixCls}-left`" class="w-full">
<!-- 裁剪器容器 -->
<!-- TODO @puhui999antd class 简单一点看看要不要对齐 -->
<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;
"
>
<div :class="`${prefixCls}-cropper`">
<CropperImage
v-if="src"
:circled="circled"
@@ -162,8 +136,7 @@ async function handleOk() {
</div>
<!-- 工具栏 -->
<!-- TODO @puhui999antd mt-4看看两边要不要对齐 -->
<div class="mt-2.5 flex items-center justify-between">
<div :class="`${prefixCls}-toolbar`">
<ElUpload
:before-upload="handleBeforeUpload"
:file-list="[]"
@@ -174,7 +147,6 @@ async function handleOk() {
placement="bottom"
>
<ElButton size="small" type="primary">
<!-- TODO @puhui999可以改成类似 /Users/yunai/Java/yudao-ui-admin-vben-v5/apps/web-antd/src/components/cropper/cropper-modal.vue 里的 Icon -->
<template #icon>
<div class="flex items-center justify-center">
<span class="icon-[ant-design--upload-outlined]"></span>
@@ -192,7 +164,6 @@ async function handleOk() {
@click="handlerToolbar('reset')"
>
<template #icon>
<!-- TODO @puhui999可以改成类似 /Users/yunai/Java/yudao-ui-admin-vben-v5/apps/web-antd/src/components/cropper/cropper-modal.vue 里的 Icon -->
<div class="flex items-center justify-center">
<span class="icon-[ant-design--reload-outlined]"></span>
</div>
@@ -211,7 +182,6 @@ async function handleOk() {
>
<template #icon>
<div class="flex items-center justify-center">
<!-- TODO @puhui999可以改成类似 /Users/yunai/Java/yudao-ui-admin-vben-v5/apps/web-antd/src/components/cropper/cropper-modal.vue 里的 Icon -->
<span
class="icon-[ant-design--rotate-left-outlined]"
></span>
@@ -230,7 +200,6 @@ async function handleOk() {
@click="handlerToolbar('rotate', 45)"
>
<template #icon>
<!-- TODO @puhui999可以改成类似 /Users/yunai/Java/yudao-ui-admin-vben-v5/apps/web-antd/src/components/cropper/cropper-modal.vue 里的 Icon -->
<div class="flex items-center justify-center">
<span
class="icon-[ant-design--rotate-right-outlined]"
@@ -250,7 +219,6 @@ async function handleOk() {
@click="handlerToolbar('scaleX')"
>
<template #icon>
<!-- TODO @puhui999可以改成类似 /Users/yunai/Java/yudao-ui-admin-vben-v5/apps/web-antd/src/components/cropper/cropper-modal.vue 里的 Icon -->
<div class="flex items-center justify-center">
<span class="icon-[vaadin--arrows-long-h]"></span>
</div>
@@ -268,7 +236,6 @@ async function handleOk() {
@click="handlerToolbar('scaleY')"
>
<template #icon>
<!-- TODO @puhui999可以改成类似 /Users/yunai/Java/yudao-ui-admin-vben-v5/apps/web-antd/src/components/cropper/cropper-modal.vue 里的 Icon -->
<div class="flex items-center justify-center">
<span class="icon-[vaadin--arrows-long-v]"></span>
</div>
@@ -286,7 +253,6 @@ async function handleOk() {
@click="handlerToolbar('zoom', 0.1)"
>
<template #icon>
<!-- TODO @puhui999可以改成类似 /Users/yunai/Java/yudao-ui-admin-vben-v5/apps/web-antd/src/components/cropper/cropper-modal.vue 里的 Icon -->
<div class="flex items-center justify-center">
<span class="icon-[ant-design--zoom-in-outlined]"></span>
</div>
@@ -304,7 +270,6 @@ async function handleOk() {
@click="handlerToolbar('zoom', -0.1)"
>
<template #icon>
<!-- TODO @puhui999可以改成类似 /Users/yunai/Java/yudao-ui-admin-vben-v5/apps/web-antd/src/components/cropper/cropper-modal.vue 里的 Icon -->
<div class="flex items-center justify-center">
<span class="icon-[ant-design--zoom-out-outlined]"></span>
</div>
@@ -316,23 +281,18 @@ async function handleOk() {
</div>
<!-- 右侧区域 -->
<div class="h-[340px] w-[45%]">
<div :class="`${prefixCls}-right`">
<!-- 预览区域 -->
<div
class="mx-auto h-[220px] w-[220px] overflow-hidden rounded-full border border-gray-200"
>
<div :class="`${prefixCls}-preview`">
<img
v-if="previewSource"
:alt="$t('ui.cropper.preview')"
:src="previewSource"
class="h-full w-full"
/>
</div>
<!-- 头像组合预览 -->
<template v-if="previewSource">
<div
class="mt-2 flex items-center justify-around border-t border-gray-200 pt-2"
>
<div :class="`${prefixCls}-group`">
<ElAvatar :src="previewSource" size="large" />
<ElAvatar :size="48" :src="previewSource" />
<ElAvatar :size="64" :src="previewSource" />
@@ -343,3 +303,77 @@ async function handleOk() {
</div>
</Modal>
</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>

View File

@@ -33,6 +33,8 @@ const imgElRef = ref<ElRef<HTMLImageElement>>();
const cropper = ref<Cropper | null>();
const isReady = ref(false);
// TODO @puhui999这个有办法去掉么
const prefixCls = 'cropper-image';
const debounceRealTimeCropped = useDebounceFn(realTimeCropped, 80);
const getImageStyle = computed((): CSSProperties => {
@@ -45,9 +47,10 @@ const getImageStyle = computed((): CSSProperties => {
const getClass = computed(() => {
return [
prefixCls,
attrs.class,
{
'cropper-image--circled': props.circled,
[`${prefixCls}--circled`]: props.circled,
},
];
});
@@ -155,7 +158,6 @@ function getRoundedCanvas() {
:crossorigin="crossorigin"
:src="src"
:style="getImageStyle"
class="h-auto max-w-full"
/>
</div>
</template>

View File

@@ -121,7 +121,7 @@ const apiSelectRule = [
field: 'data',
title: '请求参数 JSON 格式',
props: {
autosize: true, // 特殊ele 里是 autosizeantd 里是 autoSize
autosize: true, // TODO @puhui999这里时 autoSize是 autosize 哈?和 antd 不同
type: 'textarea',
placeholder: '{"type": 1}',
},
@@ -155,7 +155,7 @@ const apiSelectRule = [
info: `data 为接口返回值,需要写一个匿名函数解析返回值为选择器 options 列表
(data: any)=>{ label: string; value: any }[]`,
props: {
autosize: true, // 特殊ele 里是 autosizeantd 里是 autoSize
autosize: true, // TODO @puhui999这里时 autoSize是 autosize 哈?和 antd 不同
rows: { minRows: 2, maxRows: 6 },
type: 'textarea',
placeholder: `

View File

@@ -39,7 +39,7 @@ export function useDictSelectRule() {
title: label,
info: '',
$required: false,
modelField: 'model-value', // 特殊ele 里是 model-valueantd 里是 value
// TODO @puhui999vben 版本里,这里有个 modelField: 'value', 需要添加么?
};
},
props(_: any, { t }: any) {

View File

@@ -197,7 +197,7 @@ async function handleSubmit(values: Recordable<any>) {
await smsResetPassword({ mobile, code, password });
ElMessage.success($t('authentication.resetPasswordSuccess'));
// 重置成功后跳转到首页
await router.push('/');
router.push('/');
} catch (error) {
console.error('重置密码失败:', error);
} finally {

View File

@@ -21,6 +21,7 @@ const emit = defineEmits<{
(e: 'success'): void;
}>();
// TODO @puhui999展示貌似不太对应该是左右不是上下哈
const [Form, formApi] = useVbenForm({
commonConfig: {
labelWidth: 70,
@@ -100,7 +101,7 @@ watch(
</script>
<template>
<div class="mt-4 md:w-full lg:w-1/2 2xl:w-2/5">
<div class="mt-16px md:w-full lg:w-1/2 2xl:w-2/5">
<Form />
</div>
</template>

View File

@@ -25,6 +25,7 @@ const avatar = computed(
() => props.profile?.avatar || preferences.app.defaultAvatar,
);
// TODO @puhui999头像上传没跑通
async function handelUpload({
file,
filename,
@@ -56,8 +57,8 @@ async function handelUpload({
</ElTooltip>
</div>
<div class="mt-8">
<ElDescriptions :column="2" border>
<ElDescriptionsItem label="用户账号">
<ElDescriptions :column="2">
<ElDescriptionsItem>
<template #label>
<div class="flex items-center">
<IconifyIcon icon="ant-design:user-outlined" class="mr-1" />
@@ -115,11 +116,7 @@ async function handelUpload({
所属岗位
</div>
</template>
{{
profile.posts && profile.posts.length > 0
? profile.posts.map((post) => post.name).join(',')
: '-'
}}
{{ profile.posts.map((post) => post.name).join(',') }}
</ElDescriptionsItem>
<ElDescriptionsItem>
<template #label>

View File

@@ -100,7 +100,7 @@ async function handleSubmit(values: Recordable<any>) {
</script>
<template>
<div class="mt-4 md:w-full lg:w-1/2 2xl:w-2/5">
<div class="mt-[16px] md:w-full lg:w-1/2 2xl:w-2/5">
<Form />
</div>
</template>

View File

@@ -100,7 +100,7 @@ const [Grid, gridApi] = useVbenVxeGrid({
toolbarConfig: {
enabled: false,
},
},
} as VxeTableGridOptions<SystemSocialUserApi.SocialUser>,
});
/** 解绑账号 */
@@ -167,16 +167,19 @@ onMounted(() => {
>
<ElCard v-for="item in allBindList" :key="item.type" class="!mb-2">
<div class="flex w-full items-center gap-4">
<!-- TODO @puhui999图片大小不太对 -->
<ElImage
:src="item.img"
style="width: 40px; height: 40px"
:width="40"
:height="40"
:alt="item.title"
:preview-disabled="true"
fit="contain"
:preview="false"
/>
<div class="flex flex-1 items-center justify-between">
<div class="flex flex-col">
<h4 class="mb-1 text-sm text-black/85 dark:text-white/85">
<h4
class="mb-[4px] text-[14px] text-black/85 dark:text-white/85"
>
{{ getDictLabel(DICT_TYPE.SYSTEM_SOCIAL_TYPE, item.type) }}
</h4>
<span class="text-black/45 dark:text-white/45">
@@ -184,9 +187,9 @@ onMounted(() => {
{{ item.socialUser?.nickname || item.socialUser?.openid }}
</template>
<template v-else>
绑定
{{ getDictLabel(DICT_TYPE.SYSTEM_SOCIAL_TYPE, item.type) }}
账号
绑定{{
getDictLabel(DICT_TYPE.SYSTEM_SOCIAL_TYPE, item.type)
}}账号
</template>
</span>
</div>

View File

@@ -158,6 +158,7 @@ function changeNodeName() {
defineExpose({ open }); // 提供 open 方法,用于打开弹窗
</script>
<template>
<!-- TODO @jasonantd 1/3这里要统一么 -->
<Drawer class="w-2/5">
<template #title>
<div class="flex items-center">

View File

@@ -40,7 +40,7 @@ defineOptions({
const props = defineProps({
modelValue: {
type: Object,
required: false,
// TODO @jason这里 required: false,
default: () => ({}),
},
});

View File

@@ -54,7 +54,7 @@ function deleteHttpRequestParam(arr: HttpRequestParam[], index: number) {
}
</script>
<template>
<ElFormItem label="请求头" label-position="top">
<ElFormItem label="请求头">
<div
v-for="(item, index) in props.header"
:key="index"
@@ -144,7 +144,7 @@ function deleteHttpRequestParam(arr: HttpRequestParam[], index: number) {
添加一行
</ElButton>
</div>
<ElFormItem label="请求体" label-position="top">
<ElFormItem label="请求体">
<div
v-for="(item, index) in props.body"
:key="index"

View File

@@ -65,7 +65,7 @@ function deleteHttpResponseSetting(
}
</script>
<template>
<ElFormItem label-position="top">
<ElFormItem>
<ElAlert
title="仅支持 POST 请求,以请求体方式接收参数"
type="warning"
@@ -75,7 +75,6 @@ function deleteHttpResponseSetting(
</ElFormItem>
<!-- 请求地址-->
<ElFormItem
label-position="top"
label="请求地址"
:prop="`${formItemPrefix}.url`"
:rules="{
@@ -94,7 +93,7 @@ function deleteHttpResponseSetting(
/>
<!-- 返回值设置-->
<div v-if="responseEnable">
<ElFormItem label="返回值" label-position="top">
<ElFormItem label="返回值">
<ElAlert
title="通过请求返回值, 可以修改流程表单的值"
type="warning"
@@ -102,7 +101,6 @@ function deleteHttpResponseSetting(
:closable="false"
/>
</ElFormItem>
<!-- todo jason 更多设置排版需要优化一下-->
<ElRow
:gutter="8"
v-for="(item, index) in setting.response"

View File

@@ -383,6 +383,7 @@ onMounted(() => {
});
</script>
<template>
<!-- TODO @jasonantd 这里是 1/3需要保持一致么 -->
<Drawer class="w-2/5">
<template #title>
<div class="config-header">

View File

@@ -497,7 +497,7 @@ onBeforeUnmount(() => {
/>
<!-- 第四步更多设置 -->
<div v-show="currentStep === 3" class="mx-auto w-4/6">
<div v-if="currentStep === 3" class="mx-auto w-4/6">
<ExtraSetting v-model="formData" ref="extraSettingRef" />
</div>
</div>

View File

@@ -102,8 +102,8 @@ onBeforeUnmount(() => {
<Modal class="w-3/4" title="自定义模板">
<div class="mb-3">
<ElAlert
title="输入 @ 可选择插入流程选项和表单选项"
type="primary"
message="输入 @ 可选择插入流程选项和表单选项"
type="info"
show-icon
/>
</div>

View File

@@ -30,8 +30,12 @@ import {
} from 'element-plus';
import { getForm } from '#/api/bpm/form';
// TODO @jason这里要迁移下么
// import {
// HttpRequestSetting,
// parseFormFields,
// } from '#/views/bpm/components/simple-process-design';
import { parseFormFields } from '#/components/form-create';
import { HttpRequestSetting } from '#/views/bpm/components/simple-process-design';
import PrintTemplate from './custom-print-template.vue';
@@ -510,17 +514,16 @@ defineExpose({ initData, validate });
</div>
</ElCol>
</ElRow>
<ElRow v-if="processBeforeTriggerEnable">
<ElCol :span="24" class="mt-6">
<!-- <HttpRequestSetting
v-model:setting="modelData.processBeforeTriggerSetting"
:response-enable="true"
form-item-prefix="processBeforeTriggerSetting"
/> -->
</ElCol>
</ElRow>
</ElFormItem>
<ElRow v-if="processBeforeTriggerEnable">
<ElCol :span="2" />
<ElCol :span="22" class="mt-2">
<HttpRequestSetting
v-model:setting="modelData.processBeforeTriggerSetting"
:response-enable="true"
form-item-prefix="processBeforeTriggerSetting"
/>
</ElCol>
</ElRow>
<ElFormItem class="mb-5" label="流程后置通知">
<ElRow class="mt-1">
<ElCol :span="24">
@@ -533,17 +536,16 @@ defineExpose({ initData, validate });
</div>
</ElCol>
</ElRow>
<ElRow v-if="processAfterTriggerEnable" class="mt-2">
<ElCol :span="24">
<HttpRequestSetting
v-model:setting="modelData.processAfterTriggerSetting"
:response-enable="true"
form-item-prefix="processAfterTriggerSetting"
/>
</ElCol>
</ElRow>
</ElFormItem>
<ElRow v-if="processAfterTriggerEnable" class="mt-1">
<ElCol :span="2" />
<ElCol :span="22">
<HttpRequestSetting
v-model:setting="modelData.processAfterTriggerSetting"
:response-enable="true"
form-item-prefix="processAfterTriggerSetting"
/>
</ElCol>
</ElRow>
<ElFormItem class="mb-5" label="任务前置通知">
<ElRow class="mt-1">
<ElCol :span="24">
@@ -556,17 +558,16 @@ defineExpose({ initData, validate });
</div>
</ElCol>
</ElRow>
<ElRow v-if="taskBeforeTriggerEnable" class="mt-2">
<ElCol :span="24">
<HttpRequestSetting
v-model:setting="modelData.taskBeforeTriggerSetting"
:response-enable="true"
form-item-prefix="taskBeforeTriggerSetting"
/>
</ElCol>
</ElRow>
</ElFormItem>
<ElRow v-if="taskBeforeTriggerEnable" class="mt-1">
<ElCol :span="2" />
<ElCol :span="22">
<HttpRequestSetting
v-model:setting="modelData.taskBeforeTriggerSetting"
:response-enable="true"
form-item-prefix="taskBeforeTriggerSetting"
/>
</ElCol>
</ElRow>
<ElFormItem class="mb-5" label="任务后置通知">
<ElRow class="mt-1">
<ElCol :span="24">
@@ -579,17 +580,16 @@ defineExpose({ initData, validate });
</div>
</ElCol>
</ElRow>
<ElRow v-if="taskAfterTriggerEnable" class="mt-2">
<ElCol :span="24">
<HttpRequestSetting
v-model:setting="modelData.taskAfterTriggerSetting"
:response-enable="true"
form-item-prefix="taskAfterTriggerSetting"
/>
</ElCol>
</ElRow>
</ElFormItem>
<ElRow v-if="taskAfterTriggerEnable" class="mt-1">
<ElCol :span="2" />
<ElCol :span="22">
<HttpRequestSetting
v-model:setting="modelData.taskAfterTriggerSetting"
:response-enable="true"
form-item-prefix="taskAfterTriggerSetting"
/>
</ElCol>
</ElRow>
<ElFormItem class="mb-5" label="自定义打印模板">
<div class="flex w-full flex-col">
<div class="flex items-center">

View File

@@ -1,7 +1,8 @@
<script setup lang="ts">
import { ref, watch } from 'vue';
import { MyProcessViewer } from '#/views/bpm/components/bpmn-process-designer/package';
// TODO @jason这个貌似暂时还没迁移的样子
// import { MyProcessViewer } from '#/views/bpm/components/bpmn-process-designer/package';
defineOptions({ name: 'ProcessInstanceBpmnViewer' });
@@ -49,11 +50,11 @@ watch(
v-loading="loading"
class="h-full w-full overflow-auto rounded-lg border border-gray-200 bg-white p-4"
>
<MyProcessViewer
<!-- <MyProcessViewer
key="processViewer"
:xml="view.bpmnXml"
:view="view"
class="h-full min-h-[500px] w-full"
/>
/> -->
</div>
</template>

View File

@@ -275,7 +275,8 @@ async function openPopover(type: string) {
}
}
Object.keys(popOverVisible.value).forEach((item) => {
if (popOverVisible.value[item]) popOverVisible.value[item] = item === type;
// TODO @jason这里是不是保持和 antd 一致?
popOverVisible.value[item] = item === type;
});
}

View File

@@ -490,25 +490,16 @@ defineExpose({
@checkbox-all="handleSelectionChange"
>
<VxeColumn v-if="isComponent" type="checkbox" width="45" fixed="left" />
<VxeColumn
align="center"
title="图片"
width="80"
min-width="80"
fixed="left"
>
<!-- TODO @puhui999这里的宽度貌似有点问题图片会寄出来 -->
<VxeColumn align="center" title="图片" max-width="140" fixed="left">
<template #default="{ row }">
<div class="flex items-center justify-center overflow-hidden">
<ElImage
v-if="row.picUrl"
:src="row.picUrl"
class="h-[50px] w-[50px] cursor-pointer"
:preview-src-list="[row.picUrl]"
:preview-teleported="true"
:z-index="3000"
fit="cover"
/>
</div>
<ElImage
v-if="row.picUrl"
:src="row.picUrl"
class="h-[50px] w-[50px] cursor-pointer"
:preview-src-list="[row.picUrl]"
fit="cover"
/>
</template>
</VxeColumn>
<template v-if="formData?.specType && !isBatch">
@@ -592,24 +583,15 @@ defineExpose({
}"
>
<VxeColumn v-if="isComponent" type="checkbox" width="45" fixed="left" />
<VxeColumn
align="center"
title="图片"
width="80"
min-width="80"
fixed="left"
>
<!-- TODO @puhui999这里的宽度貌似有点问题图片会寄出来 -->
<VxeColumn align="center" title="图片" max-width="140" fixed="left">
<template #default="{ row }">
<div class="flex items-center justify-center overflow-hidden">
<ElImage
:src="row.picUrl"
class="h-[60px] w-[60px] cursor-pointer"
:preview-src-list="[row.picUrl]"
:preview-teleported="true"
:z-index="3000"
fit="cover"
/>
</div>
<ElImage
:src="row.picUrl"
class="h-[60px] w-[60px] cursor-pointer"
:preview-src-list="[row.picUrl]"
fit="cover"
/>
</template>
</VxeColumn>
<template v-if="formData?.specType">

View File

@@ -130,13 +130,12 @@ watch(
<VxeColumn field="id" align="center" title="商品编号" min-width="30" />
<VxeColumn title="商品图" min-width="80">
<template #default="{ row }">
<!-- TODO @puhui999它的 preview 貌似展示有点奇怪,不像 antd 是全屏的。。。 -->
<ElImage
v-if="row.picUrl"
:src="row.picUrl"
class="h-[30px] w-[30px] cursor-pointer"
:preview-src-list="[row.picUrl]"
:preview-teleported="true"
:z-index="3000"
fit="cover"
/>
</template>

View File

@@ -320,7 +320,7 @@ onMounted(async () => {
<ElCard class="h-full w-full" v-loading="formLoading">
<template #header>
<div class="flex items-center justify-between">
<!-- @puhui999idea 这边会有告警 -->
<!-- TODO @puhui999这里有告警需要修复下 -->
<ElTabs v-model="activeTabName" @tab-change="handleTabChange">
<ElTabPane label="基础设置" name="info" />
<ElTabPane label="价格库存" name="sku" />

View File

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

View File

@@ -1,52 +1,30 @@
<script lang="ts" setup>
import type { MallSpuApi } from '#/api/mall/product/spu';
import type { MallDiscountActivityApi } from '#/api/mall/promotion/discount/discountActivity';
import type {
PropertyAndValues,
RuleConfig,
SpuProperty,
} from '#/views/mall/product/spu/components';
import { computed, nextTick, ref } from 'vue';
import { computed, ref } from 'vue';
import { useVbenForm, useVbenModal } from '@vben/common-ui';
import {
convertToInteger,
erpCalculatePercentage,
formatToFraction,
yuanToFen,
} from '@vben/utils';
import { ElButton, ElInputNumber, ElMessage } from 'element-plus';
import { ElMessage } from 'element-plus';
import { VxeColumn } from '#/adapter/vxe-table';
import { getSpuDetailList } from '#/api/mall/product/spu';
import {
createDiscountActivity,
getDiscountActivity,
updateDiscountActivity,
} from '#/api/mall/promotion/discount/discountActivity';
import { $t } from '#/locales';
import {
getPropertyList,
SpuAndSkuList,
SpuSkuSelect,
} from '#/views/mall/product/spu/components';
import { SpuShowcase } from '#/views/mall/product/spu/components';
import { useFormSchema } from '../data';
defineOptions({ name: 'DiscountActivityForm' });
/** 折扣类型枚举 */
const PromotionDiscountTypeEnum = {
PRICE: { type: 1 }, // 满减
PERCENT: { type: 2 }, // 折扣
};
const emit = defineEmits(['success']);
// ================= 表单相关 =================
const formData = ref<Partial<MallDiscountActivityApi.DiscountActivity>>({});
const formData = ref<
Partial<MallDiscountActivityApi.DiscountActivity> & {
spuIds?: number[];
}
>({});
const getTitle = computed(() => {
return formData.value?.id
? $t('ui.actionTitle.edit', ['限时折扣活动'])
@@ -66,203 +44,28 @@ const [Form, formApi] = useVbenForm({
showDefaultActions: false,
});
// ================= 商品选择相关 =================
/** SKU 扩展类型 */
interface SkuExtension extends MallSpuApi.Sku {
productConfig: MallDiscountActivityApi.DiscountProduct;
}
/** SPU 扩展类型 */
interface SpuExtension extends MallSpuApi.Spu {
skus?: SkuExtension[];
}
const spuSelectRef = ref(); // 商品选择组件 Ref
const spuAndSkuListRef = ref(); // SKU 列表组件 Ref
const spuList = ref<SpuExtension[]>([]); // 选择的 SPU 列表
const spuPropertyList = ref<SpuProperty<SpuExtension>[]>([]); // SPU 属性列表
const spuIdList = ref<number[]>([]); // 已选择的 SPU ID 列表
/** SKU 校验规则配置 */
const ruleConfig: RuleConfig[] = [
{
name: 'productConfig.discountPrice',
rule: (arg) => arg > 0,
message: '商品优惠金额不能为 0 ',
},
];
/** 打开商品选择弹窗 */
function openSpuSelect() {
spuSelectRef.value?.open();
}
/** 选择商品后的回调 */
function handleSpuSelected(spuId: number, skuIds?: number[]) {
getSpuDetails(spuId, skuIds);
}
/** 获取 SPU 详情 */
async function getSpuDetails(
spuId: number,
skuIdArr?: number[],
products?: MallDiscountActivityApi.DiscountProduct[],
type?: string,
) {
// 如果已经包含该 SPU 则跳过
if (spuIdList.value.includes(spuId)) {
if (type !== 'load') {
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();
}
// ================= 弹窗相关 =================
// TODO @puhui999antd 和 ele 里,修改时,商品都没展示。
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
const { valid } = await formApi.validate();
if (!valid) {
return;
}
modalApi.lock();
// 提交表单
const data =
(await formApi.getValues()) as MallDiscountActivityApi.DiscountActivity;
// 校验是否选择了商品
if (spuList.value.length === 0) {
ElMessage.warning('请选择活动商品');
return;
// 确保必要的默认值
if (!data.products) {
data.products = [];
}
modalApi.lock();
try {
// 获取折扣商品配置
const products = structuredClone(
spuAndSkuListRef.value?.getSkuConfigs('productConfig') || [],
) as MallDiscountActivityApi.DiscountProduct[];
// 转换金额为分
products.forEach((item) => {
item.discountPercent = convertToInteger(item.discountPercent);
item.discountPrice = convertToInteger(item.discountPrice);
});
const data = structuredClone(
await formApi.getValues(),
) as MallDiscountActivityApi.DiscountActivity;
data.products = products;
// 提交请求
await (formData.value?.id
? updateDiscountActivity(data)
: createDiscountActivity(data));
// 关闭并提示
await modalApi.close();
emit('success');
ElMessage.success($t('ui.actionMessage.operationSuccess'));
@@ -272,45 +75,19 @@ const [Modal, modalApi] = useVbenModal({
},
async onOpenChange(isOpen: boolean) {
if (!isOpen) {
await resetForm();
formData.value = {};
return;
}
// 加载数据
const data = modalApi.getData<MallDiscountActivityApi.DiscountActivity>();
if (!data || !data.id) {
return;
}
modalApi.lock();
try {
const activityData = await getDiscountActivity(data.id);
formData.value = activityData;
// 加载商品详情
if (activityData.products && activityData.products.length > 0) {
// 按 spuId 分组
const spuProductsMap = new Map<
number,
MallDiscountActivityApi.DiscountProduct[]
>();
for (const product of activityData.products) {
const spuId = product.spuId;
if (!spuProductsMap.has(spuId)) {
spuProductsMap.set(spuId, []);
}
spuProductsMap.get(spuId)!.push(product);
}
// 加载每个 SPU 的详情
for (const [spuId, products] of spuProductsMap) {
const skuIdArr = products.map((p) => p.skuId);
await getSpuDetails(spuId, skuIdArr, products, 'load');
}
}
// 设置表单值
await formApi.setValues(activityData);
formData.value = await getDiscountActivity(data.id);
// 设置到 values
await formApi.setValues(formData.value);
} finally {
modalApi.unlock();
}
@@ -319,59 +96,12 @@ const [Modal, modalApi] = useVbenModal({
</script>
<template>
<Modal class="w-[70%]" :title="getTitle">
<Modal class="w-3/5" :title="getTitle">
<Form>
<!-- 自定义插槽商品选择 -->
<template #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>
<SpuShowcase v-model="formData.spuIds" />
</template>
</Form>
</Modal>
<!-- 商品选择弹窗 -->
<SpuSkuSelect
ref="spuSelectRef"
:is-select-sku="true"
@select="handleSpuSelected"
/>
</template>

View File

@@ -138,7 +138,6 @@ export function useFormSchema(): VbenFormSchema[] {
componentProps: {
showTime: true,
format: 'YYYY-MM-DD HH:mm:ss',
valueFormat: 'x',
placeholder: [
$t('utils.rangePicker.beginTime'),
$t('utils.rangePicker.endTime'),
@@ -223,10 +222,7 @@ export function useFormSchema(): VbenFormSchema[] {
label: '优惠设置',
component: 'Input',
formItemClass: 'items-start',
rules: z
.array(z.any())
.min(1, { message: '请添加至少一条优惠规则' })
.default([]),
rules: 'required',
},
{
fieldName: 'productScopeValues', // 隐藏字段:用于自动同步 productScopeValues

View File

@@ -8,7 +8,7 @@ import {
PromotionConditionTypeEnum,
PromotionProductScopeEnum,
} from '@vben/constants';
import { cloneDeep, convertToInteger, formatToFraction } from '@vben/utils';
import { convertToInteger, formatToFraction } from '@vben/utils';
import { ElMessage } from 'element-plus';
@@ -52,8 +52,6 @@ const [Form, formApi] = useVbenForm({
const [Modal, modalApi] = useVbenModal({
async onConfirm() {
// 在验证前同步 formData.rules 到表单中
await formApi.setFieldValue('rules', formData.value.rules || []);
const { valid } = await formApi.validate();
if (!valid) {
return;
@@ -62,25 +60,18 @@ const [Modal, modalApi] = useVbenModal({
// 提交表单
try {
const values = await formApi.getValues();
// 使用 formData.value 作为基础,确保 rules 来自 formData
const data = { ...values, ...formData.value };
const data = { ...formData.value, ...values };
if (data.startAndEndTime && Array.isArray(data.startAndEndTime)) {
// TODO @puhui999这里 ele 会告警antd 不会告警,可能要看看;
data.startTime = Number(data.startAndEndTime[0]);
data.endTime = Number(data.startAndEndTime[1]);
data.startTime = data.startAndEndTime[0];
data.endTime = data.startAndEndTime[1];
delete data.startAndEndTime;
}
// 深拷贝 rules 避免修改原始数据
const rules = cloneDeep(
data.rules,
) as unknown as MallRewardActivityApi.RewardRule[];
rules.forEach((item: any) => {
data.rules?.forEach((item: any) => {
item.discountPrice = convertToInteger(item.discountPrice || 0);
if (data.conditionType === PromotionConditionTypeEnum.PRICE.type) {
item.limit = convertToInteger(item.limit || 0);
}
});
data.rules = rules;
await (data.id
? updateRewardActivity(data as MallRewardActivityApi.RewardActivity)
: createRewardActivity(data as MallRewardActivityApi.RewardActivity));
@@ -105,11 +96,7 @@ const [Modal, modalApi] = useVbenModal({
modalApi.lock();
try {
const result = await getReward(data.id);
// valueFormat: 'x' 配置下,直接使用时间戳字符串
result.startAndEndTime = [
result.startTime ? String(result.startTime) : undefined,
result.endTime ? String(result.endTime) : undefined,
] as any[];
result.startAndEndTime = [result.startTime, result.endTime] as any[];
result.rules?.forEach((item: any) => {
item.discountPrice = formatToFraction(item.discountPrice || 0);
if (result.conditionType === PromotionConditionTypeEnum.PRICE.type) {