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

View File

@@ -1,12 +1,12 @@
<script setup lang="ts">
import type { DiyComponent, DiyComponentLibrary } from '../util';
import { reactive, watch } from 'vue';
import { ref, watch } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { cloneDeep } from '@vben/utils';
import { Collapse, CollapsePanel } from 'ant-design-vue';
import { Collapse } from 'ant-design-vue';
import draggable from 'vuedraggable';
import { componentConfigs } from './mobile/index';
@@ -19,28 +19,28 @@ const props = defineProps<{
list: DiyComponentLibrary[];
}>();
const groups = reactive<any[]>([]); // 组件分组
const extendGroups = reactive<string[]>([]); // 展开的折叠面板
const groups = ref<any[]>([]); // 组件分组
const extendGroups = ref<string[]>([]); // 展开的折叠面板
/** 监听 list 属性,按照 DiyComponentLibrary 的 name 分组 */
watch(
() => props.list,
() => {
// 清除旧数据
extendGroups.length = 0;
groups.length = 0;
extendGroups.value = [];
groups.value = [];
// 重新生成数据
props.list.forEach((group) => {
// 是否展开分组
if (group.extended) {
extendGroups.push(group.name);
extendGroups.value.push(group.name);
}
// 查找组件
const components = group.components
.map((name) => componentConfigs[name] as DiyComponent<any>)
.filter(Boolean);
if (components.length > 0) {
groups.push({
groups.value.push({
name: group.name,
components,
});
@@ -53,137 +53,52 @@ watch(
);
/** 克隆组件 */
const handleCloneComponent = (component: DiyComponent<any>) => {
function handleCloneComponent(component: DiyComponent<any>) {
const instance = cloneDeep(component);
instance.uid = Date.now();
return instance;
};
}
</script>
<template>
<aside
class="editor-left z-[1] w-[261px] shrink-0 select-none shadow-[8px_0_8px_-8px_rgb(0_0_0/0.12)]"
<div
class="z-[1] max-h-[calc(80vh)] w-96 shrink-0 select-none overflow-y-auto"
>
<div class="h-full overflow-y-auto">
<Collapse v-model:active-key="extendGroups">
<CollapsePanel
v-for="group in groups"
:key="group.name"
:header="group.name"
<Collapse
v-model:active-key="extendGroups"
:bordered="false"
class="bg-card shadow-none"
>
<Collapse.Panel
v-for="(group, index) in groups"
:key="group.name"
:header="group.name"
:force-render="true"
>
<draggable
class="flex flex-wrap items-center"
ghost-class="draggable-ghost"
:item-key="index.toString()"
:list="group.components"
:sort="false"
:group="{ name: 'component', pull: 'clone', put: false }"
:clone="handleCloneComponent"
:animation="200"
:force-fallback="false"
>
<draggable
class="flex flex-wrap items-center"
ghost-class="draggable-ghost"
item-key="index"
:list="group.components"
:sort="false"
:group="{ name: 'component', pull: 'clone', put: false }"
:clone="handleCloneComponent"
:animation="200"
:force-fallback="false"
>
<template #item="{ element }">
<div>
<div class="hidden text-white">组件放置区域</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"
:style="{
borderColor: 'var(--ant-color-split)',
}"
>
<IconifyIcon
:icon="element.icon"
:size="32"
class="mb-1 text-gray-500"
/>
<span class="mt-1 text-xs">{{ element.name }}</span>
</div>
</div>
</template>
</draggable>
</CollapsePanel>
</Collapse>
</div>
</aside>
<template #item="{ element }">
<div
class="component flex h-20 w-20 cursor-move flex-col items-center justify-center hover:border-2 hover:border-blue-500"
>
<IconifyIcon
:icon="element.icon"
class="mb-1 size-8 text-gray-500"
/>
<span class="mt-1 text-xs">{{ element.name }}</span>
</div>
</template>
</draggable>
</Collapse.Panel>
</Collapse>
</div>
</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 { Carousel, Image } from 'ant-design-vue';
/** 轮播图 */
defineOptions({ name: 'Carousel' });
@@ -16,36 +18,36 @@ const handleIndexChange = (index: number) => {
};
</script>
<template>
<!-- 无图片 -->
<div
class="flex h-[250px] items-center justify-center bg-gray-300"
v-if="property.items.length === 0"
>
<IconifyIcon icon="tdesign:image" class="text-[120px] text-gray-800" />
</div>
<div v-else class="relative">
<Carousel
:autoplay="property.autoplay"
:autoplay-speed="property.interval * 1000"
:dots="property.indicator !== 'number'"
@change="handleIndexChange"
class="h-[174px]"
>
<div v-for="(item, index) in property.items" :key="index">
<Image
class="h-full w-full object-cover"
:src="item.imgUrl"
:preview="false"
/>
</div>
</Carousel>
<div>
<!-- 无图片 -->
<div
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="bg-card flex h-64 items-center justify-center"
v-if="property.items.length === 0"
>
{{ currentIndex }} / {{ property.items.length }}
<IconifyIcon icon="tdesign:image" class="text-3xl text-gray-800" />
</div>
<div v-else class="relative">
<Carousel
:autoplay="property.autoplay"
:autoplay-speed="property.interval * 1000"
:dots="property.indicator !== 'number'"
@change="handleIndexChange"
class="h-44"
>
<div v-for="(item, index) in property.items" :key="index">
<Image
class="h-full w-full object-cover"
:src="item.imgUrl"
:preview="false"
/>
</div>
</Carousel>
<div
v-if="property.indicator === 'number'"
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 }}
</div>
</div>
</div>
</template>
<style scoped lang="scss"></style>

View File

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

View File

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

View File

@@ -25,7 +25,7 @@ const handleActive = (index: number) => {
</script>
<template>
<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="[
{
'flex-row': property.direction === 'horizontal',
@@ -74,9 +74,9 @@ const handleActive = (index: number) => {
.modal-bg {
position: absolute;
top: 0;
left: calc(50% - 375px / 2);
left: calc(50% - 384px / 2);
z-index: 11;
width: 375px;
width: 384px;
height: 100%;
background-color: rgb(0 0 0 / 40%);
}

View File

@@ -1,6 +1,8 @@
<script setup lang="ts">
import type { MenuGridProperty } from './config';
import { Image } from 'ant-design-vue';
/** 宫格导航 */
defineOptions({ name: 'MenuGrid' });
defineProps<{ property: MenuGridProperty }>();
@@ -11,13 +13,13 @@ defineProps<{ property: MenuGridProperty }>();
<div
v-for="(item, index) in property.list"
: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)}%` }"
>
<!-- 右上角角标 -->
<span
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="{
color: item.badge.textColor,
backgroundColor: item.badge.bgColor,
@@ -27,7 +29,7 @@ defineProps<{ property: MenuGridProperty }>();
</span>
<Image
v-if="item.iconUrl"
class="h-7 w-7"
:width="32"
:src="item.iconUrl"
:preview="false"
/>
@@ -46,5 +48,3 @@ defineProps<{ property: MenuGridProperty }>();
</div>
</div>
</template>
<style scoped lang="scss"></style>

View File

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

View File

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

View File

@@ -3,11 +3,11 @@ import type { DiyComponent, DiyComponentLibrary, PageConfig } from './util';
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 { 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 statusBarImg from '#/assets/imgs/diy/statusBar.png';
@@ -287,54 +287,62 @@ onMounted(() => {
});
</script>
<template>
<div>
<div class="editor flex h-full flex-col">
<!-- 顶部工具栏 -->
<div class="editor-header flex items-center">
<!-- 左侧操作区 -->
<Page auto-content-height>
<!-- 顶部工具栏 -->
<Row class="bg-card flex max-h-12 rounded-lg">
<!-- 左侧操作区 -->
<Col :span="8">
<slot name="toolBarLeft"></slot>
<!-- 中心操作区 -->
<div class="header-center flex flex-1 items-center justify-center">
<span>{{ title }}</span>
</div>
<!-- 右侧操作区 -->
<div class="header-right flex">
</Col>
<!-- 中心操作区 -->
<Col :span="8">
<span class="flex h-full items-center justify-center">{{ title }}</span>
</Col>
<!-- 右侧操作区 -->
<Col :span="8">
<Button.Group
direction="vertical"
size="large"
class="flex justify-end"
>
<Tooltip title="重置">
<Button @click="handleReset">
<IconifyIcon :size="24" icon="system-uicons:reset-alt" />
<IconifyIcon class="size-6" icon="system-uicons:reset-alt" />
</Button>
</Tooltip>
<Tooltip v-if="previewUrl" title="预览">
<Button @click="handlePreview">
<IconifyIcon :size="24" icon="ep:view" />
<IconifyIcon class="size-6" icon="ep:view" />
</Button>
</Tooltip>
<Tooltip title="保存">
<Button @click="handleSave">
<IconifyIcon :size="24" icon="ep:check" />
<IconifyIcon class="size-6" icon="ep:check" />
</Button>
</Tooltip>
</div>
</div>
<!-- 中心区域 -->
<div class="editor-container h-[calc(100vh-135px)]">
<!-- 左侧组件库ComponentLibrary -->
</Button.Group>
</Col>
</Row>
<!-- 中心区域 -->
<Row class="mt-4 h-[calc(80vh)]">
<!-- 左侧组件库ComponentLibrary -->
<Col :span="6">
<ComponentLibrary
v-if="libs && libs.length > 0"
ref="componentLibrary"
:list="libs"
/>
<!-- 中心设计区域ComponentContainer -->
</Col>
<!-- 中心设计区域ComponentContainer -->
<Col :span="12">
<div
class="editor-center page-prop-area relative mt-4 flex w-full flex-1 flex-col justify-center overflow-hidden"
:style="{ backgroundColor: 'var(--app-content-bg-color)' }"
class="relative flex max-h-[calc(80vh)] w-full flex-1 flex-col justify-center overflow-y-auto"
@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
v-if="showNavigationBar"
@@ -362,18 +370,20 @@ onMounted(() => {
</div>
<!-- 手机页面编辑区域 -->
<div
class="editor-design-center page-prop-area h-full w-full overflow-y-auto"
class="min-h-full w-full"
:style="{
backgroundColor: pageConfigComponent.property.backgroundColor,
// backgroundColor: pageConfigComponent.property.backgroundColor,
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
v-model="pageComponents"
:animation="200"
:force-fallback="false"
class="page-prop-area drag-area"
class="min-h-full w-full"
filter=".component-toolbar"
ghost-class="draggable-ghost"
group="component"
@@ -402,7 +412,7 @@ onMounted(() => {
<!-- 手机底部导航 -->
<div
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
:active="selectedComponent?.id === tabBarComponent.id"
@@ -412,9 +422,7 @@ onMounted(() => {
/>
</div>
<!-- 固定布局的组件 操作按钮区 -->
<div
class="fixed-component-action-group absolute right-4 top-0 flex flex-col gap-2"
>
<div class="absolute right-4 top-0 flex flex-col gap-2">
<Tag
v-if="showPageConfig"
:color="
@@ -426,7 +434,10 @@ onMounted(() => {
size="large"
@click="handleComponentSelected(pageConfigComponent)"
>
<IconifyIcon :icon="pageConfigComponent.icon" :size="12" />
<IconifyIcon
:icon="pageConfigComponent.icon"
class="mr-2 size-4"
/>
<span>{{ pageConfigComponent.name }}</span>
</Tag>
<template v-for="(component, index) in pageComponents" :key="index">
@@ -441,46 +452,44 @@ onMounted(() => {
@click="handleComponentSelected(component)"
@close="handleDeleteComponent(index)"
>
<IconifyIcon :icon="component.icon" :size="12" />
<IconifyIcon :icon="component.icon" class="size-4" />
<span>{{ component.name }}</span>
</Tag>
</template>
</div>
</div>
<!-- 右侧属性面板ComponentContainerProperty -->
<aside
v-if="selectedComponent?.property"
class="editor-right w-[350px] shrink-0 overflow-hidden shadow-[-8px_0_8px_-8px_rgb(0_0_0/0.12)]"
</Col>
<!-- 右侧属性面板ComponentContainerProperty -->
<Col :span="6" v-if="selectedComponent?.property">
<Card
class="h-[calc(80vh)] px-2 py-4"
:body-style="{ padding: 0 }"
:head-style="{ padding: 0, minHeight: '40px' }"
>
<Card
class="h-full"
:body-style="{ padding: 0, height: 'calc(100% - 57px)' }"
>
<!-- 组件名称 -->
<template #title>
<div class="flex items-center gap-2">
<IconifyIcon :icon="selectedComponent?.icon" color="gray" />
<span>{{ selectedComponent?.name }}</span>
</div>
</template>
<div class="property h-full overflow-y-auto p-4">
<component
:is="`${selectedComponent?.id}Property`"
:key="selectedComponent?.uid || selectedComponent?.id"
v-model="selectedComponent.property"
/>
<!-- 组件名称 -->
<template #title>
<div class="flex h-8 items-center gap-1">
<IconifyIcon :icon="selectedComponent?.icon" color="gray" />
<span>{{ selectedComponent?.name }}</span>
</div>
</Card>
</aside>
</div>
</div>
</template>
<div class="property max-h-[calc(80vh-100px)] overflow-y-auto p-4">
<component
:is="`${selectedComponent?.id}Property`"
:key="selectedComponent?.uid || selectedComponent?.id"
v-model="selectedComponent.property"
/>
</div>
</Card>
</Col>
</Row>
<!-- 预览弹框 -->
<PreviewModal title="预览" class="w-[700px]">
<div class="flex justify-around">
<iframe
: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>
<div class="flex flex-col">
<div class="text-base">手机扫码预览</div>
@@ -488,112 +497,5 @@ onMounted(() => {
</div>
</div>
</PreviewModal>
</div>
</Page>
</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 { 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 {
@@ -191,18 +191,18 @@ onMounted(async () => {
<template #toolBarLeft>
<RadioGroup
:value="selectedTemplateItem"
class="h-full!"
class="flex items-center"
size="large"
@change="handleTemplateItemChange"
>
<Tooltip
v-for="(item, index) in templateItems"
:key="index"
:title="item.name"
>
<template v-for="(item, index) in templateItems" :key="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>
</Tooltip>
</template>
</RadioGroup>
</template>
</DiyEditor>

View File

@@ -1,7 +1,8 @@
<script lang="ts" setup>
import type { MallPointActivityApi } from '#/api/mall/promotion/point';
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';
@@ -70,7 +71,9 @@ const ruleConfig: RuleConfig[] = [
]; // SKU 规则配置
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
@@ -123,7 +126,9 @@ async function getSpuDetails(
});
res.skus = selectSkus;
const spuProperties: SpuProperty[] = [];
// TODO @puhui999有问题
// const spuProperties: SpuProperty[] = [];
const spuProperties: any[] = [];
spuProperties.push({
spuId: res.id!,
spuDetail: res,