feat: 优化 diy editor 样式

This commit is contained in:
xingyu4j
2025-11-05 14:09:38 +08:00
parent 769c56aeff
commit 57f39fbc90
12 changed files with 249 additions and 404 deletions

View File

@@ -5,7 +5,7 @@ import { computed } from 'vue';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { Button, Tooltip } from 'ant-design-vue'; import { Button } from 'ant-design-vue';
import { VerticalButtonGroup } from '#/views/mall/promotion/components'; import { VerticalButtonGroup } from '#/views/mall/promotion/components';
@@ -96,7 +96,7 @@ const handleDeleteComponent = () => {
<div :style="style"> <div :style="style">
<component :is="component.id" :property="component.property" /> <component :is="component.id" :property="component.property" />
</div> </div>
<div class="component-wrap absolute left-[-2px] top-0 block h-full w-full"> <div class="component-wrap absolute -left-0.5 top-1 block h-full w-full">
<!-- 左侧组件名悬浮的小贴条 --> <!-- 左侧组件名悬浮的小贴条 -->
<div class="component-name" v-if="component.name"> <div class="component-name" v-if="component.name">
{{ component.name }} {{ component.name }}
@@ -107,32 +107,52 @@ const handleDeleteComponent = () => {
v-if="showToolbar && component.name && active" v-if="showToolbar && component.name && active"
> >
<VerticalButtonGroup type="primary"> <VerticalButtonGroup type="primary">
<Tooltip title="上移" placement="right">
<Button <Button
:disabled="!canMoveUp" :disabled="!canMoveUp"
@click.stop="handleMoveComponent(-1)" @click.stop="handleMoveComponent(-1)"
v-tippy="{
content: '上移',
delay: 100,
placement: 'right',
arrow: true,
}"
> >
<IconifyIcon icon="ep:arrow-up" /> <IconifyIcon icon="ep:arrow-up" />
</Button> </Button>
</Tooltip>
<Tooltip title="下移" placement="right">
<Button <Button
:disabled="!canMoveDown" :disabled="!canMoveDown"
@click.stop="handleMoveComponent(1)" @click.stop="handleMoveComponent(1)"
v-tippy="{
content: '下移',
delay: 100,
placement: 'right',
arrow: true,
}"
> >
<IconifyIcon icon="ep:arrow-down" /> <IconifyIcon icon="ep:arrow-down" />
</Button> </Button>
</Tooltip> <Button
<Tooltip title="复制" placement="right"> @click.stop="handleCopyComponent()"
<Button @click.stop="handleCopyComponent()"> v-tippy="{
content: '复制',
delay: 100,
placement: 'right',
arrow: true,
}"
>
<IconifyIcon icon="ep:copy-document" /> <IconifyIcon icon="ep:copy-document" />
</Button> </Button>
</Tooltip> <Button
<Tooltip title="删除" placement="right"> @click.stop="handleDeleteComponent()"
<Button @click.stop="handleDeleteComponent()"> v-tippy="{
content: '删除',
delay: 100,
placement: 'right',
arrow: true,
}"
>
<IconifyIcon icon="ep:delete" /> <IconifyIcon icon="ep:delete" />
</Button> </Button>
</Tooltip>
</VerticalButtonGroup> </VerticalButtonGroup>
</div> </div>
</div> </div>
@@ -149,7 +169,7 @@ $toolbar-position: -55px;
.component-wrap { .component-wrap {
/* 鼠标放到组件上时 */ /* 鼠标放到组件上时 */
&:hover { &:hover {
border: $hover-border-width dashed var(--ant-color-primary); border: $hover-border-width dashed var(--primary);
box-shadow: 0 0 5px 0 rgb(24 144 255 / 30%); box-shadow: 0 0 5px 0 rgb(24 144 255 / 30%);
.component-name { .component-name {
@@ -170,9 +190,9 @@ $toolbar-position: -55px;
height: 25px; height: 25px;
font-size: 12px; font-size: 12px;
line-height: 25px; line-height: 25px;
color: #6a6a6a; color: hsl(var(--text-color));
text-align: center; text-align: center;
background: #fff; background: hsl(var(--background));
box-shadow: box-shadow:
0 0 4px #00000014, 0 0 4px #00000014,
0 2px 6px #0000000f, 0 2px 6px #0000000f,
@@ -187,7 +207,7 @@ $toolbar-position: -55px;
height: 0; height: 0;
content: ' '; content: ' ';
border: 5px solid transparent; border: 5px solid transparent;
border-left-color: #fff; border-left-color: hsl(var(--background));
} }
} }
@@ -207,7 +227,7 @@ $toolbar-position: -55px;
height: 0; height: 0;
content: ' '; content: ' ';
border: 5px solid transparent; border: 5px solid transparent;
border-right-color: #2d8cf0; border-right-color: hsl(var(--primary));
} }
} }
} }
@@ -218,7 +238,7 @@ $toolbar-position: -55px;
.component-wrap { .component-wrap {
margin-bottom: $active-border-width + $active-border-width; margin-bottom: $active-border-width + $active-border-width;
border: $active-border-width solid var(--ant-color-primary) !important; border: $active-border-width solid hsl(var(--primary)) !important;
box-shadow: 0 0 10px 0 rgb(24 144 255 / 30%); box-shadow: 0 0 10px 0 rgb(24 144 255 / 30%);
.component-name { .component-name {
@@ -227,10 +247,10 @@ $toolbar-position: -55px;
/* 防止加了边框之后位置移动 */ /* 防止加了边框之后位置移动 */
left: $name-position - $active-border-width !important; left: $name-position - $active-border-width !important;
color: #fff; color: #fff;
background: var(--ant-color-primary); background: hsl(var(--primary));
&::after { &::after {
border-left-color: var(--ant-color-primary); border-left-color: hsl(var(--primary));
} }
} }

View File

@@ -1,12 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import type { DiyComponent, DiyComponentLibrary } from '../util'; import type { DiyComponent, DiyComponentLibrary } from '../util';
import { reactive, watch } from 'vue'; import { ref, watch } from 'vue';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { cloneDeep } from '@vben/utils'; import { cloneDeep } from '@vben/utils';
import { Collapse, CollapsePanel } from 'ant-design-vue'; import { Collapse } from 'ant-design-vue';
import draggable from 'vuedraggable'; import draggable from 'vuedraggable';
import { componentConfigs } from './mobile/index'; import { componentConfigs } from './mobile/index';
@@ -19,28 +19,28 @@ const props = defineProps<{
list: DiyComponentLibrary[]; list: DiyComponentLibrary[];
}>(); }>();
const groups = reactive<any[]>([]); // 组件分组 const groups = ref<any[]>([]); // 组件分组
const extendGroups = reactive<string[]>([]); // 展开的折叠面板 const extendGroups = ref<string[]>([]); // 展开的折叠面板
/** 监听 list 属性,按照 DiyComponentLibrary 的 name 分组 */ /** 监听 list 属性,按照 DiyComponentLibrary 的 name 分组 */
watch( watch(
() => props.list, () => props.list,
() => { () => {
// 清除旧数据 // 清除旧数据
extendGroups.length = 0; extendGroups.value = [];
groups.length = 0; groups.value = [];
// 重新生成数据 // 重新生成数据
props.list.forEach((group) => { props.list.forEach((group) => {
// 是否展开分组 // 是否展开分组
if (group.extended) { if (group.extended) {
extendGroups.push(group.name); extendGroups.value.push(group.name);
} }
// 查找组件 // 查找组件
const components = group.components const components = group.components
.map((name) => componentConfigs[name] as DiyComponent<any>) .map((name) => componentConfigs[name] as DiyComponent<any>)
.filter(Boolean); .filter(Boolean);
if (components.length > 0) { if (components.length > 0) {
groups.push({ groups.value.push({
name: group.name, name: group.name,
components, components,
}); });
@@ -53,28 +53,32 @@ watch(
); );
/** 克隆组件 */ /** 克隆组件 */
const handleCloneComponent = (component: DiyComponent<any>) => { function handleCloneComponent(component: DiyComponent<any>) {
const instance = cloneDeep(component); const instance = cloneDeep(component);
instance.uid = Date.now(); instance.uid = Date.now();
return instance; return instance;
}; }
</script> </script>
<template> <template>
<aside <div
class="editor-left z-[1] w-[261px] shrink-0 select-none shadow-[8px_0_8px_-8px_rgb(0_0_0/0.12)]" class="z-[1] max-h-[calc(80vh)] w-96 shrink-0 select-none overflow-y-auto"
> >
<div class="h-full overflow-y-auto"> <Collapse
<Collapse v-model:active-key="extendGroups"> v-model:active-key="extendGroups"
<CollapsePanel :bordered="false"
v-for="group in groups" class="bg-card shadow-none"
>
<Collapse.Panel
v-for="(group, index) in groups"
:key="group.name" :key="group.name"
:header="group.name" :header="group.name"
:force-render="true"
> >
<draggable <draggable
class="flex flex-wrap items-center" class="flex flex-wrap items-center"
ghost-class="draggable-ghost" ghost-class="draggable-ghost"
item-key="index" :item-key="index.toString()"
:list="group.components" :list="group.components"
:sort="false" :sort="false"
:group="{ name: 'component', pull: 'clone', put: false }" :group="{ name: 'component', pull: 'clone', put: false }"
@@ -83,107 +87,18 @@ const handleCloneComponent = (component: DiyComponent<any>) => {
:force-fallback="false" :force-fallback="false"
> >
<template #item="{ element }"> <template #item="{ element }">
<div>
<div class="hidden text-white">组件放置区域</div>
<div <div
class="component flex h-[86px] w-[86px] cursor-move flex-col items-center justify-center border-b border-r [&:nth-of-type(3n)]:border-r-0" class="component flex h-20 w-20 cursor-move flex-col items-center justify-center hover:border-2 hover:border-blue-500"
:style="{
borderColor: 'var(--ant-color-split)',
}"
> >
<IconifyIcon <IconifyIcon
:icon="element.icon" :icon="element.icon"
:size="32" class="mb-1 size-8 text-gray-500"
class="mb-1 text-gray-500"
/> />
<span class="mt-1 text-xs">{{ element.name }}</span> <span class="mt-1 text-xs">{{ element.name }}</span>
</div> </div>
</div>
</template> </template>
</draggable> </draggable>
</CollapsePanel> </Collapse.Panel>
</Collapse> </Collapse>
</div> </div>
</aside>
</template> </template>
<style scoped lang="scss">
.editor-left {
:deep(.ant-collapse) {
border-top: none;
}
:deep(.ant-collapse-item) {
border-bottom: none;
}
:deep(.ant-collapse-content-box) {
padding: 0;
}
:deep(.ant-collapse-header) {
height: 32px;
padding: 0 24px !important;
line-height: 32px;
background-color: var(--ant-color-bg-layout);
border-bottom: none;
}
/* 组件 hover 和 active 状态(需要 CSS 变量) */
.component.active,
.component:hover {
color: var(--ant-color-white);
background: var(--ant-color-primary);
:deep(.iconify) {
color: var(--ant-color-white);
}
}
}
/* 拖拽区域全局样式 */
.drag-area {
/* 拖拽到手机区域时的样式 */
.draggable-ghost {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 40px;
/* 条纹背景 */
background: linear-gradient(
45deg,
#91a8d5 0,
#91a8d5 10%,
#94b4eb 10%,
#94b4eb 50%,
#91a8d5 50%,
#91a8d5 60%,
#94b4eb 60%,
#94b4eb
);
background-size: 1rem 1rem;
transition: all 0.5s;
span {
display: inline-block;
width: 140px;
height: 25px;
font-size: 12px;
line-height: 25px;
color: #fff;
text-align: center;
background: #5487df;
}
.component {
display: none; /* 拖拽时隐藏组件 */
}
.hidden {
display: block !important; /* 拖拽时显示占位提示 */
}
}
}
</style>

View File

@@ -5,6 +5,8 @@ import { ref } from 'vue';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { Carousel, Image } from 'ant-design-vue';
/** 轮播图 */ /** 轮播图 */
defineOptions({ name: 'Carousel' }); defineOptions({ name: 'Carousel' });
@@ -16,12 +18,13 @@ const handleIndexChange = (index: number) => {
}; };
</script> </script>
<template> <template>
<div>
<!-- 无图片 --> <!-- 无图片 -->
<div <div
class="flex h-[250px] items-center justify-center bg-gray-300" class="bg-card flex h-64 items-center justify-center"
v-if="property.items.length === 0" v-if="property.items.length === 0"
> >
<IconifyIcon icon="tdesign:image" class="text-[120px] text-gray-800" /> <IconifyIcon icon="tdesign:image" class="text-3xl text-gray-800" />
</div> </div>
<div v-else class="relative"> <div v-else class="relative">
<Carousel <Carousel
@@ -29,7 +32,7 @@ const handleIndexChange = (index: number) => {
:autoplay-speed="property.interval * 1000" :autoplay-speed="property.interval * 1000"
:dots="property.indicator !== 'number'" :dots="property.indicator !== 'number'"
@change="handleIndexChange" @change="handleIndexChange"
class="h-[174px]" class="h-44"
> >
<div v-for="(item, index) in property.items" :key="index"> <div v-for="(item, index) in property.items" :key="index">
<Image <Image
@@ -41,11 +44,10 @@ const handleIndexChange = (index: number) => {
</Carousel> </Carousel>
<div <div
v-if="property.indicator === 'number'" v-if="property.indicator === 'number'"
class="absolute bottom-[10px] right-[10px] rounded-xl bg-black px-[8px] py-[2px] text-[10px] text-white opacity-40" class="absolute bottom-2.5 right-2.5 rounded-xl bg-black px-2 py-1 text-xs text-white opacity-40"
> >
{{ currentIndex }} / {{ property.items.length }} {{ currentIndex }} / {{ property.items.length }}
</div> </div>
</div> </div>
</div>
</template> </template>
<style scoped lang="scss"></style>

View File

@@ -38,12 +38,12 @@ const formData = useVModel(props, 'modelValue', emit);
<RadioGroup v-model="formData.type"> <RadioGroup v-model="formData.type">
<Tooltip class="item" content="默认" placement="bottom"> <Tooltip class="item" content="默认" placement="bottom">
<RadioButton value="default"> <RadioButton value="default">
<IconifyIcon icon="system-uicons:carousel" /> <IconifyIcon icon="system-uicons:carousel" class="size-6" />
</RadioButton> </RadioButton>
</Tooltip> </Tooltip>
<Tooltip class="item" content="卡片" placement="bottom"> <Tooltip class="item" content="卡片" placement="bottom">
<RadioButton value="card"> <RadioButton value="card">
<IconifyIcon icon="ic:round-view-carousel" /> <IconifyIcon icon="ic:round-view-carousel" class="size-6" />
</RadioButton> </RadioButton>
</Tooltip> </Tooltip>
</RadioGroup> </RadioGroup>
@@ -90,7 +90,7 @@ const formData = useVModel(props, 'modelValue', emit);
draggable="false" draggable="false"
height="80px" height="80px"
width="100%" width="100%"
class="min-w-[80px]" class="min-w-20"
:show-description="false" :show-description="false"
/> />
</FormItem> </FormItem>
@@ -102,7 +102,7 @@ const formData = useVModel(props, 'modelValue', emit);
:show-description="false" :show-description="false"
height="80px" height="80px"
width="100%" width="100%"
class="min-w-[80px]" class="min-w-20"
/> />
</FormItem> </FormItem>
<FormItem label="视频" class="mb-2" label-width="40px"> <FormItem label="视频" class="mb-2" label-width="40px">
@@ -111,7 +111,7 @@ const formData = useVModel(props, 'modelValue', emit);
:file-type="['mp4']" :file-type="['mp4']"
:limit="1" :limit="1"
:file-size="100" :file-size="100"
class="min-w-[80px]" class="min-w-20"
/> />
</FormItem> </FormItem>
</template> </template>
@@ -124,5 +124,3 @@ const formData = useVModel(props, 'modelValue', emit);
</Form> </Form>
</ComponentContainerProperty> </ComponentContainerProperty>
</template> </template>
<style scoped lang="scss"></style>

View File

@@ -155,4 +155,3 @@ onMounted(() => {
</div> </div>
</div> </div>
</template> </template>
<style scoped lang="scss"></style>

View File

@@ -25,7 +25,7 @@ const handleActive = (index: number) => {
</script> </script>
<template> <template>
<div <div
class="absolute bottom-8 right-[calc(50%-375px/2+32px)] z-20 flex items-center gap-3" class="absolute bottom-8 right-[calc(50%-384px/2+32px)] z-20 flex items-center gap-3"
:class="[ :class="[
{ {
'flex-row': property.direction === 'horizontal', 'flex-row': property.direction === 'horizontal',
@@ -74,9 +74,9 @@ const handleActive = (index: number) => {
.modal-bg { .modal-bg {
position: absolute; position: absolute;
top: 0; top: 0;
left: calc(50% - 375px / 2); left: calc(50% - 384px / 2);
z-index: 11; z-index: 11;
width: 375px; width: 384px;
height: 100%; height: 100%;
background-color: rgb(0 0 0 / 40%); background-color: rgb(0 0 0 / 40%);
} }

View File

@@ -1,6 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { MenuGridProperty } from './config'; import type { MenuGridProperty } from './config';
import { Image } from 'ant-design-vue';
/** 宫格导航 */ /** 宫格导航 */
defineOptions({ name: 'MenuGrid' }); defineOptions({ name: 'MenuGrid' });
defineProps<{ property: MenuGridProperty }>(); defineProps<{ property: MenuGridProperty }>();
@@ -11,13 +13,13 @@ defineProps<{ property: MenuGridProperty }>();
<div <div
v-for="(item, index) in property.list" v-for="(item, index) in property.list"
:key="index" :key="index"
class="relative flex flex-col items-center pb-3.5 pt-5" class="relative flex flex-col items-center pb-4 pt-4"
:style="{ width: `${100 * (1 / property.column)}%` }" :style="{ width: `${100 * (1 / property.column)}%` }"
> >
<!-- 右上角角标 --> <!-- 右上角角标 -->
<span <span
v-if="item.badge?.show" v-if="item.badge?.show"
class="absolute left-1/2 top-2.5 z-10 h-5 rounded-full px-1.5 text-center text-xs leading-5" class="absolute left-1/2 top-2 z-10 h-4 rounded-full px-2 text-center text-xs leading-5"
:style="{ :style="{
color: item.badge.textColor, color: item.badge.textColor,
backgroundColor: item.badge.bgColor, backgroundColor: item.badge.bgColor,
@@ -27,7 +29,7 @@ defineProps<{ property: MenuGridProperty }>();
</span> </span>
<Image <Image
v-if="item.iconUrl" v-if="item.iconUrl"
class="h-7 w-7" :width="32"
:src="item.iconUrl" :src="item.iconUrl"
:preview="false" :preview="false"
/> />
@@ -46,5 +48,3 @@ defineProps<{ property: MenuGridProperty }>();
</div> </div>
</div> </div>
</template> </template>
<style scoped lang="scss"></style>

View File

@@ -12,7 +12,11 @@ import {
} from 'ant-design-vue'; } from 'ant-design-vue';
import UploadImg from '#/components/upload/image-upload.vue'; import UploadImg from '#/components/upload/image-upload.vue';
import { AppLinkInput, Draggable } from '#/views/mall/promotion/components'; import {
AppLinkInput,
ColorInput,
Draggable,
} from '#/views/mall/promotion/components';
import ComponentContainerProperty from '../../component-container-property.vue'; import ComponentContainerProperty from '../../component-container-property.vue';
import { EMPTY_MENU_GRID_ITEM_PROPERTY } from './config'; import { EMPTY_MENU_GRID_ITEM_PROPERTY } from './config';
@@ -53,13 +57,13 @@ const formData = useVModel(props, 'modelValue', emit);
</UploadImg> </UploadImg>
</FormItem> </FormItem>
<FormItem label="标题" prop="title"> <FormItem label="标题" prop="title">
<InputWithColor <ColorInput
v-model="element.title" v-model="element.title"
v-model:color="element.titleColor" v-model:color="element.titleColor"
/> />
</FormItem> </FormItem>
<FormItem label="副标题" prop="subtitle"> <FormItem label="副标题" prop="subtitle">
<InputWithColor <ColorInput
v-model="element.subtitle" v-model="element.subtitle"
v-model:color="element.subtitleColor" v-model:color="element.subtitleColor"
/> />
@@ -72,7 +76,7 @@ const formData = useVModel(props, 'modelValue', emit);
</FormItem> </FormItem>
<template v-if="element.badge.show"> <template v-if="element.badge.show">
<FormItem label="角标内容" prop="badge.text"> <FormItem label="角标内容" prop="badge.text">
<InputWithColor <ColorInput
v-model="element.badge.text" v-model="element.badge.text"
v-model:color="element.badge.textColor" v-model:color="element.badge.textColor"
/> />
@@ -87,5 +91,3 @@ const formData = useVModel(props, 'modelValue', emit);
</Form> </Form>
</ComponentContainerProperty> </ComponentContainerProperty>
</template> </template>
<style scoped lang="scss"></style>

View File

@@ -3,6 +3,8 @@ import type { MenuSwiperItemProperty, MenuSwiperProperty } from './config';
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { Image } from 'ant-design-vue';
/** 菜单导航 */ /** 菜单导航 */
defineOptions({ name: 'MenuSwiper' }); defineOptions({ name: 'MenuSwiper' });
const props = defineProps<{ property: MenuSwiperProperty }>(); const props = defineProps<{ property: MenuSwiperProperty }>();
@@ -122,14 +124,14 @@ watch(
button { button {
width: 6px; width: 6px;
height: 6px; height: 6px;
background: #ff6000; background: hsl(var(--red));
border-radius: 6px; border-radius: 6px;
} }
} }
.ant-carousel-dot-active button { .ant-carousel-dot-active button {
width: 12px; width: 12px;
background: #ff6000; background: hsl(var(--red));
} }
} }
</style> </style>

View File

@@ -3,11 +3,11 @@ import type { DiyComponent, DiyComponentLibrary, PageConfig } from './util';
import { onMounted, ref, unref, watch } from 'vue'; import { onMounted, ref, unref, watch } from 'vue';
import { useVbenModal } from '@vben/common-ui'; import { Page, useVbenModal } from '@vben/common-ui';
import { IconifyIcon } from '@vben/icons'; import { IconifyIcon } from '@vben/icons';
import { cloneDeep, isEmpty, isString } from '@vben/utils'; import { cloneDeep, isEmpty, isString } from '@vben/utils';
import { Button, Card, QRCode, Tag, Tooltip } from 'ant-design-vue'; import { Button, Card, Col, QRCode, Row, Tag, Tooltip } from 'ant-design-vue';
import draggable from 'vuedraggable'; import draggable from 'vuedraggable';
import statusBarImg from '#/assets/imgs/diy/statusBar.png'; import statusBarImg from '#/assets/imgs/diy/statusBar.png';
@@ -287,54 +287,62 @@ onMounted(() => {
}); });
</script> </script>
<template> <template>
<div> <Page auto-content-height>
<div class="editor flex h-full flex-col">
<!-- 顶部工具栏 --> <!-- 顶部工具栏 -->
<div class="editor-header flex items-center"> <Row class="bg-card flex max-h-12 rounded-lg">
<!-- 左侧操作区 --> <!-- 左侧操作区 -->
<Col :span="8">
<slot name="toolBarLeft"></slot> <slot name="toolBarLeft"></slot>
</Col>
<!-- 中心操作区 --> <!-- 中心操作区 -->
<div class="header-center flex flex-1 items-center justify-center"> <Col :span="8">
<span>{{ title }}</span> <span class="flex h-full items-center justify-center">{{ title }}</span>
</div> </Col>
<!-- 右侧操作区 --> <!-- 右侧操作区 -->
<div class="header-right flex"> <Col :span="8">
<Button.Group
direction="vertical"
size="large"
class="flex justify-end"
>
<Tooltip title="重置"> <Tooltip title="重置">
<Button @click="handleReset"> <Button @click="handleReset">
<IconifyIcon :size="24" icon="system-uicons:reset-alt" /> <IconifyIcon class="size-6" icon="system-uicons:reset-alt" />
</Button> </Button>
</Tooltip> </Tooltip>
<Tooltip v-if="previewUrl" title="预览"> <Tooltip v-if="previewUrl" title="预览">
<Button @click="handlePreview"> <Button @click="handlePreview">
<IconifyIcon :size="24" icon="ep:view" /> <IconifyIcon class="size-6" icon="ep:view" />
</Button> </Button>
</Tooltip> </Tooltip>
<Tooltip title="保存"> <Tooltip title="保存">
<Button @click="handleSave"> <Button @click="handleSave">
<IconifyIcon :size="24" icon="ep:check" /> <IconifyIcon class="size-6" icon="ep:check" />
</Button> </Button>
</Tooltip> </Tooltip>
</div> </Button.Group>
</div> </Col>
</Row>
<!-- 中心区域 --> <!-- 中心区域 -->
<div class="editor-container h-[calc(100vh-135px)]"> <Row class="mt-4 h-[calc(80vh)]">
<!-- 左侧组件库ComponentLibrary --> <!-- 左侧组件库ComponentLibrary -->
<Col :span="6">
<ComponentLibrary <ComponentLibrary
v-if="libs && libs.length > 0" v-if="libs && libs.length > 0"
ref="componentLibrary" ref="componentLibrary"
:list="libs" :list="libs"
/> />
</Col>
<!-- 中心设计区域ComponentContainer --> <!-- 中心设计区域ComponentContainer -->
<Col :span="12">
<div <div
class="editor-center page-prop-area relative mt-4 flex w-full flex-1 flex-col justify-center overflow-hidden" class="relative flex max-h-[calc(80vh)] w-full flex-1 flex-col justify-center overflow-y-auto"
:style="{ backgroundColor: 'var(--app-content-bg-color)' }"
@click="handlePageSelected" @click="handlePageSelected"
> >
<!-- 手机顶部 --> <!-- 手机顶部 -->
<div class="editor-design-top mx-auto flex w-[375px] flex-col"> <div class="mx-auto flex w-96 flex-col">
<!-- 手机顶部状态栏 --> <!-- 手机顶部状态栏 -->
<img alt="" class="h-5 w-[375px] bg-white" :src="statusBarImg" /> <img alt="" class="bg-card h-6" :src="statusBarImg" />
<!-- 手机顶部导航栏 --> <!-- 手机顶部导航栏 -->
<ComponentContainer <ComponentContainer
v-if="showNavigationBar" v-if="showNavigationBar"
@@ -362,18 +370,20 @@ onMounted(() => {
</div> </div>
<!-- 手机页面编辑区域 --> <!-- 手机页面编辑区域 -->
<div <div
class="editor-design-center page-prop-area h-full w-full overflow-y-auto" class="min-h-full w-full"
:style="{ :style="{
backgroundColor: pageConfigComponent.property.backgroundColor, // backgroundColor: pageConfigComponent.property.backgroundColor,
backgroundImage: `url(${pageConfigComponent.property.backgroundImage})`, backgroundImage: `url(${pageConfigComponent.property.backgroundImage})`,
}" }"
> >
<div class="phone-container"> <div
class="bg-size-[auto_auto] relative mx-auto my-0 min-h-full w-96 items-center justify-center bg-no-repeat"
>
<draggable <draggable
v-model="pageComponents" v-model="pageComponents"
:animation="200" :animation="200"
:force-fallback="false" :force-fallback="false"
class="page-prop-area drag-area" class="min-h-full w-full"
filter=".component-toolbar" filter=".component-toolbar"
ghost-class="draggable-ghost" ghost-class="draggable-ghost"
group="component" group="component"
@@ -402,7 +412,7 @@ onMounted(() => {
<!-- 手机底部导航 --> <!-- 手机底部导航 -->
<div <div
v-if="showTabBar" v-if="showTabBar"
class="editor-design-bottom component mx-auto w-[375px] cursor-pointer" class="bottom-2 mx-auto mb-2 w-96 cursor-pointer"
> >
<ComponentContainer <ComponentContainer
:active="selectedComponent?.id === tabBarComponent.id" :active="selectedComponent?.id === tabBarComponent.id"
@@ -412,9 +422,7 @@ onMounted(() => {
/> />
</div> </div>
<!-- 固定布局的组件 操作按钮区 --> <!-- 固定布局的组件 操作按钮区 -->
<div <div class="absolute right-4 top-0 flex flex-col gap-2">
class="fixed-component-action-group absolute right-4 top-0 flex flex-col gap-2"
>
<Tag <Tag
v-if="showPageConfig" v-if="showPageConfig"
:color=" :color="
@@ -426,7 +434,10 @@ onMounted(() => {
size="large" size="large"
@click="handleComponentSelected(pageConfigComponent)" @click="handleComponentSelected(pageConfigComponent)"
> >
<IconifyIcon :icon="pageConfigComponent.icon" :size="12" /> <IconifyIcon
:icon="pageConfigComponent.icon"
class="mr-2 size-4"
/>
<span>{{ pageConfigComponent.name }}</span> <span>{{ pageConfigComponent.name }}</span>
</Tag> </Tag>
<template v-for="(component, index) in pageComponents" :key="index"> <template v-for="(component, index) in pageComponents" :key="index">
@@ -441,29 +452,28 @@ onMounted(() => {
@click="handleComponentSelected(component)" @click="handleComponentSelected(component)"
@close="handleDeleteComponent(index)" @close="handleDeleteComponent(index)"
> >
<IconifyIcon :icon="component.icon" :size="12" /> <IconifyIcon :icon="component.icon" class="size-4" />
<span>{{ component.name }}</span> <span>{{ component.name }}</span>
</Tag> </Tag>
</template> </template>
</div> </div>
</div> </div>
</Col>
<!-- 右侧属性面板ComponentContainerProperty --> <!-- 右侧属性面板ComponentContainerProperty -->
<aside <Col :span="6" v-if="selectedComponent?.property">
v-if="selectedComponent?.property"
class="editor-right w-[350px] shrink-0 overflow-hidden shadow-[-8px_0_8px_-8px_rgb(0_0_0/0.12)]"
>
<Card <Card
class="h-full" class="h-[calc(80vh)] px-2 py-4"
:body-style="{ padding: 0, height: 'calc(100% - 57px)' }" :body-style="{ padding: 0 }"
:head-style="{ padding: 0, minHeight: '40px' }"
> >
<!-- 组件名称 --> <!-- 组件名称 -->
<template #title> <template #title>
<div class="flex items-center gap-2"> <div class="flex h-8 items-center gap-1">
<IconifyIcon :icon="selectedComponent?.icon" color="gray" /> <IconifyIcon :icon="selectedComponent?.icon" color="gray" />
<span>{{ selectedComponent?.name }}</span> <span>{{ selectedComponent?.name }}</span>
</div> </div>
</template> </template>
<div class="property h-full overflow-y-auto p-4"> <div class="property max-h-[calc(80vh-100px)] overflow-y-auto p-4">
<component <component
:is="`${selectedComponent?.id}Property`" :is="`${selectedComponent?.id}Property`"
:key="selectedComponent?.uid || selectedComponent?.id" :key="selectedComponent?.uid || selectedComponent?.id"
@@ -471,16 +481,15 @@ onMounted(() => {
/> />
</div> </div>
</Card> </Card>
</aside> </Col>
</div> </Row>
</div>
<!-- 预览弹框 --> <!-- 预览弹框 -->
<PreviewModal title="预览" class="w-[700px]"> <PreviewModal title="预览" class="w-[700px]">
<div class="flex justify-around"> <div class="flex justify-around">
<iframe <iframe
:src="previewUrl" :src="previewUrl"
class="h-[667px] w-[375px] rounded-lg border-4 border-solid p-0.5" class="h-[667px] w-96 rounded-lg border-4 border-solid p-0.5"
></iframe> ></iframe>
<div class="flex flex-col"> <div class="flex flex-col">
<div class="text-base">手机扫码预览</div> <div class="text-base">手机扫码预览</div>
@@ -488,112 +497,5 @@ onMounted(() => {
</div> </div>
</div> </div>
</PreviewModal> </PreviewModal>
</div> </Page>
</template> </template>
<style lang="scss" scoped>
/* 手机宽度 */
$phone-width: 375px;
/* 根节点样式 */
.editor {
display: flex;
flex-direction: column;
height: 100%;
/* 顶部:工具栏 */
.editor-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 42px;
padding: 0;
background-color: var(--ant-color-bg-container);
border-bottom: solid 1px var(--ant-color-border);
/* 工具栏:右侧按钮 */
.header-right {
height: 100%;
.ant-btn {
height: 100%;
}
}
/* 隐藏工具栏按钮的边框 */
:deep(.ant-radio-button-wrapper),
:deep(.ant-btn) {
border-top: none !important;
border-bottom: none !important;
border-radius: 0 !important;
}
}
/* 中心操作区 */
.editor-container {
display: flex;
/* 右侧属性面板 */
:deep(.editor-right) {
/* 属性面板顶部:减少内边距 */
:deep(.ant-card-head) {
padding: 8px 16px;
}
/* 属性面板分组 */
:deep(.property-group) {
margin: 0 -20px;
&.ant-card {
border: none;
}
/* 属性分组名称 */
.ant-card-head {
padding: 8px 32px;
background: var(--ant-color-bg-layout);
border: none;
}
.ant-card-body {
border: none;
}
}
}
/* 中心区域 */
.editor-center {
/* 手机页面编辑区域 */
:deep(.editor-design-center) {
width: 100%;
/* 主体内容 */
.phone-container {
position: relative;
width: $phone-width;
height: 100%;
margin: 0 auto;
background-repeat: no-repeat;
background-size: 100% 100%;
.drag-area {
width: 100%;
height: 100%;
}
}
}
/* 固定布局的组件 操作按钮区 */
.fixed-component-action-group {
:deep(.ant-tag) {
border: none;
box-shadow: 0 2px 8px 0 rgb(0 0 0 / 10%);
.ant-tag-icon {
margin-right: 4px;
}
}
}
}
}
}
</style>

View File

@@ -11,7 +11,7 @@ import { IconifyIcon } from '@vben/icons';
import { useAccessStore } from '@vben/stores'; import { useAccessStore } from '@vben/stores';
import { isEmpty } from '@vben/utils'; import { isEmpty } from '@vben/utils';
import { message, Radio, RadioGroup, Tooltip } from 'ant-design-vue'; import { message, Radio, RadioGroup } from 'ant-design-vue';
import { updateDiyPageProperty } from '#/api/mall/promotion/diy/page'; import { updateDiyPageProperty } from '#/api/mall/promotion/diy/page';
import { import {
@@ -191,18 +191,18 @@ onMounted(async () => {
<template #toolBarLeft> <template #toolBarLeft>
<RadioGroup <RadioGroup
:value="selectedTemplateItem" :value="selectedTemplateItem"
class="h-full!" class="flex items-center"
size="large"
@change="handleTemplateItemChange" @change="handleTemplateItemChange"
> >
<Tooltip <template v-for="(item, index) in templateItems" :key="index">
v-for="(item, index) in templateItems"
:key="index"
:title="item.name"
>
<Radio.Button :value="index"> <Radio.Button :value="index">
<IconifyIcon :icon="item.icon" :size="24" /> <IconifyIcon
:icon="item.icon"
class="mt-2 flex size-6 items-center"
/>
</Radio.Button> </Radio.Button>
</Tooltip> </template>
</RadioGroup> </RadioGroup>
</template> </template>
</DiyEditor> </DiyEditor>

View File

@@ -1,7 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { MallPointActivityApi } from '#/api/mall/promotion/point'; import type { MallPointActivityApi } from '#/api/mall/promotion/point';
import type { RuleConfig } from '#/views/mall/product/spu/form'; import type { RuleConfig } from '#/views/mall/product/spu/form';
import type { SpuProperty } from '#/views/mall/promotion/components/types'; // TODO @puhui999有问题
// import type { SpuProperty } from '#/views/mall/promotion/components/types';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
@@ -70,7 +71,9 @@ const ruleConfig: RuleConfig[] = [
]; // SKU 规则配置 ]; // SKU 规则配置
const spuList = ref<any[]>([]); // 选择的 SPU 列表 const spuList = ref<any[]>([]); // 选择的 SPU 列表
const spuPropertyList = ref<SpuProperty<any>[]>([]); // SPU 属性列表 // TODO @puhui999有问题
// const spuPropertyList = ref<SpuProperty<any>[]>([]); // SPU 属性列表
const spuPropertyList = ref<any[]>([]); // SPU 属性列表
/** 打开商品选择器 */ /** 打开商品选择器 */
// TODO @puhui999spuSkuSelectRef.value.open is not a function // TODO @puhui999spuSkuSelectRef.value.open is not a function
@@ -123,7 +126,9 @@ async function getSpuDetails(
}); });
res.skus = selectSkus; res.skus = selectSkus;
const spuProperties: SpuProperty[] = []; // TODO @puhui999有问题
// const spuProperties: SpuProperty[] = [];
const spuProperties: any[] = [];
spuProperties.push({ spuProperties.push({
spuId: res.id!, spuId: res.id!,
spuDetail: res, spuDetail: res,