最近要实习一个泡泡弹层。看了下市场的代码,要么写的不怎么好,要么过于复杂。于是拿个轮子自己加工。200行代码撸了个弹出层组件。兼容H5和APP和小程序。
功能:
1)只支持上下左右4个方向的弹层不支持侧边靠齐
2)不对屏幕边界适配
3)支持弹层外边点击自动隐藏
4)支持3种内容模式:
1. 弹出提示文本
2. slot内容占位
3. 支持菜单模式
BWT:弹层外点击自动隐藏基于unibest框架的页面模板技术,这里就不放代码了,自己想想怎么弄😏 。提示:使用事件总线模式,放出的代码也提示了部分用法。
效果,H5下:
APP下:
小程序下:
组件代码:
<!--
自定义弹出层/菜单组件
1)只支持上下左右4个方向的弹层不支持侧边靠齐
2)不对屏幕边界适配
3)支持弹层外边点击自动隐藏
4)支持3种内容模式:
1. 文本为内容
2. slot内容占位
3. 菜单模式
@Author Jim 24/10/08
-->
<template>
<view>
<view class="cc_popper" @click.stop="handleClick">
<slot></slot>
<view
class="cc_popper_layer border-2rpx border-solid"
@click.stop="() => {}"
:style="[
data.layerStyle,
{
visibility: data.isShow ? 'visible' : 'hidden',
opacity: data.isShow ? 1 : 0,
color: props.textColor,
backgroundColor: props.bgColor,
borderColor: 'var(--cc-box-border)'
}
]"
>
<view class="px-20rpx py-10rpx" v-if="content.length > 0 || $slots.content">
<!-- 内容模式 -->
<slot name="content">{{ content }}</slot>
</view>
<view v-else class="py-5rpx px-10rpx">
<template v-for="(conf, index) in props.menu" :key="index">
<view v-if="index > 0" class="bg-box-border opacity-70 h-2rpx w-full" />
<view
class="px-20rpx py-10rpx menu-item my-5rpx"
@click="
() => {
conf.callback()
data.isShow = false
}
"
>
{{ conf.title }}
</view>
</template>
</view>
<view
:class="['w-0', 'h-0', 'z-9', 'absolute', 'popper-arrow-on-' + props.direction]"
:style="[data.arrowStyle]"
/>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { CSSProperties } from 'vue'
import * as utils from '@/utils'
let instance
const { screenWidth } = uni.getSystemInfoSync()
const pixelUnit = screenWidth / 750 // rpx->px比例基数
export interface MenuConf {
icon?: string // 指示图标
title: string // 菜单文本
callback: () => void // 点击事件
}
const props = withDefaults(
defineProps<{
textColor?: string // 指定内部文本颜色
bgColor?: string
borderColor?: string
content?: string // 可以指定文本content,或者指定 slot content来显示弹窗内容
menu?: Array<MenuConf> // 下拉菜单模式
direction?: 'top' | 'bottom' | 'left' | 'right' // 弹层位置
alwaysShow: boolean
}>(),
{
textColor: 'var(--cc-txt)',
bgColor: 'var(--cc-box-fill)', // 默认弹框色
borderColor: 'var(--cc-box-border)', // 默认弹框边框色
content: '',
menu: () => [],
direction: 'top',
alwaysShow: false
}
)
const data = reactive<{
isShow: boolean
layerStyle: CSSProperties // CSS定义一层够了
arrowStyle: CSSProperties
}>({
isShow: false,
layerStyle: {},
arrowStyle: {}
})
onMounted(() => {
instance = getCurrentInstance()
if (props.alwaysShow) {
nextTick(() => handleClick())
}
})
onUnmounted(() => {
if (!props.alwaysShow) {
utils.off(utils.Global.CC_GLOBAL_CLICK, hideLayer) // 移除全局点击监听
}
})
const hideLayer = (event: MouseEvent) => {
data.isShow = false
utils.off(utils.Global.CC_GLOBAL_CLICK, hideLayer)
}
const handleClick = async () => {
if (data.isShow) {
if (props.alwaysShow) {
return
}
utils.off(utils.Global.CC_GLOBAL_CLICK, hideLayer)
return (data.isShow = false)
}
const rects: UniApp.NodeInfo[] = await utils.getRectAll('.cc_popper,.cc_popper_layer', instance)
const srcRect: UniApp.NodeInfo = rects[0]
const layerRect: UniApp.NodeInfo = rects[1]
data.arrowStyle['border' + props.direction.charAt(0).toUpperCase() + props.direction.slice(1)] =
'10rpx solid var(--cc-box-border)'
switch (props.direction) {
case 'top': {
data.layerStyle.left = `${(srcRect.width - layerRect.width) / 2}px`
data.layerStyle.bottom = `${srcRect.height + 16 * pixelUnit}px`
data.arrowStyle.left = `${layerRect.width / 2 - 12 * pixelUnit}px`
console.log(data.arrowStyle.left)
break
}
case 'bottom': {
data.layerStyle.left = `${(srcRect.width - layerRect.width) / 2}px`
data.layerStyle.top = `${srcRect.height + 16 * pixelUnit}px`
data.arrowStyle.left = `${layerRect.width / 2 - 12 * pixelUnit}px`
break
}
case 'left': {
data.layerStyle.right = `${srcRect.width + 16 * pixelUnit}px`
data.layerStyle.top = `${(srcRect.height - layerRect.height) / 2}px`
data.arrowStyle.top = `${layerRect.height / 2 - 12 * pixelUnit}px`
break
}
case 'right': {
data.layerStyle.left = `${srcRect.width + 16 * pixelUnit}px`
data.layerStyle.top = `${(srcRect.height - layerRect.height) / 2}px`
data.arrowStyle.top = `${layerRect.height / 2 - 12 * pixelUnit}px`
break
}
}
data.isShow = true
if (!props.alwaysShow) {
utils.on(utils.Global.CC_GLOBAL_CLICK, hideLayer)
}
}
</script>
<style lang="scss" scoped>
$arrow-size: 12rpx;
$arrow-offset: -12rpx;
.cc_popper {
position: relative;
display: inline-block;
}
.cc_popper_layer {
position: absolute;
display: inline-block;
white-space: nowrap;
border-radius: 10rpx;
transition: opacity 0.3s ease-in-out;
}
.popper-arrow-on-top {
bottom: $arrow-offset;
border-right: $arrow-size solid transparent;
border-left: $arrow-size solid transparent;
}
.popper-arrow-on-right {
left: $arrow-offset;
border-top: $arrow-size solid transparent;
border-bottom: $arrow-size solid transparent;
}
.popper-arrow-on-left {
right: $arrow-offset;
border-top: $arrow-size solid transparent;
border-bottom: $arrow-size solid transparent;
}
.popper-arrow-on-bottom {
top: $arrow-offset;
border-right: $arrow-size solid transparent;
border-left: $arrow-size solid transparent;
}
.menu-item {
&:active {
background-color: #88888840;
}
}
</style>
测试页面:
<template>
<view class="text-txt w-full h-full">
<view>消息</view>
<view class="x-items-between px-200rpx pt-100rpx">
<cc-popper direction="left" content="说啥好呢" alwaysShow>
<view class="w-100rpx"><u-button text="左边" /></view>
</cc-popper>
<view class="w-100rpx">
<cc-popper direction="top" content="向上看" alwaysShow>
<view class="w-100rpx"><u-button text="上面" /></view>
</cc-popper>
<cc-popper direction="bottom" content="下边也没有" alwaysShow>
<view class="w-100rpx mt-20rpx"><u-button text="下面" /></view>
</cc-popper>
</view>
<cc-popper direction="right" content="右边找找" alwaysShow>
<view class="w-100rpx"><u-button text="右边" /></view>
</cc-popper>
</view>
<view class="x-items-between px-150rpx pt-400rpx">
<cc-popper alwaysShow>
<view class="w-200rpx"><u-button shape="circle" text="烎" /></view>
<template #content><text class="text-100rpx">🤩</text></template>
</cc-popper>
<cc-popper alwaysShow :menu="data.menu">
<div class="w-100rpx h-100rpx bg-red"></div>
</cc-popper>
</view>
</view>
</template>
<script lang="ts" setup>
import { MenuConf } from '@/components/ccframe/cc-popper.vue'
const data = reactive<{
menu: Array<MenuConf>
}>({
menu: [
{
title: '口袋1',
callback: () => {
console.log('糖果')
}
},
{
title: '口袋2',
callback: () => {
console.log('退出系统')
}
},
{
title: '口袋3',
callback: () => {
console.log('空的')
}
}
]
})
</script>
对了,菜单的图标支持还没写。等用到的时候再加上去,代码放这存档,后面再更新:)