feat: 优化 diy editor

This commit is contained in:
xingyu4j
2025-11-05 18:31:37 +08:00
parent f60069d662
commit 56ae9c0230
45 changed files with 213 additions and 332 deletions

View File

@@ -161,25 +161,22 @@ function handleProductCategorySelected(id: number) {
@ok="handleSubmit"
>
<div class="flex h-[500px] gap-2">
<div class="flex flex-col">
<!-- 左侧分组列表 -->
<div
class="h-full overflow-y-auto border-r border-gray-200 pr-2"
ref="groupScrollbar"
<!-- 左侧分组列表 -->
<div
class="flex h-full flex-col overflow-y-auto border-r border-gray-200 pr-2"
ref="groupScrollbar"
>
<Button
v-for="(group, groupIndex) in APP_LINK_GROUP_LIST"
:key="groupIndex"
class="!ml-0 mb-1 mr-4 !justify-start"
:class="[{ active: activeGroup === group.name }]"
ref="groupBtnRefs"
:type="activeGroup === group.name ? 'primary' : 'default'"
@click="handleGroupSelected(group.name)"
>
<Button
v-for="(group, groupIndex) in APP_LINK_GROUP_LIST"
:key="groupIndex"
class="!ml-0 mb-1 mr-4 !justify-start"
:class="[{ active: activeGroup === group.name }]"
ref="groupBtnRefs"
:type="activeGroup === group.name ? 'primary' : 'default'"
:ghost="activeGroup !== group.name"
@click="handleGroupSelected(group.name)"
>
{{ group.name }}
</Button>
</div>
{{ group.name }}
</Button>
</div>
<!-- 右侧链接列表 -->
<div

View File

@@ -48,17 +48,13 @@ watch(
<template>
<Input v-model:value="appLink" placeholder="输入或选择链接">
<template #addonAfter>
<Button @click="handleOpenDialog" class="!border-none">选择</Button>
<Button
@click="handleOpenDialog"
class="!border-none !bg-transparent !p-0"
>
选择
</Button>
</template>
</Input>
<AppLinkSelectDialog ref="dialogRef" @change="handleLinkSelected" />
</template>
<style scoped lang="scss">
:deep(.ant-input-group-addon) {
padding: 0;
background: transparent;
border: 0;
}
</style>

View File

@@ -3,7 +3,6 @@ import type { ComponentStyle } from '../util';
import { useVModel } from '@vueuse/core';
import {
Card,
Col,
Form,
FormItem,
@@ -135,7 +134,8 @@ function handleSliderChange(prop: string) {
<!-- 每个组件的通用内容 -->
<TabPane tab="样式" key="style" force-render>
<Card title="组件样式" class="property-group">
<p class="text-lg font-bold">组件样式</p>
<div class="flex flex-col gap-2 rounded-md p-4 shadow-lg">
<Form :model="formData">
<FormItem label="组件背景" name="bgType">
<RadioGroup v-model:value="formData.bgType">
@@ -196,7 +196,7 @@ function handleSliderChange(prop: string) {
</Tree>
<slot name="style" :style="formData"></slot>
</Form>
</Card>
</div>
</TabPane>
</Tabs>
</template>

View File

@@ -117,7 +117,7 @@ const handleDeleteComponent = () => {
arrow: true,
}"
>
<IconifyIcon icon="ep:arrow-up" />
<IconifyIcon icon="lucide:arrow-up" />
</Button>
<Button
:disabled="!canMoveDown"
@@ -129,7 +129,7 @@ const handleDeleteComponent = () => {
arrow: true,
}"
>
<IconifyIcon icon="ep:arrow-down" />
<IconifyIcon icon="lucide:arrow-down" />
</Button>
<Button
@click.stop="handleCopyComponent()"
@@ -140,7 +140,7 @@ const handleDeleteComponent = () => {
arrow: true,
}"
>
<IconifyIcon icon="ep:copy-document" />
<IconifyIcon icon="lucide:copy" />
</Button>
<Button
@click.stop="handleDeleteComponent()"
@@ -151,7 +151,7 @@ const handleDeleteComponent = () => {
arrow: true,
}"
>
<IconifyIcon icon="ep:delete" />
<IconifyIcon icon="lucide:trash-2" />
</Button>
</VerticalButtonGroup>
</div>

View File

@@ -61,13 +61,11 @@ function handleCloneComponent(component: DiyComponent<any>) {
</script>
<template>
<div
class="z-[1] max-h-[calc(80vh)] w-96 shrink-0 select-none overflow-y-auto"
>
<div class="z-[1] max-h-[calc(80vh)] shrink-0 select-none overflow-y-auto">
<Collapse
v-model:active-key="extendGroups"
:bordered="false"
class="bg-card shadow-none"
class="bg-card"
>
<Collapse.Panel
v-for="(group, index) in groups"

View File

@@ -5,7 +5,6 @@ import { IconifyIcon } from '@vben/icons';
import { useVModel } from '@vueuse/core';
import {
Card,
Form,
FormItem,
Radio,
@@ -33,7 +32,8 @@ const formData = useVModel(props, 'modelValue', emit);
<template>
<ComponentContainerProperty v-model="formData.style">
<Form label-width="80px" :model="formData">
<Card header="样式设置" class="property-group" shadow="never">
<p class="text-base font-bold">样式设置</p>
<div class="flex flex-col gap-2 rounded-md p-4 shadow-lg">
<FormItem label="样式" prop="type">
<RadioGroup v-model="formData.type">
<Tooltip class="item" content="默认" placement="bottom">
@@ -69,8 +69,9 @@ const formData = useVModel(props, 'modelValue', emit);
/>
<p class="text-info">单位</p>
</FormItem>
</Card>
<Card header="内容设置" class="property-group" shadow="never">
</div>
<p class="text-base font-bold">内容设置</p>
<div class="flex flex-col gap-2 rounded-md p-4 shadow-lg">
<Draggable v-model="formData.items" :empty-item="{ type: 'img' }">
<template #default="{ element }">
<FormItem label="类型" prop="type" class="mb-2" label-width="40px">
@@ -120,7 +121,7 @@ const formData = useVModel(props, 'modelValue', emit);
</FormItem>
</template>
</Draggable>
</Card>
</div>
</Form>
</ComponentContainerProperty>
</template>

View File

@@ -25,5 +25,3 @@ defineProps<{ property: DividerProperty }>();
></div>
</div>
</template>
<style scoped lang="scss"></style>

View File

@@ -61,7 +61,10 @@ const formData = useVModel(props, 'modelValue', emit);
:title="item.text"
>
<RadioButton :value="item.type">
<IconifyIcon :icon="item.icon" />
<IconifyIcon
:icon="item.icon"
class="inset-0 size-6 items-center"
/>
</RadioButton>
</Tooltip>
</RadioGroup>
@@ -74,12 +77,18 @@ const formData = useVModel(props, 'modelValue', emit);
<RadioGroup v-model:value="formData!.paddingType">
<Tooltip title="无边距" placement="top">
<RadioButton value="none">
<IconifyIcon icon="tabler:box-padding" />
<IconifyIcon
icon="tabler:box-padding"
class="inset-0 size-6 items-center"
/>
</RadioButton>
</Tooltip>
<Tooltip title="左右留边" placement="top">
<RadioButton value="horizontal">
<IconifyIcon icon="vaadin:padding" />
<IconifyIcon
icon="vaadin:padding"
class="inset-0 size-6 items-center"
/>
</RadioButton>
</Tooltip>
</RadioGroup>

View File

@@ -33,7 +33,7 @@ const handleActive = (index: number) => {
<Image :src="item.imgUrl" fit="contain" class="h-full w-full">
<template #error>
<div class="flex h-full w-full items-center justify-center">
<IconifyIcon icon="ep:picture" />
<IconifyIcon icon="lucide:image" />
</div>
</template>
</Image>

View File

@@ -62,7 +62,7 @@ onMounted(() => {
});
</script>
<template>
<div class="z-10 min-h-[30px]" wrap-class="w-full" ref="containerRef">
<div class="z-10 min-h-8" wrap-class="w-full" ref="containerRef">
<div
class="flex flex-row text-xs"
:style="{

View File

@@ -16,7 +16,6 @@ import { floatToFixed2 } from '@vben/utils';
import { useVModel } from '@vueuse/core';
import {
Button,
Card,
Form,
FormItem,
RadioButton,
@@ -82,7 +81,8 @@ watch(
<template>
<ComponentContainerProperty v-model="formData.style">
<Form :model="formData">
<Card title="优惠券列表" class="property-group">
<p class="text-base font-bold">优惠券列表</p>
<div class="flex flex-col gap-2 rounded-md p-4 shadow-lg">
<div
v-for="(coupon, index) in couponList"
:key="index"
@@ -118,15 +118,16 @@ watch(
添加
</Button>
</FormItem>
</Card>
<Card title="优惠券样式" class="property-group">
</div>
<p class="text-base font-bold">优惠券样式:</p>
<div class="flex flex-col gap-2 rounded-md p-4 shadow-lg">
<FormItem label="列数" name="type">
<RadioGroup v-model:value="formData.columns">
<Tooltip title="一列" placement="bottom">
<RadioButton :value="1">
<IconifyIcon
icon="fluent:text-column-one-24-filled"
class="size-6"
class="inset-0 size-6 items-center"
/>
</RadioButton>
</Tooltip>
@@ -169,7 +170,7 @@ watch(
<FormItem label="间隔" name="space">
<Slider v-model:value="formData.space" :max="100" :min="0" />
</FormItem>
</Card>
</div>
</Form>
</ComponentContainerProperty>

View File

@@ -44,9 +44,9 @@ const handleActive = (index: number) => {
<template #error>
<div class="flex h-full w-full items-center justify-center">
<IconifyIcon
icon="ep:picture"
icon="lucide:image"
:color="item.textColor"
class="size-6"
class="inset-0 size-6 items-center"
/>
</div>
</template>
@@ -63,7 +63,7 @@ const handleActive = (index: number) => {
<!-- todo: @owen 使用APP主题色 -->
<Button type="primary" size="large" circle @click="handleToggleFab">
<IconifyIcon
icon="ep:plus"
icon="lucide:plus"
class="fab-icon"
:class="[{ active: expanded }]"
/>

View File

@@ -2,14 +2,7 @@
import type { FloatingActionButtonProperty } from './config';
import { useVModel } from '@vueuse/core';
import {
Card,
Form,
FormItem,
Radio,
RadioGroup,
Switch,
} from 'ant-design-vue';
import { Form, FormItem, Radio, RadioGroup, Switch } from 'ant-design-vue';
import UploadImg from '#/components/upload/image-upload.vue';
import {
@@ -30,7 +23,8 @@ const formData = useVModel(props, 'modelValue', emit);
<template>
<Form :model="formData">
<Card title="按钮配置" class="property-group">
<p class="text-base font-bold">按钮配置</p>
<div class="flex flex-col gap-2 rounded-md p-4 shadow-lg">
<FormItem label="展开方向" name="direction">
<RadioGroup v-model:value="formData.direction">
<Radio value="vertical">垂直</Radio>
@@ -40,8 +34,9 @@ const formData = useVModel(props, 'modelValue', emit);
<FormItem label="显示文字" name="showText">
<Switch v-model:checked="formData.showText" />
</FormItem>
</Card>
<Card title="按钮列表" class="property-group">
</div>
<p class="text-base font-bold">按钮列表</p>
<div class="flex flex-col gap-2 rounded-md p-4 shadow-lg">
<Draggable v-model="formData.list" :empty-item="{ textColor: '#fff' }">
<template #default="{ element, index }">
<FormItem label="图标" :name="`list[${index}].imgUrl`">
@@ -63,6 +58,6 @@ const formData = useVModel(props, 'modelValue', emit);
</FormItem>
</template>
</Draggable>
</Card>
</div>
</Form>
</template>

View File

@@ -200,10 +200,10 @@ const handleAppLinkChange = (appLink: AppLink) => {
height: `${item.height}px`,
top: `${item.top}px`,
left: `${item.left}px`,
color: 'var(--ant-color-primary)',
color: 'hsl(var(--primary))',
background:
'color-mix(in srgb, var(--ant-color-primary) 30%, transparent)',
borderColor: 'var(--ant-color-primary)',
'color-mix(in srgb, hsl(var(--primary)) 30%, transparent)',
borderColor: 'hsl(var(--primary))',
}"
@mousedown="handleMove(item, $event)"
@dblclick="handleShowAppLinkDialog(item)"
@@ -212,10 +212,9 @@ const handleAppLinkChange = (appLink: AppLink) => {
{{ item.name || '双击选择链接' }}
</span>
<IconifyIcon
icon="ep:close"
class="absolute right-0 top-0 hidden cursor-pointer rounded-bl-[80%] p-[2px_2px_6px_6px] text-right text-white group-hover:block"
:style="{ backgroundColor: 'var(--ant-color-primary)' }"
:size="14"
icon="lucide:x"
class="absolute inset-0 right-0 top-0 hidden size-6 cursor-pointer items-center rounded-bl-[80%] p-[2px_2px_6px_6px] text-right text-white group-hover:block"
:style="{ backgroundColor: 'hsl(var(--primary))' }"
@click="handleRemove(item)"
/>
@@ -232,7 +231,7 @@ const handleAppLinkChange = (appLink: AppLink) => {
<template #prepend-footer>
<Button @click="handleAdd" type="primary" ghost>
<template #icon>
<IconifyIcon icon="ep:plus" />
<IconifyIcon icon="lucide:plus" />
</template>
添加热区
</Button>

View File

@@ -16,7 +16,7 @@ const props = defineProps<{ property: HotZoneProperty }>();
<div
v-for="(item, index) in props.property.list"
:key="index"
class="hot-zone"
class="bg-primary-700 absolute z-10 flex cursor-move items-center justify-center border text-sm opacity-80"
:style="{
width: `${item.width}px`,
height: `${item.height}px`,
@@ -24,23 +24,9 @@ const props = defineProps<{ property: HotZoneProperty }>();
left: `${item.left}px`,
}"
>
{{ item.name }}
<p class="text-primary">
{{ item.name }}
</p>
</div>
</div>
</template>
<style scoped lang="scss">
.hot-zone {
position: absolute;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: var(--el-color-primary);
cursor: move;
background: var(--el-color-primary-light-7);
border: 1px solid var(--el-color-primary);
opacity: 0.8;
}
</style>

View File

@@ -4,7 +4,7 @@ import type { HotZoneProperty } from './config';
import { ref } from 'vue';
import { useVModel } from '@vueuse/core';
import { Button, Form, FormItem, Typography } from 'ant-design-vue';
import { Button, Form, FormItem } from 'ant-design-vue';
import UploadImg from '#/components/upload/image-upload.vue';
@@ -40,19 +40,14 @@ const handleOpenEditDialog = () => {
v-model="formData.imgUrl"
height="50px"
width="auto"
class="min-w-[80px]"
class="min-w-20"
:show-description="false"
>
<template #tip>
<Typography.Text type="secondary" class="text-xs">
推荐宽度 750
</Typography.Text>
</template>
</UploadImg>
/>
</FormItem>
<p class="text-center text-sm text-gray-500">推荐宽度 750</p>
</Form>
<Button type="primary" class="w-full" @click="handleOpenEditDialog">
<Button type="primary" class="mt-4 w-full" @click="handleOpenEditDialog">
设置热区
</Button>
</ComponentContainerProperty>

View File

@@ -11,7 +11,7 @@ export interface ImageBarProperty {
export const component = {
id: 'ImageBar',
name: '图片展示',
icon: 'ep:picture',
icon: 'lucide:image',
property: {
imgUrl: '',
url: '',

View File

@@ -13,10 +13,10 @@ defineProps<{ property: ImageBarProperty }>();
<template>
<!-- 无图片 -->
<div
class="flex h-12 items-center justify-center bg-gray-300"
class="bg-card flex h-12 items-center justify-center"
v-if="!property.imgUrl"
>
<IconifyIcon icon="ep:picture" class="text-3xl text-gray-600" />
<IconifyIcon icon="lucide:image" class="text-3xl text-gray-600" />
</div>
<Image
class="block h-full min-h-8 w-full"

View File

@@ -30,11 +30,9 @@ const formData = useVModel(props, 'modelValue', emit);
draggable="false"
height="80px"
width="100%"
class="min-w-[80px]"
class="min-w-20"
:show-description="false"
>
<template #tip> 建议宽度750 </template>
</UploadImg>
/>
</FormItem>
<FormItem label="链接" prop="url">
<AppLinkInput v-model="formData.url" />
@@ -42,5 +40,3 @@ const formData = useVModel(props, 'modelValue', emit);
</Form>
</ComponentContainerProperty>
</template>
<style scoped lang="scss"></style>

View File

@@ -5,6 +5,8 @@ import { computed } from 'vue';
import { IconifyIcon } from '@vben/icons';
import { Image } from 'ant-design-vue';
/** 广告魔方 */
defineOptions({ name: 'MagicCube' });
const props = defineProps<{ property: MagicCubeProperty }>();
@@ -78,5 +80,3 @@ const rowCount = computed(() => {
</div>
</div>
</template>
<style scoped lang="scss"></style>

View File

@@ -4,7 +4,7 @@ import type { MagicCubeProperty } from './config';
import { ref } from 'vue';
import { useVModel } from '@vueuse/core';
import { Form, FormItem, Slider, Typography } from 'ant-design-vue';
import { Form, FormItem, Slider } from 'ant-design-vue';
import UploadImg from '#/components/upload/image-upload.vue';
import {
@@ -21,8 +21,6 @@ const props = defineProps<{ modelValue: MagicCubeProperty }>();
const emit = defineEmits(['update:modelValue']);
const { Text: ATypographyText } = Typography;
const formData = useVModel(props, 'modelValue', emit);
const selectedHotAreaIndex = ref(-1); // 选中的热区
@@ -36,10 +34,7 @@ const handleHotAreaSelected = (_: any, index: number) => {
<template>
<ComponentContainerProperty v-model="formData.style">
<Form :model="formData" class="mt-2">
<ATypographyText tag="p"> 魔方设置 </ATypographyText>
<ATypographyText type="secondary" class="text-sm">
每格尺寸187 * 187
</ATypographyText>
<p class="text-base font-bold">魔方设置</p>
<MagicCubeEditor
class="my-4"
v-model="formData.list"

View File

@@ -2,14 +2,7 @@
import type { MenuGridProperty } from './config';
import { useVModel } from '@vueuse/core';
import {
Card,
Form,
FormItem,
Radio,
RadioGroup,
Switch,
} from 'ant-design-vue';
import { Form, FormItem, Radio, RadioGroup, Switch } from 'ant-design-vue';
import UploadImg from '#/components/upload/image-upload.vue';
import {
@@ -40,7 +33,8 @@ const formData = useVModel(props, 'modelValue', emit);
</RadioGroup>
</FormItem>
<Card header="菜单设置" class="property-group" shadow="never">
<p class="text-base font-bold">菜单设置</p>
<div class="flex flex-col gap-2 rounded-md p-4 shadow-lg">
<Draggable
v-model="formData.list"
:empty-item="EMPTY_MENU_GRID_ITEM_PROPERTY"
@@ -87,7 +81,7 @@ const formData = useVModel(props, 'modelValue', emit);
</template>
</template>
</Draggable>
</Card>
</div>
</Form>
</ComponentContainerProperty>
</template>

View File

@@ -11,11 +11,11 @@ defineProps<{ property: MenuListProperty }>();
</script>
<template>
<div class="flex min-h-[42px] flex-col">
<div class="flex min-h-10 flex-col">
<div
v-for="(item, index) in property.list"
:key="index"
class="item flex h-[42px] flex-row items-center justify-between gap-1 px-3"
class="flex h-10 flex-row items-center justify-between gap-1 border-t border-gray-200 px-3 first:border-t-0"
>
<div class="flex flex-1 flex-row items-center gap-2">
<Image v-if="item.iconUrl" class="h-4 w-4" :src="item.iconUrl" />
@@ -27,14 +27,8 @@ defineProps<{ property: MenuListProperty }>();
<span class="text-xs" :style="{ color: item.subtitleColor }">
{{ item.subtitle }}
</span>
<IconifyIcon icon="ep:arrow-right" color="#000" :size="16" />
<IconifyIcon icon="lucide:arrow-right" class="size-4" />
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.item + .item {
border-top: 1px solid #eee;
}
</style>

View File

@@ -2,7 +2,7 @@
import type { MenuListProperty } from './config';
import { useVModel } from '@vueuse/core';
import { Form, Typography } from 'ant-design-vue';
import { Form, FormItem } from 'ant-design-vue';
import UploadImg from '#/components/upload/image-upload.vue';
import {
@@ -21,17 +21,12 @@ const props = defineProps<{ modelValue: MenuListProperty }>();
const emit = defineEmits(['update:modelValue']);
const { Text: ATypographyText } = Typography;
const formData = useVModel(props, 'modelValue', emit);
</script>
<template>
<ComponentContainerProperty v-model="formData.style">
<ATypographyText tag="p"> 菜单设置 </ATypographyText>
<ATypographyText type="secondary" class="text-sm">
拖动左侧的小圆点可以调整顺序
</ATypographyText>
<p class="text-base font-bold">菜单设置</p>
<Form :model="formData" class="mt-2">
<Draggable
v-model="formData.list"
@@ -44,9 +39,8 @@ const formData = useVModel(props, 'modelValue', emit);
height="80px"
width="80px"
:show-description="false"
>
<template #tip> 建议尺寸44 * 44 </template>
</UploadImg>
/>
<p class="text-sm text-gray-500">建议尺寸44 * 44</p>
</FormItem>
<FormItem label="标题" name="title">
<InputWithColor

View File

@@ -78,12 +78,7 @@ const handleHotAreaSelected = (
class="m-b-16px"
@hot-area-selected="handleHotAreaSelected"
/>
<Image
v-if="isMp"
alt=""
style="width: 76px; height: 30px"
:src="appNavBarMp"
/>
<Image v-if="isMp" alt="" class="w-19 h-8" :src="appNavBarMp" />
</div>
<template v-for="(cell, cellIndex) in cellList" :key="cellIndex">
<template v-if="selectedHotAreaIndex === Number(cellIndex)">
@@ -112,12 +107,10 @@ const handleHotAreaSelected = (
<UploadImg
v-model="cell.imgUrl"
:limit="1"
height="56px"
width="56px"
:show-description="false"
>
<template #tip>建议尺寸 56*56</template>
</UploadImg>
class="size-14"
/>
<span class="text-xs text-gray-500">建议尺寸 56*56</span>
</FormItem>
<FormItem label="链接">
<AppLinkInput v-model="cell.url" />
@@ -135,5 +128,3 @@ const handleHotAreaSelected = (
</template>
</template>
</template>
<style lang="scss" scoped></style>

View File

@@ -78,7 +78,7 @@ const getSearchProp = computed(() => (cell: NavigationBarCellProperty) => {
v-if="property._local?.previewMp"
:src="appNavbarMp"
alt=""
style="width: 86px; height: 30px"
class="w-22 h-8"
/>
</div>
</template>

View File

@@ -130,5 +130,3 @@ if (!formData.value._local) {
</Card>
</Form>
</template>
<style scoped lang="scss"></style>

View File

@@ -19,7 +19,7 @@ export interface NoticeContentProperty {
export const component = {
id: 'NoticeBar',
name: '公告栏',
icon: 'ep:bell',
icon: 'lucide:bell',
property: {
iconUrl: 'http://mall.yudao.iocoder.cn/static/images/xinjian.png',
contents: [

View File

@@ -33,7 +33,7 @@ setInterval(() => {
<div class="h-6 flex-1 truncate pr-2 leading-6">
{{ property.contents?.[activeIndex]?.text }}
</div>
<IconifyIcon icon="ep:arrow-right" />
<IconifyIcon icon="lucide:arrow-right" />
</div>
</template>

View File

@@ -11,7 +11,7 @@ export interface PageConfigProperty {
export const component = {
id: 'PageConfig',
name: '页面设置',
icon: 'ep:document',
icon: 'lucide:file-text',
property: {
description: '',
backgroundColor: '#f5f5f5',

View File

@@ -39,7 +39,7 @@ export interface ProductCardFieldProperty {
export const component = {
id: 'ProductCard',
name: '商品卡片',
icon: 'fluent:text-column-two-left-24-filled',
icon: 'lucide:grid-3x3',
property: {
layoutType: 'oneColBigImg',
fields: {

View File

@@ -61,7 +61,7 @@ function calculateWidth() {
ref="containerRef"
>
<div
class="relative box-content flex flex-row flex-wrap overflow-hidden bg-white"
class="bg-card relative box-content flex flex-row flex-wrap overflow-hidden"
:style="{
...calculateSpace(index),
...calculateWidth(),
@@ -78,30 +78,26 @@ function calculateWidth() {
v-if="property.badge.show && property.badge.imgUrl"
class="absolute left-0 top-0 z-[1] items-center justify-center"
>
<Image
fit="cover"
:src="property.badge.imgUrl"
class="h-[26px] w-[38px]"
/>
<Image fit="cover" :src="property.badge.imgUrl" class="h-6 w-8" />
</div>
<!-- 商品封面图 -->
<div
class="h-[140px]"
class="h-36"
:class="[
{
'w-full': property.layoutType !== 'oneColSmallImg',
'w-[140px]': property.layoutType === 'oneColSmallImg',
'w-36': property.layoutType === 'oneColSmallImg',
},
]"
>
<Image fit="cover" class="h-full w-full" :src="spu.picUrl" />
</div>
<div
class="box-border flex flex-col gap-[8px] p-[8px]"
class="box-border flex flex-col gap-2 p-2"
:class="[
{
'w-full': property.layoutType !== 'oneColSmallImg',
'w-[calc(100%-140px-16px)]':
'w-[calc(100vh-140px-16px)]':
property.layoutType === 'oneColSmallImg',
},
]"
@@ -109,7 +105,7 @@ function calculateWidth() {
<!-- 商品名称 -->
<div
v-if="property.fields.name.show"
class="text-[14px]"
class="text-sm"
:class="[
{
truncate: property.layoutType !== 'oneColSmallImg',
@@ -124,7 +120,7 @@ function calculateWidth() {
<!-- 商品简介 -->
<div
v-if="property.fields.introduction.show"
class="truncate text-[12px]"
class="truncate text-xs"
:style="{ color: property.fields.introduction.color }"
>
{{ spu.introduction }}
@@ -133,7 +129,7 @@ function calculateWidth() {
<!-- 价格 -->
<span
v-if="property.fields.price.show"
class="text-[16px]"
class="text-base"
:style="{ color: property.fields.price.color }"
>
{{ fenToYuan(spu.price as any) }}
@@ -141,12 +137,12 @@ function calculateWidth() {
<!-- 市场价 -->
<span
v-if="property.fields.marketPrice.show && spu.marketPrice"
class="ml-[4px] text-[10px] line-through"
class="ml-1 text-xs line-through"
:style="{ color: property.fields.marketPrice.color }"
>{{ fenToYuan(spu.marketPrice) }}
</span>
</div>
<div class="text-[12px]">
<div class="text-xs">
<!-- 销量 -->
<span
v-if="property.fields.salesCount.show"
@@ -164,11 +160,11 @@ function calculateWidth() {
</div>
</div>
<!-- 购买按钮 -->
<div class="absolute bottom-[8px] right-[8px]">
<div class="absolute bottom-2 right-2">
<!-- 文字按钮 -->
<span
v-if="property.btnBuy.type === 'text'"
class="rounded-full px-[12px] py-[4px] text-[12px] text-white"
class="rounded-full px-3 py-1 text-sm text-white"
:style="{
background: `linear-gradient(to right, ${property.btnBuy.bgBeginColor}, ${property.btnBuy.bgEndColor}`,
}"
@@ -178,7 +174,7 @@ function calculateWidth() {
<!-- 图片按钮 -->
<Image
v-else
class="h-[28px] w-[28px] rounded-full"
class="size-7 rounded-full"
fit="cover"
:src="property.btnBuy.imgUrl"
/>
@@ -186,5 +182,3 @@ function calculateWidth() {
</div>
</div>
</template>
<style scoped lang="scss"></style>

View File

@@ -20,7 +20,7 @@ export type PlaceholderPosition = 'center' | 'left';
export const component = {
id: 'SearchBar',
name: '搜索框',
icon: 'ep:search',
icon: 'lucide:search',
property: {
height: 28,
showScan: false,

View File

@@ -30,19 +30,16 @@ defineProps<{ property: SearchProperty }>();
justifyContent: property.placeholderPosition,
}"
>
<IconifyIcon icon="ep:search" />
<IconifyIcon icon="lucide:search" />
<span>{{ property.placeholder || '搜索商品' }}</span>
</div>
<div class="right">
<!-- 搜索热词 -->
<span v-for="(keyword, index) in property.hotKeywords" :key="index">{{
keyword
}}</span>
<span v-for="(keyword, index) in property.hotKeywords" :key="index">
{{ keyword }}
</span>
<!-- 扫一扫 -->
<IconifyIcon
icon="ant-design:scan-outlined"
v-show="property.showScan"
/>
<IconifyIcon icon="lucide:scan-barcode" v-show="property.showScan" />
</div>
</div>
</div>

View File

@@ -31,7 +31,7 @@ defineProps<{ property: TabBarProperty }>();
<Image :src="index === 0 ? item.activeIconUrl : item.iconUrl">
<template #error>
<div class="flex h-full w-full items-center justify-center">
<IconifyIcon icon="ep:picture" />
<IconifyIcon icon="lucide:image" />
</div>
</template>
</Image>

View File

@@ -60,7 +60,10 @@ defineProps<{ property: TitleBarProperty }>();
<span v-if="property.more.type !== 'icon'">
{{ property.more.text }}
</span>
<IconifyIcon icon="ep:arrow-right" v-if="property.more.type !== 'text'" />
<IconifyIcon
icon="lucide:arrow-right"
v-if="property.more.type !== 'text'"
/>
</div>
</div>
</template>

View File

@@ -3,6 +3,8 @@ import type { UserCardProperty } from './config';
import { IconifyIcon } from '@vben/icons';
import { Avatar } from 'ant-design-vue';
/** 用户卡片 */
defineOptions({ name: 'UserCard' });
// 定义属性
@@ -10,24 +12,20 @@ defineProps<{ property: UserCardProperty }>();
</script>
<template>
<div class="flex flex-col">
<div class="flex items-center justify-between px-[18px] py-[24px]">
<div class="flex flex-1 items-center gap-[16px]">
<Avatar :size="60">
<IconifyIcon icon="ep:avatar" :size="60" />
<div class="flex items-center justify-between px-4 py-6">
<div class="flex flex-1 items-center gap-4">
<Avatar class="size-14">
<IconifyIcon icon="lucide:user" class="size-14" />
</Avatar>
<span class="text-[18px] font-bold">芋道源码</span>
<span class="text-lg font-bold">芋道源码</span>
</div>
<IconifyIcon icon="tdesign:qrcode" :size="20" />
<IconifyIcon icon="lucide:qr-code" class="size-5" />
</div>
<div
class="flex items-center justify-between bg-white px-[20px] py-[8px] text-[12px]"
>
<span class="text-[#ff690d]">点击绑定手机号</span>
<span class="rounded-[26px] bg-[#ff6100] px-[8px] py-[5px] text-white">
<div class="flex items-center justify-between bg-white px-5 py-2 text-xs">
<span class="text-orange-500">点击绑定手机号</span>
<span class="rounded-lg bg-orange-500 px-2 py-1 text-white">
去绑定
</span>
</div>
</div>
</template>
<style scoped lang="scss"></style>

View File

@@ -10,7 +10,7 @@ export interface UserCouponProperty {
export const component = {
id: 'UserCoupon',
name: '用户卡券',
icon: 'ep:ticket',
icon: 'lucide:ticket',
property: {
style: {
bgType: 'color',

View File

@@ -9,7 +9,7 @@ export interface UserOrderProperty {
export const component = {
id: 'UserOrder',
name: '用户订单',
icon: 'ep:list',
icon: 'lucide:clipboard-list',
property: {
style: {
bgType: 'color',

View File

@@ -9,7 +9,7 @@ export interface UserWalletProperty {
export const component = {
id: 'UserWallet',
name: '用户资产',
icon: 'ep:wallet-filled',
icon: 'lucide:wallet',
property: {
style: {
bgType: 'color',

View File

@@ -17,7 +17,7 @@ export interface VideoPlayerStyle extends ComponentStyle {
export const component = {
id: 'VideoPlayer',
name: '视频播放',
icon: 'ep:video-play',
icon: 'lucide:video',
property: {
videoUrl: '',
posterUrl: '',

View File

@@ -307,17 +307,17 @@ onMounted(() => {
>
<Tooltip title="重置">
<Button @click="handleReset">
<IconifyIcon class="size-6" icon="system-uicons:reset-alt" />
<IconifyIcon class="size-6" icon="lucide:refresh-cw" />
</Button>
</Tooltip>
<Tooltip v-if="previewUrl" title="预览">
<Button @click="handlePreview">
<IconifyIcon class="size-6" icon="ep:view" />
<IconifyIcon class="size-6" icon="lucide:eye" />
</Button>
</Tooltip>
<Tooltip title="保存">
<Button @click="handleSave">
<IconifyIcon class="size-6" icon="ep:check" />
<IconifyIcon class="size-6" icon="lucide:check" />
</Button>
</Tooltip>
</Button.Group>
@@ -473,7 +473,9 @@ onMounted(() => {
<span>{{ selectedComponent?.name }}</span>
</div>
</template>
<div class="property max-h-[calc(80vh-100px)] overflow-y-auto p-4">
<div
class="property mt-0 max-h-[calc(80vh-100px)] overflow-y-auto p-4"
>
<component
:is="`${selectedComponent?.id}Property`"
:key="selectedComponent?.uid || selectedComponent?.id"

View File

@@ -58,18 +58,17 @@ const handleDelete = function (index: number) {
<div class="mb-1 flex flex-col gap-1 rounded border border-gray-200 p-2">
<!-- 操作按钮区 -->
<div
class="-m-2 mb-1 flex flex-row items-center justify-between rounded-t p-2"
style="background-color: var(--ant-color-bg-container-secondary)"
class="bg-secondary -m-2 mb-1 flex flex-row items-center justify-between rounded-t p-2"
>
<Tooltip title="拖动排序">
<IconifyIcon
icon="ic:round-drag-indicator"
icon="lucide:move"
class="drag-icon cursor-move text-gray-500"
/>
</Tooltip>
<Tooltip v-if="formData.length > min" title="删除">
<IconifyIcon
icon="ep:delete"
icon="lucide:trash-2"
class="cursor-pointer text-red-500 hover:text-red-600"
@click="handleDelete(index)"
/>
@@ -93,7 +92,7 @@ const handleDelete = function (index: number) {
@click="handleAdd"
>
<template #icon>
<IconifyIcon icon="ep:plus" />
<IconifyIcon icon="lucide:plus" />
</template>
添加
</Button>

View File

@@ -198,103 +198,54 @@ function eachCube(callback: (x: number, y: number, cube: Cube) => void) {
}
</script>
<template>
<div class="relative">
<table class="cube-table">
<!-- 底层魔方矩阵 -->
<tbody>
<tr v-for="(rowCubes, row) in cubes" :key="row">
<td
v-for="(cube, col) in rowCubes"
:key="col"
class="cube"
:class="[{ active: cube.active }]"
:style="{
width: `${cubeSize}px`,
height: `${cubeSize}px`,
}"
@click="handleCubeClick(row, col)"
@mouseenter="handleCellHover(row, col)"
>
<IconifyIcon icon="ep-plus" />
</td>
</tr>
</tbody>
<!-- 顶层热区 -->
<div
v-for="(hotArea, index) in hotAreas"
:key="index"
class="hot-area"
:style="{
top: `${cubeSize * hotArea.top}px`,
left: `${cubeSize * hotArea.left}px`,
height: `${cubeSize * hotArea.height}px`,
width: `${cubeSize * hotArea.width}px`,
}"
@click="handleHotAreaSelected(hotArea, index)"
@mouseover="exitHotAreaSelectMode"
>
<!-- 右上角热区删除按钮 -->
<div
v-if="
selectedHotAreaIndex === index && hotArea.width && hotArea.height
"
class="btn-delete"
@click="handleDeleteHotArea(index)"
<table class="relative border-collapse border-spacing-0">
<!-- 底层魔方矩阵 -->
<tbody>
<tr v-for="(rowCubes, row) in cubes" :key="row">
<td
v-for="(cube, col) in rowCubes"
:key="col"
class="active:bg-primary-200 hover:bg-primary-100 box-border cursor-pointer border text-center align-middle"
:class="[{ active: cube.active }]"
:style="{
width: `${cubeSize}px`,
height: `${cubeSize}px`,
}"
@click="handleCubeClick(row, col)"
@mouseenter="handleCellHover(row, col)"
>
<IconifyIcon icon="ep:circle-close-filled" />
</div>
<span v-if="hotArea.width">{{
`${hotArea.width}×${hotArea.height}`
}}</span>
<IconifyIcon icon="lucide:plus" class="inline-block size-6" />
</td>
</tr>
</tbody>
<!-- 顶层热区 -->
<div
v-for="(hotArea, index) in hotAreas"
:key="index"
class="bg-primary-200 border-primary absolute box-border flex items-center justify-center border"
:style="{
top: `${cubeSize * hotArea.top}px`,
left: `${cubeSize * hotArea.left}px`,
height: `${cubeSize * hotArea.height}px`,
width: `${cubeSize * hotArea.width}px`,
}"
@click="handleHotAreaSelected(hotArea, index)"
@mouseover="exitHotAreaSelectMode"
>
<!-- 右上角热区删除按钮 -->
<div
v-if="selectedHotAreaIndex === index && hotArea.width && hotArea.height"
class="bg-card absolute -right-2 -top-2 z-[1] size-6 h-4 w-4 items-center rounded-lg"
@click="handleDeleteHotArea(index)"
>
<IconifyIcon
icon="lucide:x"
class="bg-primary inset-0 items-center text-white"
/>
</div>
</table>
</div>
<span v-if="hotArea.width">
{{ `${hotArea.width}×${hotArea.height}` }}
</span>
</div>
</table>
</template>
<style lang="scss" scoped>
.cube-table {
position: relative;
border-spacing: 0;
border-collapse: collapse;
.cube {
box-sizing: border-box;
line-height: 1;
color: var(--ant-color-text-secondary);
text-align: center;
cursor: pointer;
border: 1px solid var(--ant-color-border);
&.active {
background: color-mix(in srgb, var(--ant-color-primary) 10%, transparent);
}
}
.hot-area {
position: absolute;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
color: var(--ant-color-primary);
cursor: pointer;
border-spacing: 0;
border-collapse: collapse;
background: color-mix(in srgb, var(--ant-color-primary) 20%, transparent);
border: 1px solid var(--ant-color-primary);
.btn-delete {
position: absolute;
top: -8px;
right: -8px;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
background-color: var(--ant-color-bg-container);
border-radius: 50%;
}
}
}
</style>

View File

@@ -30,9 +30,9 @@ const DIY_PAGE_INDEX_KEY = 'diy_page_index'; // 特殊:存储 reset 重置时
const selectedTemplateItem = ref(0);
const templateItems = reactive([
{ name: '基础设置', icon: 'ep:iphone' },
{ name: '首页', icon: 'ep:home-filled' },
{ name: '我的', icon: 'ep:user-filled' },
{ name: '基础设置', icon: 'lucide:settings' },
{ name: '首页', icon: 'lucide:home' },
{ name: '我的', icon: 'lucide:user' },
]); // 左上角工具栏操作按钮
const formData = ref<MallDiyTemplateApi.DiyTemplateProperty>();