From 5e259eb685f70626b2d578d40d01f275a07d5469 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 25 Oct 2025 21:27:37 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E3=80=90antd=E3=80=91=E3=80=90mal?= =?UTF-8?q?l=E3=80=91diy=20=E4=B8=BB=E9=A1=B5=E9=9D=A2=E7=9A=84=E8=BF=81?= =?UTF-8?q?=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/api/mall/promotion/diy/page.ts | 20 +- .../src/api/mall/promotion/diy/template.ts | 24 +- .../src/router/routes/modules/mall.ts | 34 +++ .../promotion/components/diy-editor/index.vue | 44 ++++ .../promotion/components/diy-editor/util.ts | 125 +++++++++++ .../views/mall/promotion/components/index.ts | 2 + .../src/views/mall/promotion/diy/page/data.ts | 7 +- .../promotion/diy/page/decorate/index.vue | 64 ++++++ .../views/mall/promotion/diy/page/index.vue | 16 +- .../mall/promotion/diy/page/modules/form.vue | 8 +- .../views/mall/promotion/diy/template/data.ts | 7 +- .../promotion/diy/template/decorate/index.vue | 208 ++++++++++++++++++ .../mall/promotion/diy/template/index.vue | 34 ++- .../promotion/diy/template/modules/form.vue | 13 +- .../mall/promotion/diy/template/index.vue | 1 - 15 files changed, 552 insertions(+), 55 deletions(-) create mode 100644 apps/web-antd/src/views/mall/promotion/components/diy-editor/index.vue create mode 100644 apps/web-antd/src/views/mall/promotion/components/diy-editor/util.ts create mode 100644 apps/web-antd/src/views/mall/promotion/diy/page/decorate/index.vue create mode 100644 apps/web-antd/src/views/mall/promotion/diy/template/decorate/index.vue diff --git a/apps/web-antd/src/api/mall/promotion/diy/page.ts b/apps/web-antd/src/api/mall/promotion/diy/page.ts index d332cc09e..afdface5a 100644 --- a/apps/web-antd/src/api/mall/promotion/diy/page.ts +++ b/apps/web-antd/src/api/mall/promotion/diy/page.ts @@ -5,12 +5,18 @@ import { requestClient } from '#/api/request'; export namespace MallDiyPageApi { /** 装修页面 */ export interface DiyPage { - id?: number; // 页面编号 - templateId?: number; // 模板编号 - name: string; // 页面名称 - remark: string; // 备注 - previewPicUrls: string[]; // 预览图片地址数组 - property: string; // 页面属性 + /** 页面编号 */ + id?: number; + /** 模板编号 */ + templateId?: number; + /** 页面名称 */ + name: string; + /** 备注 */ + remark: string; + /** 预览图片地址数组 */ + previewPicUrls: string[]; + /** 页面属性 */ + property: string; } } @@ -46,7 +52,7 @@ export function deleteDiyPage(id: number) { /** 获得装修页面属性 */ export function getDiyPageProperty(id: number) { - return requestClient.get(`/promotion/diy-page/get-property?id=${id}`); + return requestClient.get(`/promotion/diy-page/get-property?id=${id}`); } /** 更新装修页面属性 */ diff --git a/apps/web-antd/src/api/mall/promotion/diy/template.ts b/apps/web-antd/src/api/mall/promotion/diy/template.ts index 9b7596e85..f7d82d352 100644 --- a/apps/web-antd/src/api/mall/promotion/diy/template.ts +++ b/apps/web-antd/src/api/mall/promotion/diy/template.ts @@ -7,18 +7,26 @@ import { requestClient } from '#/api/request'; export namespace MallDiyTemplateApi { /** 装修模板 */ export interface DiyTemplate { - id?: number; // 模板编号 - name: string; // 模板名称 - used: boolean; // 是否使用 - usedTime?: Date; // 使用时间 - remark: string; // 备注 - previewPicUrls: string[]; // 预览图片地址数组 - property: string; // 模板属性 + /** 模板编号 */ + id?: number; + /** 模板名称 */ + name: string; + /** 是否使用 */ + used: boolean; + /** 使用时间 */ + usedTime?: Date; + /** 备注 */ + remark: string; + /** 预览图片地址数组 */ + previewPicUrls: string[]; + /** 模板属性 */ + property: string; } /** 装修模板属性(包含页面列表) */ export interface DiyTemplateProperty extends DiyTemplate { - pages: MallDiyPageApi.DiyPage[]; // 页面列表 + /** 页面列表 */ + pages: MallDiyPageApi.DiyPage[]; } } diff --git a/apps/web-antd/src/router/routes/modules/mall.ts b/apps/web-antd/src/router/routes/modules/mall.ts index 870c111f5..db8fcab5e 100644 --- a/apps/web-antd/src/router/routes/modules/mall.ts +++ b/apps/web-antd/src/router/routes/modules/mall.ts @@ -71,6 +71,40 @@ const routes: RouteRecordRaw[] = [ }, ], }, + { + path: '/diy', + name: 'DiyCenter', + meta: { + title: '营销中心', + icon: 'lucide:shopping-bag', + keepAlive: true, + hideInMenu: true, + }, + children: [ + { + path: String.raw`template/decorate/:id(\d+)`, + name: 'DiyTemplateDecorate', + meta: { + title: '模板装修', + activePath: '/mall/promotion/diy-template/diy-template', + }, + component: () => + import('#/views/mall/promotion/diy/template/decorate/index.vue'), + }, + { + path: 'page/decorate/:id', + name: 'DiyPageDecorate', + meta: { + title: '页面装修', + noCache: false, + hidden: true, + activePath: '/mall/promotion/diy-template/diy-page', + }, + component: () => + import('#/views/mall/promotion/diy/page/decorate/index.vue'), + }, + ], + }, ]; export default routes; diff --git a/apps/web-antd/src/views/mall/promotion/components/diy-editor/index.vue b/apps/web-antd/src/views/mall/promotion/components/diy-editor/index.vue new file mode 100644 index 000000000..4f0124fe3 --- /dev/null +++ b/apps/web-antd/src/views/mall/promotion/components/diy-editor/index.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/apps/web-antd/src/views/mall/promotion/components/diy-editor/util.ts b/apps/web-antd/src/views/mall/promotion/components/diy-editor/util.ts new file mode 100644 index 000000000..b273e8a92 --- /dev/null +++ b/apps/web-antd/src/views/mall/promotion/components/diy-editor/util.ts @@ -0,0 +1,125 @@ +// 页面装修组件 +export interface DiyComponent { + // 用于区分同一种组件的不同实例 + uid?: number; + // 组件唯一标识 + id: string; + // 组件名称 + name: string; + // 组件图标 + icon: string; + /* + 组件位置: + top: 固定于手机顶部,例如 顶部的导航栏 + bottom: 固定于手机底部,例如 底部的菜单导航栏 + center: 位于手机中心,每个组件占一行,顺序向下排列 + 空:同center + fixed: 由组件自己决定位置,如弹窗位于手机中心、浮动按钮一般位于手机右下角 + */ + position?: '' | 'bottom' | 'center' | 'fixed' | 'top'; + // 组件属性 + property: T; +} + +// 页面装修组件库 +export interface DiyComponentLibrary { + // 组件库名称 + name: string; + // 是否展开 + extended: boolean; + // 组件列表 + components: string[]; +} + +// 组件样式 +export interface ComponentStyle { + // 背景类型 + bgType: 'color' | 'img'; + // 背景颜色 + bgColor: string; + // 背景图片 + bgImg: string; + // 外边距 + margin: number; + marginTop: number; + marginRight: number; + marginBottom: number; + marginLeft: number; + // 内边距 + padding: number; + paddingTop: number; + paddingRight: number; + paddingBottom: number; + paddingLeft: number; + // 边框圆角 + borderRadius: number; + borderTopLeftRadius: number; + borderTopRightRadius: number; + borderBottomRightRadius: number; + borderBottomLeftRadius: number; +} + +// 页面配置 +export interface PageConfig { + // 页面属性 + page: any; + // 顶部导航栏属性 + navigationBar: any; + // 底部导航菜单属性 + tabBar?: any; + // 页面组件列表 + components: PageComponent[]; +} +// 页面组件,只保留组件ID,组件属性 +export type PageComponent = Pick, 'id' | 'property'>; + +// 页面组件库 +export const PAGE_LIBS = [ + { + name: '基础组件', + extended: true, + components: [ + 'SearchBar', + 'NoticeBar', + 'MenuSwiper', + 'MenuGrid', + 'MenuList', + 'Popover', + 'FloatingActionButton', + ], + }, + { + name: '图文组件', + extended: true, + components: [ + 'ImageBar', + 'Carousel', + 'TitleBar', + 'VideoPlayer', + 'Divider', + 'MagicCube', + 'HotZone', + ], + }, + { + name: '商品组件', + extended: true, + components: ['ProductCard', 'ProductList'], + }, + { + name: '用户组件', + extended: true, + components: ['UserCard', 'UserOrder', 'UserWallet', 'UserCoupon'], + }, + { + name: '营销组件', + extended: true, + components: [ + 'PromotionCombination', + 'PromotionSeckill', + 'PromotionPoint', + 'CouponCard', + 'PromotionArticle', + ], + }, +] as DiyComponentLibrary[]; diff --git a/apps/web-antd/src/views/mall/promotion/components/index.ts b/apps/web-antd/src/views/mall/promotion/components/index.ts index b28e0c632..a382904c1 100644 --- a/apps/web-antd/src/views/mall/promotion/components/index.ts +++ b/apps/web-antd/src/views/mall/promotion/components/index.ts @@ -1,3 +1,5 @@ +export { default as DiyEditor } from './diy-editor/index.vue'; +export { type DiyComponentLibrary, PAGE_LIBS } from './diy-editor/util'; export { default as SpuAndSkuList } from './spu-and-sku-list.vue'; export { default as SpuSkuSelect } from './spu-sku-select.vue'; diff --git a/apps/web-antd/src/views/mall/promotion/diy/page/data.ts b/apps/web-antd/src/views/mall/promotion/diy/page/data.ts index 5e6682825..cec26ffcf 100644 --- a/apps/web-antd/src/views/mall/promotion/diy/page/data.ts +++ b/apps/web-antd/src/views/mall/promotion/diy/page/data.ts @@ -1,6 +1,8 @@ import type { VbenFormSchema } from '#/adapter/form'; import type { VxeTableGridOptions } from '#/adapter/vxe-table'; +import { getRangePickerDefaultProps } from '#/utils'; + /** 表单配置 */ export function useFormSchema(): VbenFormSchema[] { return [ @@ -51,7 +53,7 @@ export function useGridFormSchema(): VbenFormSchema[] { component: 'Input', componentProps: { placeholder: '请输入页面名称', - allowClear: true, + clearable: true, }, }, { @@ -59,9 +61,8 @@ export function useGridFormSchema(): VbenFormSchema[] { label: '创建时间', component: 'RangePicker', componentProps: { - placeholder: ['开始时间', '结束时间'], + ...getRangePickerDefaultProps(), allowClear: true, - valueFormat: 'YYYY-MM-DD HH:mm:ss', }, }, ]; diff --git a/apps/web-antd/src/views/mall/promotion/diy/page/decorate/index.vue b/apps/web-antd/src/views/mall/promotion/diy/page/decorate/index.vue new file mode 100644 index 000000000..d2ba2441e --- /dev/null +++ b/apps/web-antd/src/views/mall/promotion/diy/page/decorate/index.vue @@ -0,0 +1,64 @@ + + diff --git a/apps/web-antd/src/views/mall/promotion/diy/page/index.vue b/apps/web-antd/src/views/mall/promotion/diy/page/index.vue index 65bc4da59..c1af949b1 100644 --- a/apps/web-antd/src/views/mall/promotion/diy/page/index.vue +++ b/apps/web-antd/src/views/mall/promotion/diy/page/index.vue @@ -6,6 +6,8 @@ import { useRouter } from 'vue-router'; import { DocAlert, Page, useVbenModal } from '@vben/common-ui'; +import { message } from 'ant-design-vue'; + import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table'; import { deleteDiyPage, getDiyPagePage } from '#/api/mall/promotion/diy/page'; import { $t } from '#/locales'; @@ -37,7 +39,6 @@ function handleEdit(row: MallDiyPageApi.DiyPage) { formModalApi.setData(row).open(); } -// TODO @xingyu:装修未实现 /** 装修页面 */ function handleDecorate(row: MallDiyPageApi.DiyPage) { push({ name: 'DiyPageDecorate', params: { id: row.id } }); @@ -45,8 +46,17 @@ function handleDecorate(row: MallDiyPageApi.DiyPage) { /** 删除 DIY 页面 */ async function handleDelete(row: MallDiyPageApi.DiyPage) { - await deleteDiyPage(row.id as number); - handleRefresh(); + const hideLoading = message.loading({ + content: $t('ui.actionMessage.deleting', [row.name]), + duration: 0, + }); + try { + await deleteDiyPage(row.id as number); + message.success($t('ui.actionMessage.deleteSuccess', [row.name])); + handleRefresh(); + } finally { + hideLoading(); + } } const [Grid, gridApi] = useVbenVxeGrid({ diff --git a/apps/web-antd/src/views/mall/promotion/diy/page/modules/form.vue b/apps/web-antd/src/views/mall/promotion/diy/page/modules/form.vue index 5045bf240..b3e383e83 100644 --- a/apps/web-antd/src/views/mall/promotion/diy/page/modules/form.vue +++ b/apps/web-antd/src/views/mall/promotion/diy/page/modules/form.vue @@ -45,12 +45,6 @@ const [Modal, modalApi] = useVbenModal({ modalApi.lock(); // 提交表单 const data = (await formApi.getValues()) as MallDiyPageApi.DiyPage; - - // 确保必要的默认值 - if (!data.previewPicUrls) { - data.previewPicUrls = []; - } - try { await (formData.value?.id ? updateDiyPage(data) : createDiyPage(data)); // 关闭并提示 @@ -84,7 +78,7 @@ const [Modal, modalApi] = useVbenModal({ diff --git a/apps/web-antd/src/views/mall/promotion/diy/template/data.ts b/apps/web-antd/src/views/mall/promotion/diy/template/data.ts index 8c736f1ac..e3c8f1421 100644 --- a/apps/web-antd/src/views/mall/promotion/diy/template/data.ts +++ b/apps/web-antd/src/views/mall/promotion/diy/template/data.ts @@ -3,6 +3,8 @@ import type { VxeTableGridOptions } from '#/adapter/vxe-table'; import { DICT_TYPE } from '@vben/constants'; +import { getRangePickerDefaultProps } from '#/utils'; + /** 表单配置 */ export function useFormSchema(): VbenFormSchema[] { return [ @@ -53,7 +55,7 @@ export function useGridFormSchema(): VbenFormSchema[] { component: 'Input', componentProps: { placeholder: '请输入模板名称', - allowClear: true, + clearable: true, }, }, { @@ -61,9 +63,8 @@ export function useGridFormSchema(): VbenFormSchema[] { label: '创建时间', component: 'RangePicker', componentProps: { - placeholder: ['开始时间', '结束时间'], + ...getRangePickerDefaultProps(), allowClear: true, - valueFormat: 'YYYY-MM-DD HH:mm:ss', }, }, ]; diff --git a/apps/web-antd/src/views/mall/promotion/diy/template/decorate/index.vue b/apps/web-antd/src/views/mall/promotion/diy/template/decorate/index.vue new file mode 100644 index 000000000..efe233077 --- /dev/null +++ b/apps/web-antd/src/views/mall/promotion/diy/template/decorate/index.vue @@ -0,0 +1,208 @@ + + diff --git a/apps/web-antd/src/views/mall/promotion/diy/template/index.vue b/apps/web-antd/src/views/mall/promotion/diy/template/index.vue index da7be750e..ec43bcea0 100644 --- a/apps/web-antd/src/views/mall/promotion/diy/template/index.vue +++ b/apps/web-antd/src/views/mall/promotion/diy/template/index.vue @@ -43,7 +43,6 @@ function handleEdit(row: MallDiyTemplateApi.DiyTemplate) { formModalApi.setData(row).open(); } -// TODO @xingyu:装修未实现 /** 装修模板 */ function handleDecorate(row: MallDiyTemplateApi.DiyTemplate) { router.push({ name: 'DiyTemplateDecorate', params: { id: row.id } }); @@ -51,20 +50,33 @@ function handleDecorate(row: MallDiyTemplateApi.DiyTemplate) { /** 使用模板 */ async function handleUse(row: MallDiyTemplateApi.DiyTemplate) { - confirm({ - content: `是否使用模板"${row.name}"?`, - }).then(async () => { - // 发起删除 + await confirm(`是否使用模板"${row.name}"?`); + const hideLoading = message.loading({ + content: `正在使用模板"${row.name}"...`, + duration: 0, + }); + try { await useDiyTemplate(row.id as number); message.success('使用成功'); handleRefresh(); - }); + } finally { + hideLoading(); + } } -/** 删除DIY模板 */ +/** 删除 DIY 模板 */ async function handleDelete(row: MallDiyTemplateApi.DiyTemplate) { - await deleteDiyTemplate(row.id as number); - handleRefresh(); + const hideLoading = message.loading({ + content: $t('ui.actionMessage.deleting', [row.name]), + duration: 0, + }); + try { + await deleteDiyTemplate(row.id as number); + message.success($t('ui.actionMessage.deleteSuccess', [row.name])); + handleRefresh(); + } finally { + hideLoading(); + } } const [Grid, gridApi] = useVbenVxeGrid({ @@ -142,14 +154,14 @@ const [Grid, gridApi] = useVbenVxeGrid({ }, { label: '使用', - type: 'link' as const, + type: 'link', auth: ['promotion:diy-template:use'], ifShow: !row.used, onClick: handleUse.bind(null, row), }, { label: $t('common.delete'), - type: 'link' as const, + type: 'link', danger: true, icon: ACTION_ICON.DELETE, auth: ['promotion:diy-template:delete'], diff --git a/apps/web-antd/src/views/mall/promotion/diy/template/modules/form.vue b/apps/web-antd/src/views/mall/promotion/diy/template/modules/form.vue index c262b70c7..339c949e9 100644 --- a/apps/web-antd/src/views/mall/promotion/diy/template/modules/form.vue +++ b/apps/web-antd/src/views/mall/promotion/diy/template/modules/form.vue @@ -16,9 +16,7 @@ import { $t } from '#/locales'; import { useFormSchema } from '../data'; -/** 提交表单 */ const emit = defineEmits(['success']); - const formData = ref(); const getTitle = computed(() => { return formData.value?.id @@ -47,15 +45,6 @@ const [Modal, modalApi] = useVbenModal({ modalApi.lock(); // 提交表单 const data = (await formApi.getValues()) as MallDiyTemplateApi.DiyTemplate; - - // 确保必要的默认值 - if (!data.previewPicUrls) { - data.previewPicUrls = []; - } - if (data.used === undefined) { - data.used = false; - } - try { await (formData.value?.id ? updateDiyTemplate(data) @@ -91,7 +80,7 @@ const [Modal, modalApi] = useVbenModal({ diff --git a/apps/web-ele/src/views/mall/promotion/diy/template/index.vue b/apps/web-ele/src/views/mall/promotion/diy/template/index.vue index 456b50d13..df68ff883 100644 --- a/apps/web-ele/src/views/mall/promotion/diy/template/index.vue +++ b/apps/web-ele/src/views/mall/promotion/diy/template/index.vue @@ -51,7 +51,6 @@ function handleDecorate(row: MallDiyTemplateApi.DiyTemplate) { /** 使用模板 */ async function handleUse(row: MallDiyTemplateApi.DiyTemplate) { await confirm(`是否使用模板"${row.name}"?`); - const loadingInstance = ElLoading.service({ text: `正在使用模板"${row.name}"...`, });