营销:适配商城装修组件【热区】
parent
edfdee8cc1
commit
41a404a52c
|
@ -32,7 +32,7 @@
|
||||||
>
|
>
|
||||||
<el-button
|
<el-button
|
||||||
class="m-b-8px m-r-8px m-l-0px!"
|
class="m-b-8px m-r-8px m-l-0px!"
|
||||||
:type="isSameLink(appLink.path, activeAppLink) ? 'primary' : 'default'"
|
:type="isSameLink(appLink.path, activeAppLink.path) ? 'primary' : 'default'"
|
||||||
@click="handleAppLinkSelected(appLink)"
|
@click="handleAppLinkSelected(appLink)"
|
||||||
>
|
>
|
||||||
{{ appLink.name }}
|
{{ appLink.name }}
|
||||||
|
@ -63,7 +63,7 @@
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { APP_LINK_GROUP_LIST, APP_LINK_TYPE_ENUM } from './data'
|
import { APP_LINK_GROUP_LIST, APP_LINK_TYPE_ENUM, AppLink } from './data'
|
||||||
import { ButtonInstance, ScrollbarInstance } from 'element-plus'
|
import { ButtonInstance, ScrollbarInstance } from 'element-plus'
|
||||||
import { split } from 'lodash-es'
|
import { split } from 'lodash-es'
|
||||||
import ProductCategorySelect from '@/views/mall/product/category/components/ProductCategorySelect.vue'
|
import ProductCategorySelect from '@/views/mall/product/category/components/ProductCategorySelect.vue'
|
||||||
|
@ -74,17 +74,23 @@ defineOptions({ name: 'AppLinkSelectDialog' })
|
||||||
// 选中的分组,默认选中第一个
|
// 选中的分组,默认选中第一个
|
||||||
const activeGroup = ref(APP_LINK_GROUP_LIST[0].name)
|
const activeGroup = ref(APP_LINK_GROUP_LIST[0].name)
|
||||||
// 选中的 APP 链接
|
// 选中的 APP 链接
|
||||||
const activeAppLink = ref('')
|
const activeAppLink = ref({} as AppLink)
|
||||||
|
|
||||||
/** 打开弹窗 */
|
/** 打开弹窗 */
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
const open = (link: string) => {
|
const open = (link: string) => {
|
||||||
activeAppLink.value = link
|
activeAppLink.value.path = link
|
||||||
dialogVisible.value = true
|
dialogVisible.value = true
|
||||||
|
|
||||||
// 滚动到当前的链接
|
// 滚动到当前的链接
|
||||||
const group = APP_LINK_GROUP_LIST.find((group) =>
|
const group = APP_LINK_GROUP_LIST.find((group) =>
|
||||||
group.links.some((linkItem) => isSameLink(linkItem.path, link))
|
group.links.some((linkItem) => {
|
||||||
|
const sameLink = isSameLink(linkItem.path, link)
|
||||||
|
if (sameLink) {
|
||||||
|
activeAppLink.value = { ...linkItem, path: link }
|
||||||
|
}
|
||||||
|
return sameLink
|
||||||
|
})
|
||||||
)
|
)
|
||||||
if (group) {
|
if (group) {
|
||||||
// 使用 nextTick 的原因:可能 Dom 还没生成,导致滚动失败
|
// 使用 nextTick 的原因:可能 Dom 还没生成,导致滚动失败
|
||||||
|
@ -94,9 +100,9 @@ const open = (link: string) => {
|
||||||
defineExpose({ open })
|
defineExpose({ open })
|
||||||
|
|
||||||
// 处理 APP 链接选中
|
// 处理 APP 链接选中
|
||||||
const handleAppLinkSelected = (appLink: any) => {
|
const handleAppLinkSelected = (appLink: AppLink) => {
|
||||||
if (!isSameLink(appLink.path, activeAppLink.value)) {
|
if (!isSameLink(appLink.path, activeAppLink.value.path)) {
|
||||||
activeAppLink.value = appLink.path
|
activeAppLink.value = appLink
|
||||||
}
|
}
|
||||||
switch (appLink.type) {
|
switch (appLink.type) {
|
||||||
case APP_LINK_TYPE_ENUM.PRODUCT_CATEGORY_LIST:
|
case APP_LINK_TYPE_ENUM.PRODUCT_CATEGORY_LIST:
|
||||||
|
@ -104,7 +110,7 @@ const handleAppLinkSelected = (appLink: any) => {
|
||||||
detailSelectDialog.value.type = appLink.type
|
detailSelectDialog.value.type = appLink.type
|
||||||
// 返显
|
// 返显
|
||||||
detailSelectDialog.value.id =
|
detailSelectDialog.value.id =
|
||||||
getUrlNumberValue('id', 'http://127.0.0.1' + activeAppLink.value) || undefined
|
getUrlNumberValue('id', 'http://127.0.0.1' + activeAppLink.value.path) || undefined
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
|
@ -114,10 +120,12 @@ const handleAppLinkSelected = (appLink: any) => {
|
||||||
// 处理绑定值更新
|
// 处理绑定值更新
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
change: [link: string]
|
change: [link: string]
|
||||||
|
appLinkChange: [appLink: AppLink]
|
||||||
}>()
|
}>()
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
dialogVisible.value = false
|
dialogVisible.value = false
|
||||||
emit('change', activeAppLink.value)
|
emit('change', activeAppLink.value.path)
|
||||||
|
emit('appLinkChange', activeAppLink.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分组标题引用列表
|
// 分组标题引用列表
|
||||||
|
@ -127,7 +135,7 @@ const groupTitleRefs = ref<HTMLInputElement[]>([])
|
||||||
* @param scrollTop 滚动条的位置
|
* @param scrollTop 滚动条的位置
|
||||||
*/
|
*/
|
||||||
const handleScroll = ({ scrollTop }: { scrollTop: number }) => {
|
const handleScroll = ({ scrollTop }: { scrollTop: number }) => {
|
||||||
const titleEl = groupTitleRefs.value.find((titleEl) => {
|
const titleEl = groupTitleRefs.value.find((titleEl: HTMLInputElement) => {
|
||||||
// 获取标题的位置信息
|
// 获取标题的位置信息
|
||||||
const { offsetHeight, offsetTop } = titleEl
|
const { offsetHeight, offsetTop } = titleEl
|
||||||
// 判断标题是否在可视范围内
|
// 判断标题是否在可视范围内
|
||||||
|
@ -146,7 +154,7 @@ const linkScrollbar = ref<ScrollbarInstance>()
|
||||||
// 处理分组选中
|
// 处理分组选中
|
||||||
const handleGroupSelected = (group: string) => {
|
const handleGroupSelected = (group: string) => {
|
||||||
activeGroup.value = group
|
activeGroup.value = group
|
||||||
const titleRef = groupTitleRefs.value.find((item) => item.textContent === group)
|
const titleRef = groupTitleRefs.value.find((item: HTMLInputElement) => item.textContent === group)
|
||||||
if (titleRef) {
|
if (titleRef) {
|
||||||
// 滚动分组标题
|
// 滚动分组标题
|
||||||
linkScrollbar.value?.setScrollTop(titleRef.offsetTop)
|
linkScrollbar.value?.setScrollTop(titleRef.offsetTop)
|
||||||
|
@ -160,8 +168,8 @@ const groupBtnRefs = ref<ButtonInstance[]>([])
|
||||||
// 自动滚动分组按钮,确保分组按钮保持在可视区域内
|
// 自动滚动分组按钮,确保分组按钮保持在可视区域内
|
||||||
const scrollToGroupBtn = (group: string) => {
|
const scrollToGroupBtn = (group: string) => {
|
||||||
const groupBtn = groupBtnRefs.value
|
const groupBtn = groupBtnRefs.value
|
||||||
.map((btn) => btn['ref'])
|
.map((btn: ButtonInstance) => btn['ref'])
|
||||||
.find((ref) => ref.textContent === group)
|
.find((ref: Node) => ref.textContent === group)
|
||||||
if (groupBtn) {
|
if (groupBtn) {
|
||||||
groupScrollbar.value?.setScrollTop(groupBtn.offsetTop)
|
groupScrollbar.value?.setScrollTop(groupBtn.offsetTop)
|
||||||
}
|
}
|
||||||
|
@ -184,11 +192,11 @@ const detailSelectDialog = ref<{
|
||||||
})
|
})
|
||||||
// 处理详情选择
|
// 处理详情选择
|
||||||
const handleProductCategorySelected = (id: number) => {
|
const handleProductCategorySelected = (id: number) => {
|
||||||
const url = new URL(activeAppLink.value, 'http://127.0.0.1')
|
const url = new URL(activeAppLink.value.path, 'http://127.0.0.1')
|
||||||
// 修改 id 参数
|
// 修改 id 参数
|
||||||
url.searchParams.set('id', `${id}`)
|
url.searchParams.set('id', `${id}`)
|
||||||
// 排除域名
|
// 排除域名
|
||||||
activeAppLink.value = `${url.pathname}${url.search}`
|
activeAppLink.value.path = `${url.pathname}${url.search}`
|
||||||
// 关闭对话框
|
// 关闭对话框
|
||||||
detailSelectDialog.value.visible = false
|
detailSelectDialog.value.visible = false
|
||||||
// 重置 id
|
// 重置 id
|
||||||
|
|
|
@ -1,3 +1,20 @@
|
||||||
|
// APP 链接分组
|
||||||
|
export interface AppLinkGroup {
|
||||||
|
// 分组名称
|
||||||
|
name: string
|
||||||
|
// 链接列表
|
||||||
|
links: AppLink[]
|
||||||
|
}
|
||||||
|
// APP 链接
|
||||||
|
export interface AppLink {
|
||||||
|
// 链接名称
|
||||||
|
name: string
|
||||||
|
// 链接地址
|
||||||
|
path: string
|
||||||
|
// 链接的类型
|
||||||
|
type?: APP_LINK_TYPE_ENUM
|
||||||
|
}
|
||||||
|
|
||||||
// APP 链接类型(需要特殊处理,例如商品详情)
|
// APP 链接类型(需要特殊处理,例如商品详情)
|
||||||
export const enum APP_LINK_TYPE_ENUM {
|
export const enum APP_LINK_TYPE_ENUM {
|
||||||
// 拼团活动
|
// 拼团活动
|
||||||
|
@ -243,4 +260,4 @@ export const APP_LINK_GROUP_LIST = [
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
] as AppLinkGroup[]
|
||||||
|
|
|
@ -0,0 +1,143 @@
|
||||||
|
import { HotZoneItemProperty } from '@/components/DiyEditor/components/mobile/HotZone/config'
|
||||||
|
import { StyleValue } from 'vue'
|
||||||
|
|
||||||
|
// 热区的最小宽高
|
||||||
|
export const HOT_ZONE_MIN_SIZE = 100
|
||||||
|
|
||||||
|
// 控制的类型
|
||||||
|
export enum CONTROL_TYPE_ENUM {
|
||||||
|
LEFT,
|
||||||
|
TOP,
|
||||||
|
WIDTH,
|
||||||
|
HEIGHT
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义热区的控制点
|
||||||
|
export interface ControlDot {
|
||||||
|
position: string
|
||||||
|
types: CONTROL_TYPE_ENUM[]
|
||||||
|
style: StyleValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 热区的8个控制点
|
||||||
|
export const CONTROL_DOT_LIST = [
|
||||||
|
{
|
||||||
|
position: '左上角',
|
||||||
|
types: [
|
||||||
|
CONTROL_TYPE_ENUM.LEFT,
|
||||||
|
CONTROL_TYPE_ENUM.TOP,
|
||||||
|
CONTROL_TYPE_ENUM.WIDTH,
|
||||||
|
CONTROL_TYPE_ENUM.HEIGHT
|
||||||
|
],
|
||||||
|
style: { left: '-5px', top: '-5px', cursor: 'nwse-resize' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
position: '上方中间',
|
||||||
|
types: [CONTROL_TYPE_ENUM.TOP, CONTROL_TYPE_ENUM.HEIGHT],
|
||||||
|
style: { left: '50%', top: '-5px', cursor: 'n-resize', transform: 'translateX(-50%)' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
position: '右上角',
|
||||||
|
types: [CONTROL_TYPE_ENUM.TOP, CONTROL_TYPE_ENUM.WIDTH, CONTROL_TYPE_ENUM.HEIGHT],
|
||||||
|
style: { right: '-5px', top: '-5px', cursor: 'nesw-resize' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
position: '右侧中间',
|
||||||
|
types: [CONTROL_TYPE_ENUM.WIDTH],
|
||||||
|
style: { right: '-5px', top: '50%', cursor: 'e-resize', transform: 'translateX(-50%)' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
position: '右下角',
|
||||||
|
types: [CONTROL_TYPE_ENUM.WIDTH, CONTROL_TYPE_ENUM.HEIGHT],
|
||||||
|
style: { right: '-5px', bottom: '-5px', cursor: 'nwse-resize' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
position: '下方中间',
|
||||||
|
types: [CONTROL_TYPE_ENUM.HEIGHT],
|
||||||
|
style: { left: '50%', bottom: '-5px', cursor: 's-resize', transform: 'translateX(-50%)' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
position: '左下角',
|
||||||
|
types: [CONTROL_TYPE_ENUM.LEFT, CONTROL_TYPE_ENUM.WIDTH, CONTROL_TYPE_ENUM.HEIGHT],
|
||||||
|
style: { left: '-5px', bottom: '-5px', cursor: 'nesw-resize' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
position: '左侧中间',
|
||||||
|
types: [CONTROL_TYPE_ENUM.LEFT, CONTROL_TYPE_ENUM.WIDTH],
|
||||||
|
style: { left: '-5px', top: '50%', cursor: 'w-resize', transform: 'translateX(-50%)' }
|
||||||
|
}
|
||||||
|
] as ControlDot[]
|
||||||
|
|
||||||
|
//region 热区的缩放
|
||||||
|
// 热区的缩放比例
|
||||||
|
export const HOT_ZONE_SCALE_RATE = 2
|
||||||
|
// 缩小:缩回适合手机屏幕的大小
|
||||||
|
export const zoomOut = (list?: HotZoneItemProperty[]) => {
|
||||||
|
return (
|
||||||
|
list?.map((hotZone) => ({
|
||||||
|
...hotZone,
|
||||||
|
left: (hotZone.left /= HOT_ZONE_SCALE_RATE),
|
||||||
|
top: (hotZone.top /= HOT_ZONE_SCALE_RATE),
|
||||||
|
width: (hotZone.width /= HOT_ZONE_SCALE_RATE),
|
||||||
|
height: (hotZone.height /= HOT_ZONE_SCALE_RATE)
|
||||||
|
})) || []
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// 放大:作用是为了方便在电脑屏幕上编辑
|
||||||
|
export const zoomIn = (list?: HotZoneItemProperty[]) => {
|
||||||
|
return (
|
||||||
|
list?.map((hotZone) => ({
|
||||||
|
...hotZone,
|
||||||
|
left: (hotZone.left *= HOT_ZONE_SCALE_RATE),
|
||||||
|
top: (hotZone.top *= HOT_ZONE_SCALE_RATE),
|
||||||
|
width: (hotZone.width *= HOT_ZONE_SCALE_RATE),
|
||||||
|
height: (hotZone.height *= HOT_ZONE_SCALE_RATE)
|
||||||
|
})) || []
|
||||||
|
)
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 封装热区拖拽
|
||||||
|
*
|
||||||
|
* 注:为什么不使用vueuse的useDraggable。在本场景下,其使用方式比较复杂
|
||||||
|
* @param hotZone 热区
|
||||||
|
* @param downEvent 鼠标按下事件
|
||||||
|
* @param callback 回调函数
|
||||||
|
*/
|
||||||
|
export const useDraggable = (
|
||||||
|
hotZone: HotZoneItemProperty,
|
||||||
|
downEvent: MouseEvent,
|
||||||
|
callback: (
|
||||||
|
left: number,
|
||||||
|
top: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
moveWidth: number,
|
||||||
|
moveHeight: number
|
||||||
|
) => void
|
||||||
|
) => {
|
||||||
|
// 阻止事件冒泡
|
||||||
|
downEvent.stopPropagation()
|
||||||
|
|
||||||
|
// 移动前的鼠标坐标
|
||||||
|
const { clientX: startX, clientY: startY } = downEvent
|
||||||
|
// 移动前的热区坐标、大小
|
||||||
|
const { left, top, width, height } = hotZone
|
||||||
|
|
||||||
|
// 监听鼠标移动
|
||||||
|
document.onmousemove = (e) => {
|
||||||
|
// 移动宽度
|
||||||
|
const moveWidth = e.clientX - startX
|
||||||
|
// 移动高度
|
||||||
|
const moveHeight = e.clientY - startY
|
||||||
|
// 移动回调
|
||||||
|
callback(left, top, width, height, moveWidth, moveHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 松开鼠标后,结束拖拽
|
||||||
|
document.onmouseup = () => {
|
||||||
|
document.onmousemove = null
|
||||||
|
document.onmouseup = null
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,236 @@
|
||||||
|
<template>
|
||||||
|
<Dialog v-model="dialogVisible" title="设置热区" width="780" @close="handleClose">
|
||||||
|
<div ref="container" class="relative h-full w-750px">
|
||||||
|
<el-image :src="imgUrl" class="pointer-events-none h-full w-750px select-none" />
|
||||||
|
<div
|
||||||
|
v-for="(item, hotZoneIndex) in formData"
|
||||||
|
:key="hotZoneIndex"
|
||||||
|
class="hot-zone"
|
||||||
|
:style="{
|
||||||
|
width: `${item.width}px`,
|
||||||
|
height: `${item.height}px`,
|
||||||
|
top: `${item.top}px`,
|
||||||
|
left: `${item.left}px`
|
||||||
|
}"
|
||||||
|
@mousedown="handleMove(item, $event)"
|
||||||
|
@dblclick="handleShowAppLinkDialog(item)"
|
||||||
|
>
|
||||||
|
<span class="pointer-events-none select-none">{{ item.name || '双击选择链接' }}</span>
|
||||||
|
<Icon icon="ep:close" class="delete" :size="14" @click="handleRemove(item)" />
|
||||||
|
|
||||||
|
<!-- 8个控制点 -->
|
||||||
|
<span
|
||||||
|
class="ctrl-dot"
|
||||||
|
v-for="(dot, dotIndex) in CONTROL_DOT_LIST"
|
||||||
|
:key="dotIndex"
|
||||||
|
:style="dot.style"
|
||||||
|
@mousedown="handleResize(item, dot, $event)"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="handleAdd" type="primary" plain>
|
||||||
|
<Icon icon="ep:plus" class="mr-5px" />
|
||||||
|
添加热区
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="handleSubmit" type="primary" plain>
|
||||||
|
<Icon icon="ep:check" class="mr-5px" />
|
||||||
|
确定
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
<AppLinkSelectDialog ref="appLinkDialogRef" @app-link-change="handleAppLinkChange" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { HotZoneItemProperty } from '@/components/DiyEditor/components/mobile/HotZone/config'
|
||||||
|
import { array, string } from 'vue-types'
|
||||||
|
import {
|
||||||
|
CONTROL_DOT_LIST,
|
||||||
|
CONTROL_TYPE_ENUM,
|
||||||
|
ControlDot,
|
||||||
|
HOT_ZONE_MIN_SIZE,
|
||||||
|
useDraggable,
|
||||||
|
zoomIn,
|
||||||
|
zoomOut
|
||||||
|
} from './controller'
|
||||||
|
import { AppLink } from '@/components/AppLinkInput/data'
|
||||||
|
import { remove } from 'lodash-es'
|
||||||
|
|
||||||
|
/** 热区编辑对话框 */
|
||||||
|
defineOptions({ name: 'HotZoneEditDialog' })
|
||||||
|
|
||||||
|
// 定义属性
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: array<HotZoneItemProperty>(),
|
||||||
|
imgUrl: string().def('')
|
||||||
|
})
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
const formData = ref<HotZoneItemProperty[]>([])
|
||||||
|
|
||||||
|
// 弹窗的是否显示
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
// 打开弹窗
|
||||||
|
const open = () => {
|
||||||
|
// 放大
|
||||||
|
formData.value = zoomIn(props.modelValue)
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
// 提供 open 方法,用于打开弹窗
|
||||||
|
defineExpose({ open })
|
||||||
|
|
||||||
|
// 热区容器
|
||||||
|
const container = ref<HTMLDivElement>()
|
||||||
|
|
||||||
|
// 增加热区
|
||||||
|
const handleAdd = () => {
|
||||||
|
formData.value.push({
|
||||||
|
width: HOT_ZONE_MIN_SIZE,
|
||||||
|
height: HOT_ZONE_MIN_SIZE,
|
||||||
|
top: 0,
|
||||||
|
left: 0
|
||||||
|
} as HotZoneItemProperty)
|
||||||
|
}
|
||||||
|
// 删除热区
|
||||||
|
const handleRemove = (hotZone: HotZoneItemProperty) => {
|
||||||
|
remove(formData.value, hotZone)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动热区
|
||||||
|
const handleMove = (item: HotZoneItemProperty, e: MouseEvent) => {
|
||||||
|
useDraggable(item, e, (left, top, _, __, moveWidth, moveHeight) => {
|
||||||
|
setLeft(item, left + moveWidth)
|
||||||
|
setTop(item, top + moveHeight)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调整热区大小、位置
|
||||||
|
const handleResize = (item: HotZoneItemProperty, ctrlDot: ControlDot, e: MouseEvent) => {
|
||||||
|
useDraggable(item, e, (left, top, width, height, moveWidth, moveHeight) => {
|
||||||
|
ctrlDot.types.forEach((type) => {
|
||||||
|
switch (type) {
|
||||||
|
case CONTROL_TYPE_ENUM.LEFT:
|
||||||
|
setLeft(item, left + moveWidth)
|
||||||
|
break
|
||||||
|
case CONTROL_TYPE_ENUM.TOP:
|
||||||
|
setTop(item, top + moveHeight)
|
||||||
|
break
|
||||||
|
case CONTROL_TYPE_ENUM.WIDTH:
|
||||||
|
{
|
||||||
|
// 上移时,高度为减少
|
||||||
|
const direction = ctrlDot.types.includes(CONTROL_TYPE_ENUM.LEFT) ? -1 : 1
|
||||||
|
setWidth(item, width + moveWidth * direction)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case CONTROL_TYPE_ENUM.HEIGHT:
|
||||||
|
{
|
||||||
|
// 左移时,宽度为减少
|
||||||
|
const direction = ctrlDot.types.includes(CONTROL_TYPE_ENUM.TOP) ? -1 : 1
|
||||||
|
setHeight(item, height + moveHeight * direction)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置X轴坐标
|
||||||
|
const setLeft = (item: HotZoneItemProperty, left: number) => {
|
||||||
|
// 不能超出容器
|
||||||
|
if (left >= 0 && left <= container.value!.offsetWidth - item.width) {
|
||||||
|
item.left = left
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 设置Y轴坐标
|
||||||
|
const setTop = (item: HotZoneItemProperty, top: number) => {
|
||||||
|
// 不能超出容器
|
||||||
|
if (top >= 0 && top <= container.value!.offsetHeight - item.height) {
|
||||||
|
item.top = top
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 设置宽度
|
||||||
|
const setWidth = (item: HotZoneItemProperty, width: number) => {
|
||||||
|
// 不能小于最小宽度 && 不能超出容器右边
|
||||||
|
if (width >= HOT_ZONE_MIN_SIZE && item.left + width <= container.value!.offsetWidth) {
|
||||||
|
item.width = width
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 设置高度
|
||||||
|
const setHeight = (item: HotZoneItemProperty, height: number) => {
|
||||||
|
// 不能小于最小高度 && 不能超出容器底部
|
||||||
|
if (height >= HOT_ZONE_MIN_SIZE && item.top + height <= container.value!.offsetHeight) {
|
||||||
|
item.height = height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理对话框关闭
|
||||||
|
const handleSubmit = () => {
|
||||||
|
// 会自动触发handleClose
|
||||||
|
dialogVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理对话框关闭
|
||||||
|
const handleClose = () => {
|
||||||
|
// 缩小
|
||||||
|
const list = zoomOut(formData.value)
|
||||||
|
emit('update:modelValue', list)
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeHotZone = ref<HotZoneItemProperty>()
|
||||||
|
const appLinkDialogRef = ref()
|
||||||
|
const handleShowAppLinkDialog = (hotZone: HotZoneItemProperty) => {
|
||||||
|
activeHotZone.value = hotZone
|
||||||
|
appLinkDialogRef.value.open(hotZone.url)
|
||||||
|
}
|
||||||
|
const handleAppLinkChange = (appLink: AppLink) => {
|
||||||
|
if (!appLink || !activeHotZone.value) return
|
||||||
|
activeHotZone.value.name = appLink.name
|
||||||
|
activeHotZone.value.url = appLink.path
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.hot-zone {
|
||||||
|
position: absolute;
|
||||||
|
background: var(--el-color-primary-light-7);
|
||||||
|
opacity: 0.8;
|
||||||
|
border: 1px solid var(--el-color-primary);
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
font-size: 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: move;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
/* 控制点 */
|
||||||
|
.ctrl-dot {
|
||||||
|
position: absolute;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: inherit;
|
||||||
|
background-color: #fff;
|
||||||
|
z-index: 11;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 2px 2px 6px 6px;
|
||||||
|
background-color: var(--el-color-primary);
|
||||||
|
border-radius: 0 0 0 80%;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #fff;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.delete {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
|
||||||
|
|
||||||
|
/** 热区属性 */
|
||||||
|
export interface HotZoneProperty {
|
||||||
|
// 图片地址
|
||||||
|
imgUrl: string
|
||||||
|
// 导航菜单列表
|
||||||
|
list: HotZoneItemProperty[]
|
||||||
|
// 组件样式
|
||||||
|
style: ComponentStyle
|
||||||
|
}
|
||||||
|
/** 热区项目属性 */
|
||||||
|
export interface HotZoneItemProperty {
|
||||||
|
// 链接的名称
|
||||||
|
name: string
|
||||||
|
// 链接
|
||||||
|
url: string
|
||||||
|
// 宽
|
||||||
|
width: number
|
||||||
|
// 高
|
||||||
|
height: number
|
||||||
|
// 上
|
||||||
|
top: number
|
||||||
|
// 左
|
||||||
|
left: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义组件
|
||||||
|
export const component = {
|
||||||
|
id: 'HotZone',
|
||||||
|
name: '热区',
|
||||||
|
icon: 'tabler:hand-click',
|
||||||
|
property: {
|
||||||
|
imgUrl: '',
|
||||||
|
list: [] as HotZoneItemProperty[],
|
||||||
|
style: {
|
||||||
|
bgType: 'color',
|
||||||
|
bgColor: '#fff',
|
||||||
|
marginBottom: 8
|
||||||
|
} as ComponentStyle
|
||||||
|
}
|
||||||
|
} as DiyComponent<HotZoneProperty>
|
|
@ -0,0 +1,42 @@
|
||||||
|
<template>
|
||||||
|
<div class="relative h-full min-h-30px w-full">
|
||||||
|
<el-image :src="property.imgUrl" class="pointer-events-none h-full w-full select-none" />
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in property.list"
|
||||||
|
:key="index"
|
||||||
|
class="hot-zone"
|
||||||
|
:style="{
|
||||||
|
width: `${item.width}px`,
|
||||||
|
height: `${item.height}px`,
|
||||||
|
top: `${item.top}px`,
|
||||||
|
left: `${item.left}px`
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ item.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { HotZoneProperty } from './config'
|
||||||
|
|
||||||
|
/** 热区 */
|
||||||
|
defineOptions({ name: 'HotZone' })
|
||||||
|
const props = defineProps<{ property: HotZoneProperty }>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.hot-zone {
|
||||||
|
position: absolute;
|
||||||
|
background: var(--el-color-primary-light-7);
|
||||||
|
opacity: 0.8;
|
||||||
|
border: 1px solid var(--el-color-primary);
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: move;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,63 @@
|
||||||
|
<template>
|
||||||
|
<ComponentContainerProperty v-model="formData.style">
|
||||||
|
<!-- 表单 -->
|
||||||
|
<el-form label-width="80px" :model="formData" class="m-t-8px">
|
||||||
|
<el-form-item label="上传图片" prop="imgUrl">
|
||||||
|
<UploadImg v-model="formData.imgUrl" height="50px" width="auto" class="min-w-80px">
|
||||||
|
<template #tip>
|
||||||
|
<el-text type="info" size="small"> 推荐宽度 750</el-text>
|
||||||
|
</template>
|
||||||
|
</UploadImg>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-button type="primary" plain class="w-full" @click="handleOpenEditDialog">
|
||||||
|
设置热区
|
||||||
|
</el-button>
|
||||||
|
</ComponentContainerProperty>
|
||||||
|
<!-- 热区编辑对话框 -->
|
||||||
|
<HotZoneEditDialog ref="editDialogRef" v-model="formData.list" :img-url="formData.imgUrl" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { usePropertyForm } from '@/components/DiyEditor/util'
|
||||||
|
import { HotZoneProperty } from '@/components/DiyEditor/components/mobile/HotZone/config'
|
||||||
|
import HotZoneEditDialog from './components/HotZoneEditDialog/index.vue'
|
||||||
|
|
||||||
|
/** 热区属性面板 */
|
||||||
|
defineOptions({ name: 'HotZoneProperty' })
|
||||||
|
|
||||||
|
const props = defineProps<{ modelValue: HotZoneProperty }>()
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
const { formData } = usePropertyForm(props.modelValue, emit)
|
||||||
|
|
||||||
|
// 热区编辑对话框
|
||||||
|
const editDialogRef = ref()
|
||||||
|
// 打开热区编辑对话框
|
||||||
|
const handleOpenEditDialog = () => {
|
||||||
|
editDialogRef.value.open()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.hot-zone {
|
||||||
|
position: absolute;
|
||||||
|
background: #409effbf;
|
||||||
|
border: 1px solid var(--el-color-primary);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: move;
|
||||||
|
|
||||||
|
/* 控制点 */
|
||||||
|
.ctrl-dot {
|
||||||
|
position: absolute;
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -124,7 +124,15 @@ export const PAGE_LIBS = [
|
||||||
{
|
{
|
||||||
name: '图文组件',
|
name: '图文组件',
|
||||||
extended: true,
|
extended: true,
|
||||||
components: ['ImageBar', 'Carousel', 'TitleBar', 'VideoPlayer', 'Divider', 'MagicCube']
|
components: [
|
||||||
|
'ImageBar',
|
||||||
|
'Carousel',
|
||||||
|
'TitleBar',
|
||||||
|
'VideoPlayer',
|
||||||
|
'Divider',
|
||||||
|
'MagicCube',
|
||||||
|
'HotZone'
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{ name: '商品组件', extended: true, components: ['ProductCard', 'ProductList'] },
|
{ name: '商品组件', extended: true, components: ['ProductCard', 'ProductList'] },
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in New Issue