feat: 优化 diy editor 样式
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -155,4 +155,3 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -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%);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 @puhui999:spuSkuSelectRef.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,
|
||||
|
||||
Reference in New Issue
Block a user