Skip to content

Commit 41a404a

Browse files
committed
营销:适配商城装修组件【热区】
1 parent edfdee8 commit 41a404a

File tree

8 files changed

+577
-18
lines changed

8 files changed

+577
-18
lines changed

src/components/AppLinkInput/AppLinkSelectDialog.vue

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
>
3333
<el-button
3434
class="m-b-8px m-r-8px m-l-0px!"
35-
:type="isSameLink(appLink.path, activeAppLink) ? 'primary' : 'default'"
35+
:type="isSameLink(appLink.path, activeAppLink.path) ? 'primary' : 'default'"
3636
@click="handleAppLinkSelected(appLink)"
3737
>
3838
{{ appLink.name }}
@@ -63,7 +63,7 @@
6363
</Dialog>
6464
</template>
6565
<script lang="ts" setup>
66-
import { APP_LINK_GROUP_LIST, APP_LINK_TYPE_ENUM } from './data'
66+
import { APP_LINK_GROUP_LIST, APP_LINK_TYPE_ENUM, AppLink } from './data'
6767
import { ButtonInstance, ScrollbarInstance } from 'element-plus'
6868
import { split } from 'lodash-es'
6969
import ProductCategorySelect from '@/views/mall/product/category/components/ProductCategorySelect.vue'
@@ -74,17 +74,23 @@ defineOptions({ name: 'AppLinkSelectDialog' })
7474
// 选中的分组,默认选中第一个
7575
const activeGroup = ref(APP_LINK_GROUP_LIST[0].name)
7676
// 选中的 APP 链接
77-
const activeAppLink = ref('')
77+
const activeAppLink = ref({} as AppLink)
7878
7979
/** 打开弹窗 */
8080
const dialogVisible = ref(false)
8181
const open = (link: string) => {
82-
activeAppLink.value = link
82+
activeAppLink.value.path = link
8383
dialogVisible.value = true
8484
8585
// 滚动到当前的链接
8686
const group = APP_LINK_GROUP_LIST.find((group) =>
87-
group.links.some((linkItem) => isSameLink(linkItem.path, link))
87+
group.links.some((linkItem) => {
88+
const sameLink = isSameLink(linkItem.path, link)
89+
if (sameLink) {
90+
activeAppLink.value = { ...linkItem, path: link }
91+
}
92+
return sameLink
93+
})
8894
)
8995
if (group) {
9096
// 使用 nextTick 的原因:可能 Dom 还没生成,导致滚动失败
@@ -94,17 +100,17 @@ const open = (link: string) => {
94100
defineExpose({ open })
95101
96102
// 处理 APP 链接选中
97-
const handleAppLinkSelected = (appLink: any) => {
98-
if (!isSameLink(appLink.path, activeAppLink.value)) {
99-
activeAppLink.value = appLink.path
103+
const handleAppLinkSelected = (appLink: AppLink) => {
104+
if (!isSameLink(appLink.path, activeAppLink.value.path)) {
105+
activeAppLink.value = appLink
100106
}
101107
switch (appLink.type) {
102108
case APP_LINK_TYPE_ENUM.PRODUCT_CATEGORY_LIST:
103109
detailSelectDialog.value.visible = true
104110
detailSelectDialog.value.type = appLink.type
105111
// 返显
106112
detailSelectDialog.value.id =
107-
getUrlNumberValue('id', 'http://127.0.0.1' + activeAppLink.value) || undefined
113+
getUrlNumberValue('id', 'http://127.0.0.1' + activeAppLink.value.path) || undefined
108114
break
109115
default:
110116
break
@@ -114,10 +120,12 @@ const handleAppLinkSelected = (appLink: any) => {
114120
// 处理绑定值更新
115121
const emit = defineEmits<{
116122
change: [link: string]
123+
appLinkChange: [appLink: AppLink]
117124
}>()
118125
const handleSubmit = () => {
119126
dialogVisible.value = false
120-
emit('change', activeAppLink.value)
127+
emit('change', activeAppLink.value.path)
128+
emit('appLinkChange', activeAppLink.value)
121129
}
122130
123131
// 分组标题引用列表
@@ -127,7 +135,7 @@ const groupTitleRefs = ref<HTMLInputElement[]>([])
127135
* @param scrollTop 滚动条的位置
128136
*/
129137
const handleScroll = ({ scrollTop }: { scrollTop: number }) => {
130-
const titleEl = groupTitleRefs.value.find((titleEl) => {
138+
const titleEl = groupTitleRefs.value.find((titleEl: HTMLInputElement) => {
131139
// 获取标题的位置信息
132140
const { offsetHeight, offsetTop } = titleEl
133141
// 判断标题是否在可视范围内
@@ -146,7 +154,7 @@ const linkScrollbar = ref<ScrollbarInstance>()
146154
// 处理分组选中
147155
const handleGroupSelected = (group: string) => {
148156
activeGroup.value = group
149-
const titleRef = groupTitleRefs.value.find((item) => item.textContent === group)
157+
const titleRef = groupTitleRefs.value.find((item: HTMLInputElement) => item.textContent === group)
150158
if (titleRef) {
151159
// 滚动分组标题
152160
linkScrollbar.value?.setScrollTop(titleRef.offsetTop)
@@ -160,8 +168,8 @@ const groupBtnRefs = ref<ButtonInstance[]>([])
160168
// 自动滚动分组按钮,确保分组按钮保持在可视区域内
161169
const scrollToGroupBtn = (group: string) => {
162170
const groupBtn = groupBtnRefs.value
163-
.map((btn) => btn['ref'])
164-
.find((ref) => ref.textContent === group)
171+
.map((btn: ButtonInstance) => btn['ref'])
172+
.find((ref: Node) => ref.textContent === group)
165173
if (groupBtn) {
166174
groupScrollbar.value?.setScrollTop(groupBtn.offsetTop)
167175
}
@@ -184,11 +192,11 @@ const detailSelectDialog = ref<{
184192
})
185193
// 处理详情选择
186194
const handleProductCategorySelected = (id: number) => {
187-
const url = new URL(activeAppLink.value, 'http://127.0.0.1')
195+
const url = new URL(activeAppLink.value.path, 'http://127.0.0.1')
188196
// 修改 id 参数
189197
url.searchParams.set('id', `${id}`)
190198
// 排除域名
191-
activeAppLink.value = `${url.pathname}${url.search}`
199+
activeAppLink.value.path = `${url.pathname}${url.search}`
192200
// 关闭对话框
193201
detailSelectDialog.value.visible = false
194202
// 重置 id

src/components/AppLinkInput/data.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
// APP 链接分组
2+
export interface AppLinkGroup {
3+
// 分组名称
4+
name: string
5+
// 链接列表
6+
links: AppLink[]
7+
}
8+
// APP 链接
9+
export interface AppLink {
10+
// 链接名称
11+
name: string
12+
// 链接地址
13+
path: string
14+
// 链接的类型
15+
type?: APP_LINK_TYPE_ENUM
16+
}
17+
118
// APP 链接类型(需要特殊处理,例如商品详情)
219
export const enum APP_LINK_TYPE_ENUM {
320
// 拼团活动
@@ -243,4 +260,4 @@ export const APP_LINK_GROUP_LIST = [
243260
}
244261
]
245262
}
246-
]
263+
] as AppLinkGroup[]
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { HotZoneItemProperty } from '@/components/DiyEditor/components/mobile/HotZone/config'
2+
import { StyleValue } from 'vue'
3+
4+
// 热区的最小宽高
5+
export const HOT_ZONE_MIN_SIZE = 100
6+
7+
// 控制的类型
8+
export enum CONTROL_TYPE_ENUM {
9+
LEFT,
10+
TOP,
11+
WIDTH,
12+
HEIGHT
13+
}
14+
15+
// 定义热区的控制点
16+
export interface ControlDot {
17+
position: string
18+
types: CONTROL_TYPE_ENUM[]
19+
style: StyleValue
20+
}
21+
22+
// 热区的8个控制点
23+
export const CONTROL_DOT_LIST = [
24+
{
25+
position: '左上角',
26+
types: [
27+
CONTROL_TYPE_ENUM.LEFT,
28+
CONTROL_TYPE_ENUM.TOP,
29+
CONTROL_TYPE_ENUM.WIDTH,
30+
CONTROL_TYPE_ENUM.HEIGHT
31+
],
32+
style: { left: '-5px', top: '-5px', cursor: 'nwse-resize' }
33+
},
34+
{
35+
position: '上方中间',
36+
types: [CONTROL_TYPE_ENUM.TOP, CONTROL_TYPE_ENUM.HEIGHT],
37+
style: { left: '50%', top: '-5px', cursor: 'n-resize', transform: 'translateX(-50%)' }
38+
},
39+
{
40+
position: '右上角',
41+
types: [CONTROL_TYPE_ENUM.TOP, CONTROL_TYPE_ENUM.WIDTH, CONTROL_TYPE_ENUM.HEIGHT],
42+
style: { right: '-5px', top: '-5px', cursor: 'nesw-resize' }
43+
},
44+
{
45+
position: '右侧中间',
46+
types: [CONTROL_TYPE_ENUM.WIDTH],
47+
style: { right: '-5px', top: '50%', cursor: 'e-resize', transform: 'translateX(-50%)' }
48+
},
49+
{
50+
position: '右下角',
51+
types: [CONTROL_TYPE_ENUM.WIDTH, CONTROL_TYPE_ENUM.HEIGHT],
52+
style: { right: '-5px', bottom: '-5px', cursor: 'nwse-resize' }
53+
},
54+
{
55+
position: '下方中间',
56+
types: [CONTROL_TYPE_ENUM.HEIGHT],
57+
style: { left: '50%', bottom: '-5px', cursor: 's-resize', transform: 'translateX(-50%)' }
58+
},
59+
{
60+
position: '左下角',
61+
types: [CONTROL_TYPE_ENUM.LEFT, CONTROL_TYPE_ENUM.WIDTH, CONTROL_TYPE_ENUM.HEIGHT],
62+
style: { left: '-5px', bottom: '-5px', cursor: 'nesw-resize' }
63+
},
64+
{
65+
position: '左侧中间',
66+
types: [CONTROL_TYPE_ENUM.LEFT, CONTROL_TYPE_ENUM.WIDTH],
67+
style: { left: '-5px', top: '50%', cursor: 'w-resize', transform: 'translateX(-50%)' }
68+
}
69+
] as ControlDot[]
70+
71+
//region 热区的缩放
72+
// 热区的缩放比例
73+
export const HOT_ZONE_SCALE_RATE = 2
74+
// 缩小:缩回适合手机屏幕的大小
75+
export const zoomOut = (list?: HotZoneItemProperty[]) => {
76+
return (
77+
list?.map((hotZone) => ({
78+
...hotZone,
79+
left: (hotZone.left /= HOT_ZONE_SCALE_RATE),
80+
top: (hotZone.top /= HOT_ZONE_SCALE_RATE),
81+
width: (hotZone.width /= HOT_ZONE_SCALE_RATE),
82+
height: (hotZone.height /= HOT_ZONE_SCALE_RATE)
83+
})) || []
84+
)
85+
}
86+
// 放大:作用是为了方便在电脑屏幕上编辑
87+
export const zoomIn = (list?: HotZoneItemProperty[]) => {
88+
return (
89+
list?.map((hotZone) => ({
90+
...hotZone,
91+
left: (hotZone.left *= HOT_ZONE_SCALE_RATE),
92+
top: (hotZone.top *= HOT_ZONE_SCALE_RATE),
93+
width: (hotZone.width *= HOT_ZONE_SCALE_RATE),
94+
height: (hotZone.height *= HOT_ZONE_SCALE_RATE)
95+
})) || []
96+
)
97+
}
98+
//endregion
99+
100+
/**
101+
* 封装热区拖拽
102+
*
103+
* 注:为什么不使用vueuse的useDraggable。在本场景下,其使用方式比较复杂
104+
* @param hotZone 热区
105+
* @param downEvent 鼠标按下事件
106+
* @param callback 回调函数
107+
*/
108+
export const useDraggable = (
109+
hotZone: HotZoneItemProperty,
110+
downEvent: MouseEvent,
111+
callback: (
112+
left: number,
113+
top: number,
114+
width: number,
115+
height: number,
116+
moveWidth: number,
117+
moveHeight: number
118+
) => void
119+
) => {
120+
// 阻止事件冒泡
121+
downEvent.stopPropagation()
122+
123+
// 移动前的鼠标坐标
124+
const { clientX: startX, clientY: startY } = downEvent
125+
// 移动前的热区坐标、大小
126+
const { left, top, width, height } = hotZone
127+
128+
// 监听鼠标移动
129+
document.onmousemove = (e) => {
130+
// 移动宽度
131+
const moveWidth = e.clientX - startX
132+
// 移动高度
133+
const moveHeight = e.clientY - startY
134+
// 移动回调
135+
callback(left, top, width, height, moveWidth, moveHeight)
136+
}
137+
138+
// 松开鼠标后,结束拖拽
139+
document.onmouseup = () => {
140+
document.onmousemove = null
141+
document.onmouseup = null
142+
}
143+
}

0 commit comments

Comments
 (0)