【兼容多端】UNIAPP popper气泡弹层vue3+typescript unibest

最近要实习一个泡泡弹层。看了下市场的代码,要么写的不怎么好,要么过于复杂。于是拿个轮子自己加工。200行代码撸了个弹出层组件。兼容H5和APP和小程序。

功能:

1)只支持上下左右4个方向的弹层不支持侧边靠齐

2)不对屏幕边界适配

3)支持弹层外边点击自动隐藏

4)支持3种内容模式:

  1. 弹出提示文本

  2. slot内容占位

  3. 支持菜单模式

BWT:弹层外点击自动隐藏基于unibest框架的页面模板技术,这里就不放代码了,自己想想怎么弄😏 。提示:使用事件总线模式,放出的代码也提示了部分用法。

效果,H5下:

APP下:

小程序下:

组件代码:

TypeScript 复制代码
<!--
  自定义弹出层/菜单组件
  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>

测试页面:

TypeScript 复制代码
<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>

对了,菜单的图标支持还没写。等用到的时候再加上去,代码放这存档,后面再更新:)

相关推荐
前端百草阁32 分钟前
【TS简单上手,快速入门教程】————适合零基础
javascript·typescript
彭世瑜32 分钟前
ts: TypeScript跳过检查/忽略类型检查
前端·javascript·typescript
FØund40433 分钟前
antd form.setFieldsValue问题总结
前端·react.js·typescript·html
Backstroke fish33 分钟前
Token刷新机制
前端·javascript·vue.js·typescript·vue
小曲程序35 分钟前
vue3 封装request请求
java·前端·typescript·vue
临枫54135 分钟前
Nuxt3封装网络请求 useFetch & $fetch
前端·javascript·vue.js·typescript
RAY_CHEN.36 分钟前
vue3 pinia 中actions修改状态不生效
vue.js·typescript·npm
酷酷的威朗普36 分钟前
医院绩效考核系统
javascript·css·vue.js·typescript·node.js·echarts·html5
小张不爱写代码37 分钟前
CocosCreator 音效管理器
typescript
小刺猬_98537 分钟前
(超详细)数组方法 ——— splice( )
前端·javascript·typescript