quanyawei
2024-03-01 bd99a5211f3a5fcaa051e5da868d87bb870148f5
uni_modules/uview-ui/components/u-tooltip/u-tooltip.vue
New file
@@ -0,0 +1,365 @@
<template>
   <view
      class="u-tooltip"
      :style="[$u.addStyle(customStyle)]"
   >
      <u-overlay
         :show="showTooltip && tooltipTop !== -10000 && overlay"
         customStyle="backgroundColor: rgba(0, 0, 0, 0)"
         @click="overlayClickHandler"
      ></u-overlay>
      <view class="u-tooltip__wrapper">
         <text
            class="u-tooltip__wrapper__text"
            :id="textId"
            :ref="textId"
            :userSelect="false"
            :selectable="false"
            @longpress.stop="longpressHandler"
            :style="{
               color: color,
               backgroundColor: bgColor && showTooltip && tooltipTop !== -10000 ? bgColor : 'transparent'
            }"
         >{{ text }}</text>
         <u-transition
            mode="fade"
            :show="showTooltip"
            duration="300"
            :customStyle="{
               position: 'absolute',
               top: $u.addUnit(tooltipTop),
               zIndex: zIndex,
               ...tooltipStyle
            }"
         >
            <view
               class="u-tooltip__wrapper__popup"
               :id="tooltipId"
               :ref="tooltipId"
            >
               <view
                  class="u-tooltip__wrapper__popup__indicator"
                  hover-class="u-tooltip__wrapper__popup__indicator--hover"
                  v-if="showCopy || buttons.length"
                  :style="[indicatorStyle, {
                     width: $u.addUnit(indicatorWidth),
                     height: $u.addUnit(indicatorWidth),
                  }]"
               >
                  <!-- 由于nvue不支持三角形绘制,这里就做一个四方形,再旋转45deg,得到露出的一个三角 -->
               </view>
               <view class="u-tooltip__wrapper__popup__list">
                  <view
                     v-if="showCopy"
                     class="u-tooltip__wrapper__popup__list__btn"
                     hover-class="u-tooltip__wrapper__popup__list__btn--hover"
                     @tap="setClipboardData"
                  >
                     <text
                        class="u-tooltip__wrapper__popup__list__btn__text"
                     >复制</text>
                  </view>
                  <u-line
                     direction="column"
                     color="#8d8e90"
                     v-if="showCopy && buttons.length > 0"
                     length="18"
                  ></u-line>
                  <block v-for="(item , index) in buttons" :key="index">
                     <view
                        class="u-tooltip__wrapper__popup__list__btn"
                        hover-class="u-tooltip__wrapper__popup__list__btn--hover"
                     >
                        <text
                           class="u-tooltip__wrapper__popup__list__btn__text"
                           @tap="btnClickHandler(index)"
                        >{{ item }}</text>
                     </view>
                     <u-line
                        direction="column"
                        color="#8d8e90"
                        v-if="index < buttons.length - 1"
                        length="18"
                     ></u-line>
                  </block>
               </view>
            </view>
         </u-transition>
      </view>
   </view>
</template>
<script>
   import props from './props.js';
   // #ifdef APP-NVUE
   const dom = uni.requireNativePlugin('dom')
   // #endif
   // #ifdef H5
   import ClipboardJS from "./clipboard.min.js"
   // #endif
   /**
    * Tooltip
    * @description
    * @tutorial https://www.uviewui.com/components/tooltip.html
    * @property {String | Number}   text      需要显示的提示文字
    * @property {String | Number}   copyText   点击复制按钮时,复制的文本,为空则使用text值
    * @property {String | Number}   size      文本大小(默认 14 )
    * @property {String}         color      字体颜色(默认 '#606266' )
    * @property {String}         bgColor      弹出提示框时,文本的背景色(默认 'transparent' )
    * @property {String}         direction   弹出提示的方向,top-上方,bottom-下方(默认 'top' )
    * @property {String | Number}   zIndex      弹出提示的z-index,nvue无效(默认 10071 )
    * @property {Boolean}         showCopy   是否显示复制按钮(默认 true )
    * @property {Array}         buttons      扩展的按钮组
    * @property {Boolean}         overlay      是否显示透明遮罩以防止触摸穿透(默认 true )
    * @property {Object}         customStyle   定义需要用到的外部样式
    *
    * @event {Function}
    * @example
    */
   export default {
      name: 'u-tooltip',
      mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
      data() {
         return {
            // 是否展示气泡
            showTooltip: true,
            // 生成唯一id,防止一个页面多个组件,造成干扰
            textId: uni.$u.guid(),
            tooltipId: uni.$u.guid(),
            // 初始时甚至为很大的值,让其移到屏幕外面,为了计算元素的尺寸
            tooltipTop: -10000,
            // 气泡的位置信息
            tooltipInfo: {
               width: 0,
               left: 0
            },
            // 文本的位置信息
            textInfo: {
               width: 0,
               left: 0
            },
            // 三角形指示器的样式
            indicatorStyle: {},
            // 气泡在可能超出屏幕边沿范围时,重新定位后,距离屏幕边沿的距离
            screenGap: 12,
            // 三角形指示器的宽高,由于对元素进行了角度旋转,精确计算指示器位置时,需要用到其尺寸信息
            indicatorWidth: 14,
         }
      },
      watch: {
         propsChange() {
            this.getElRect()
         }
      },
      computed: {
         // 特别处理H5的复制,因为H5浏览器是自带系统复制功能的,在H5环境
         // 当一些依赖参数变化时,需要重新计算气泡和指示器的位置信息
         propsChange() {
            return [this.text, this.buttons]
         },
         // 计算气泡和指示器的位置信息
         tooltipStyle() {
            const style = {
                  transform: `translateY(${this.direction === 'top' ? '-100%' : '100%'})`,
               },
               sys = uni.$u.sys(),
               getPx = uni.$u.getPx,
               addUnit = uni.$u.addUnit
            if (this.tooltipInfo.width / 2 > this.textInfo.left + this.textInfo.width / 2 - this.screenGap) {
               this.indicatorStyle = {}
               style.left = `-${addUnit(this.textInfo.left - this.screenGap)}`
               this.indicatorStyle.left = addUnit(this.textInfo.width / 2 - getPx(style.left) - this.indicatorWidth /
                  2)
            } else if (this.tooltipInfo.width / 2 > sys.windowWidth - this.textInfo.right + this.textInfo.width / 2 -
               this.screenGap) {
               this.indicatorStyle = {}
               style.right = `-${addUnit(sys.windowWidth - this.textInfo.right - this.screenGap)}`
               this.indicatorStyle.right = addUnit(this.textInfo.width / 2 - getPx(style.right) - this
                  .indicatorWidth / 2)
            } else {
               const left = Math.abs(this.textInfo.width / 2 - this.tooltipInfo.width / 2)
               style.left = this.textInfo.width > this.tooltipInfo.width ? addUnit(left) : -addUnit(left)
               this.indicatorStyle = {}
            }
            if (this.direction === 'top') {
               style.marginTop = '-10px'
               this.indicatorStyle.bottom = '-4px'
            } else {
               style.marginBottom = '-10px'
               this.indicatorStyle.top = '-4px'
            }
            return style
         }
      },
      mounted() {
         this.init()
      },
      methods: {
         init() {
            this.getElRect()
         },
         // 长按触发事件
         async longpressHandler() {
            this.tooltipTop = 0
            this.showTooltip = true
         },
         // 点击透明遮罩
         overlayClickHandler() {
            this.showTooltip = false
         },
         // 点击弹出按钮
         btnClickHandler(index) {
            this.showTooltip = false
            // 如果需要展示复制按钮,此处index需要加1,因为复制按钮在第一个位置
            this.$emit('click', this.showCopy ? index + 1 : index)
         },
         // 查询内容高度
         queryRect(ref) {
            // #ifndef APP-NVUE
            // $uGetRect为uView自带的节点查询简化方法,详见文档介绍:https://www.uviewui.com/js/getRect.html
            // 组件内部一般用this.$uGetRect,对外的为uni.$u.getRect,二者功能一致,名称不同
            return new Promise(resolve => {
               this.$uGetRect(`#${ref}`).then(size => {
                  resolve(size)
               })
            })
            // #endif
            // #ifdef APP-NVUE
            // nvue下,使用dom模块查询元素高度
            // 返回一个promise,让调用此方法的主体能使用then回调
            return new Promise(resolve => {
               dom.getComponentRect(this.$refs[ref], res => {
                  resolve(res.size)
               })
            })
            // #endif
         },
         // 元素尺寸
         getElRect() {
            // 调用之前,先将指示器调整到屏幕外,方便获取尺寸
            this.showTooltip = true
            this.tooltipTop = -10000
            uni.$u.sleep(500).then(() => {
               this.queryRect(this.tooltipId).then(size => {
                  this.tooltipInfo = size
                  // 获取气泡尺寸之后,将其隐藏,为了让下次切换气泡显示与隐藏时,有淡入淡出的效果
                  this.showTooltip = false
               })
               this.queryRect(this.textId).then(size => {
                  this.textInfo = size
               })
            })
         },
         // 复制文本到粘贴板
         setClipboardData() {
            // 关闭组件
            this.showTooltip = false
            this.$emit('click', 0)
            // #ifndef H5
            uni.setClipboardData({
               // 优先使用copyText字段,如果没有,则默认使用text字段当做复制的内容
               data: this.copyText || this.text,
               success: () => {
                  this.showToast && uni.$u.toast('复制成功')
               },
               fail: () => {
                  this.showToast && uni.$u.toast('复制失败')
               },
               complete: () => {
                  this.showTooltip = false
               }
            })
            // #endif
            // #ifdef H5
            let event = window.event || e || {}
            let clipboard = new ClipboardJS('', {
               text: () => this.copyText || this.text
            })
            clipboard.on('success', (e) => {
               this.showToast && uni.$u.toast('复制成功')
               clipboard.off('success')
               clipboard.off('error')
               // 在单页应用中,需要销毁DOM的监听
               clipboard.destroy()
            })
            clipboard.on('error', (e) => {
               this.showToast && uni.$u.toast('复制失败')
               clipboard.off('success')
               clipboard.off('error')
               // 在单页应用中,需要销毁DOM的监听
               clipboard.destroy()
            })
            clipboard.onClick(event)
            // #endif
         }
      }
   }
</script>
<style lang="scss" scoped>
   @import "../../libs/css/components.scss";
   .u-tooltip {
      position: relative;
      @include flex;
      &__wrapper {
         @include flex;
         justify-content: center;
         /* #ifndef APP-NVUE */
         white-space: nowrap;
         /* #endif */
         &__text {
            font-size: 14px;
         }
         &__popup {
            @include flex;
            justify-content: center;
            &__list {
               background-color: #060607;
               position: relative;
               flex: 1;
               border-radius: 5px;
               padding: 0px 0;
               @include flex(row);
               align-items: center;
               overflow: hidden;
               &__btn {
                  padding: 11px 13px;
                  &--hover {
                     background-color: #58595B;
                  }
                  &__text {
                     line-height: 12px;
                     font-size: 13px;
                     color: #FFFFFF;
                  }
               }
            }
            &__indicator {
               position: absolute;
               background-color: #060607;
               width: 14px;
               height: 14px;
               bottom: -4px;
               transform: rotate(45deg);
               border-radius: 2px;
               z-index: -1;
               &--hover {
                  background-color: #58595B;
               }
            }
         }
      }
   }
</style>