# 弹窗组件 Popover 的实现
# 基于 Teleport 实现 Popover 组件
项目中,对于 Popover
我们是借助 Vue3
的 Teleport
组件自行封装的;
因为从使用场景上,我们需要的 Popover
组件和在其它 UI 组件的 Popover
还不太一样;比如
- 当点击一个事项,打开
Popover
在 UI 组件库中, Popover
是需要通过 slot
插槽去声明 trigger
作为触发元素,并根据
trigger
元素的 DOM
信息去确认 Popover
的位置信息的。通俗点来说就是点击在谁身上,就在谁身
上弹 Popover
。
在我们的场景中,事项是存在跨天事项的,比如用户点击了 5号的一个事项,但可能这个事项跨了 1号到10
号,比如下图,所以通过 slot
插槽的方式去获取触发元素的位置信息就明显不合适了;
- 当我们打开事项并点击其它事项进行事项切换时;
在我们的场景中 Popover
是不关闭的,并且切换事项时还会有一个过渡动画;
而在 UI 组件库点击其它事项时,Popver
会先关闭,需要再次点击,才能打开;虽然我们能手动控制它的
显示隐藏,但是用手动控制进行切换时,就会丢过渡动画
基于以上原因,所以需要我们自己封装一个具有自己业务属性 Popover
组件
# 代码实现
# 接收的 props
const props = defineProps<{
show: boolean;
positionInfo: PopoverPosition | null;
// 放置源的样式
dropStyle: {
width: number;
height: number;
};
popoverStyle?: {
width: number;
height: number | "auto";
};
eventId?: number;
overlap?: boolean;
placement?: Placement;
raw?: boolean; // 是否有基础样式,默认不带
}>();
type PopoverPosition = {
start: PopoverTarget;
end: PopoverTarget | null; // 如果是单天的事项,那么 end 为 null
isRange: boolean;
};
type PopoverTarget = {
time: string;
dateIndex: number;
target?: HTMLElement;
};
type Placement =
| "right-start"
| "left-start"
| "right-end"
| "left-end"
| "top-start"
| "top-end"
| "bottom"
| "bottom-start"
| "bottom-end";
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
show | 受控模式下控制是否显示 |
---|---|
positionInfo | popover 弹出的位置信息 |
dropStyle | 放置源的样式,此处就是 Day 组件的宽高信息 |
popoverStyle | 自定义 Popover 的宽高 |
eventId | 事件 eventId,用于判断点击是否是事项 |
overlap | 弹出时,是否覆盖自身元素 |
placement | 弹出位置 |
raw | Popover 是否带默认样式,会带 padding |
# template
<template>
<!-- to 选择 #app 是因为可以使用 naive 组件库提供的样式信息 -->
<Teleport to="#app">
<Transition>
<n-el
v-if="show"
tag="section"
:class="[
'absolute left-0 top-0 z-40 rounded transition transform box-border',
{ 'px-3 py-2': raw },
]"
:style="contentstyle"
@click="handleClick"
>
<slot />
</n-el>
</Transition>
</Teleport>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 实现思路
通过 positionInfo.isRange
判断是否是跨天事项
# 单天事项
- 如果
placement
通过props
传递了,那么使用传递的placement
,否则根据positionInfo.start.dateIndex
获取placement
的值 - 通过调用
getBoundingClientRect
去获取positionInfo.start.target
位置信息 - 根据
placement
的值和positionInfo.start.target
位置信息去求Popover
的位置信息
let { placement } = getPopoverPlacement(start.dateIndex);
if (props.placement) {
placement = props.placement;
}
const position = getPositionInfo(start.target as HTMLElement);
isLeft = placement.startsWith("left");
isRight = placement.startsWith("right");
isEnd = placement.endsWith("end");
isBottom = placement.startsWith("bottom");
isTop = placement.startsWith("top");
left = position?.left;
top = position?.top;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
# 跨天事项
跨天事项和单天事项,唯一不一样的点就是在获取 placement
值的时候不一样,因为单天事项两侧都是由冗余空间的,而跨天事项不一定,比如这种,只能让 Popover
向上或者向下弹
在代码中,我们专门对这种情况做了判断
// 判断事件的两侧是否有冗余空间,如果没有冗余空间,那么 popover 需要向上或者向下弹出
const isRedundantSpace = toLeft || toRight;
if (isRedundantSpace) {
if (isEndRow) {
// 如果左侧非末行,并且还有冗余空间,那么按照 left-start 处理
const leftPlacement = isLeftEndRow ? "left-end" : "left-start";
return {
direction: toLeft ? "start" : "end",
placement: toLeft ? leftPlacement : "right-end",
};
}
return {
direction: toLeft ? "start" : "end",
placement: toLeft ? "left-start" : "right-start",
};
} else {
return {
direction: "start",
placement: startRowNum <= 2 ? "bottom-start" : "top-start",
};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 获取位置信息
获取位置信息就是体力活了,根据 top、left、right、bottom、end 分别做不同方向的处理就好
# 使用 Popover 组件
在组件中使用