feat: 优化 diy editor
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -25,5 +25,3 @@ defineProps<{ property: DividerProperty }>();
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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="{
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 }]"
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -11,7 +11,7 @@ export interface ImageBarProperty {
|
||||
export const component = {
|
||||
id: 'ImageBar',
|
||||
name: '图片展示',
|
||||
icon: 'ep:picture',
|
||||
icon: 'lucide:image',
|
||||
property: {
|
||||
imgUrl: '',
|
||||
url: '',
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -130,5 +130,3 @@ if (!formData.value._local) {
|
||||
</Card>
|
||||
</Form>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ export interface PageConfigProperty {
|
||||
export const component = {
|
||||
id: 'PageConfig',
|
||||
name: '页面设置',
|
||||
icon: 'ep:document',
|
||||
icon: 'lucide:file-text',
|
||||
property: {
|
||||
description: '',
|
||||
backgroundColor: '#f5f5f5',
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -10,7 +10,7 @@ export interface UserCouponProperty {
|
||||
export const component = {
|
||||
id: 'UserCoupon',
|
||||
name: '用户卡券',
|
||||
icon: 'ep:ticket',
|
||||
icon: 'lucide:ticket',
|
||||
property: {
|
||||
style: {
|
||||
bgType: 'color',
|
||||
|
||||
@@ -9,7 +9,7 @@ export interface UserOrderProperty {
|
||||
export const component = {
|
||||
id: 'UserOrder',
|
||||
name: '用户订单',
|
||||
icon: 'ep:list',
|
||||
icon: 'lucide:clipboard-list',
|
||||
property: {
|
||||
style: {
|
||||
bgType: 'color',
|
||||
|
||||
@@ -9,7 +9,7 @@ export interface UserWalletProperty {
|
||||
export const component = {
|
||||
id: 'UserWallet',
|
||||
name: '用户资产',
|
||||
icon: 'ep:wallet-filled',
|
||||
icon: 'lucide:wallet',
|
||||
property: {
|
||||
style: {
|
||||
bgType: 'color',
|
||||
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>();
|
||||
|
||||
Reference in New Issue
Block a user