基于uview-pro的u-dropdown扩展自己的dropdown组件

0 阅读2分钟

基于uview-pro的u-dropdown扩展自己的dropdown组件

uview-pro的u-dropdown只能是菜单,且只能向下展开,当前组件采用它的核心逻辑,去除多余逻辑,兼容上/下展开,以及自定义展示的内容,不再局限于菜单形式

e4043c3d-df06-4a4f-9d61-237d8254cf54.png

import type { ExtractPropTypes, PropType } from 'vue';
import { baseProps } from 'uview-pro/components/common/props';

/**
 * u-dropdown 下拉菜单 Props
 * @description 该组件一般用于向下展开菜单,同时可切换多个选项卡的场景
 */
export const DropdownProps = {
  ...baseProps,
  /** 点击遮罩是否关闭菜单 */
  closeOnClickMask: { type: Boolean, default: true },
  /** 过渡时间 */
  duration: { type: [Number, String] as PropType<number | string>, default: 300 },
  /** 下拉出来的内容部分的圆角值 */
  borderRadius: { type: [Number, String] as PropType<number | string>, default: 20 },
  /** 展开方向 down/up */
  direction: { type: String as PropType<'down' | 'up'>, default: 'up' },
  /** 弹出层最大高度 */
  maxHeight: { type: String as PropType<`${number}rpx` | `${number}vh`>, default: '80vh' },
  /** 弹出层最小高度 */
  minHeight: { type: String as PropType<`${number}rpx` | `${number}vh`>, default: '0rpx' },
  /** 是否隐藏关闭按钮 */
  hiddenClose: { type: Boolean, default: false },
  /** 弹出层标题 */
  title: { type: String, default: '' }
};

export type DropdownProps = ExtractPropTypes<typeof DropdownProps>;

<template>
  <view class="u-dropdown" :style="$u.toStyle(styles, customStyle)" :class="customClass">
    <view class="u-dropdown__menu">
      <slot></slot>
    </view>
    <view
      class="u-dropdown__content"
      :style="[
        contentStyle,
        {
          transition: `opacity ${Number(duration) / 1000}s linear`,
          [currentDirection === 'down' ? 'top' : 'bottom']: menuHeight + 'px',
          height: contentHeight + 'px'
        }
      ]"
      @tap="maskClick"
      @touchmove.stop.prevent>
      <view @tap.stop.prevent class="u-dropdown__content__popup" :style="[popupStyle]">
        <slot name="close" v-if="!hiddenClose">
          <view class="u-dropdown__content__popup__close" @click="close">
            <u-icon name="close" size="48" custom-prefix="custom-icon" />
          </view>
        </slot>

        <slot name="header">
          <view class="u-dropdown__content__popup__header" v-if="title"> {{ title }} </view>
        </slot>

        <view class="u-dropdown__content__popup__body">
          <scroll-view scroll-y class="u-dropdown__content__popup__scroll-view">
            <slot name="content"></slot>
          </scroll-view>
        </view>
        <view class="u-dropdown__content__popup__footer">
          <slot name="footer"></slot>
        </view>
      </view>
      <view class="u-dropdown__content__mask"></view>
    </view>
  </view>
</template>

<script lang="ts">
  export default {
    name: 'hj-dropdown',
    options: {
      addGlobalClass: true,
      // #ifndef MP-TOUTIAO
      virtualHost: true,
      // #endif
      styleIsolation: 'shared'
    }
  };
</script>

<script setup lang="ts">
  import { ref, computed, onMounted, getCurrentInstance, watch, type CSSProperties } from 'vue';
  import { $u } from 'uview-pro';
  import { DropdownProps } from './types';

  /**
   * dropdown 下拉菜单
   * @description 该组件一般用于向下展开菜单,同时可切换多个选项卡的场景
   * @tutorial https://uviewpro.cn/zh/components/dropdown.html
   * @property {Boolean} close-on-click-mask 点击遮罩是否关闭菜单(默认true)
   * @property {String | Number} duration 选项卡展开和收起的过渡时间,单位ms(默认300)
   * @property {String | Number} border-radius 菜单展开内容下方的圆角值,单位任意(默认20)
   * @property {String} direction 展开方向 down/up(默认up)
   * @property {String} max-height 弹出层最大高度(默认80vh)
   * @property {String} min-height 弹出层最小高度
   * @property {Boolean} hidden-close 是否隐藏关闭按钮(默认false)
   * @property {String} title 弹出层标题
   * @property {Boolean} show 是否显示下拉菜单(默认false)
   * @event {Function} open 下拉菜单被打开时触发
   * @event {Function} close 下拉菜单被关闭时触发
   * @example <hj-dropdown></hj-dropdown>
   */

  const props = defineProps(DropdownProps);
  const emit = defineEmits(['open', 'close']);

  // 展开状态
  const active = ref(false);
  // 外层内容样式
  const contentStyle = ref<CSSProperties>({
    zIndex: -1,
    opacity: 0
  });
  // 下拉内容高度
  const contentHeight = ref<number>(0);
  // 菜单实际高度
  const menuHeight = ref<number>(0);
  // 当前展开方向
  const currentDirection = ref<'down' | 'up'>(props.direction);
  // 子组件引用
  const instance = getCurrentInstance();

  const vShow = defineModel('show', {
    type: Boolean,
    default: false
  });

  watch(vShow, val => {
    if (val === active.value) return;
    if (val) {
      open();
    } else {
      close();
    }
  });

  // 监听方向变化
  watch(
    () => props.direction,
    val => {
      currentDirection.value = val;
    }
  );

  // 兼容头条样式
  const styles = computed<CSSProperties>(() => {
    const style: CSSProperties = {};
    // #ifdef MP-TOUTIAO
    style.width = '100vw';
    // #endif
    return style;
  });

  // 下拉出来部分的样式
  const popupStyle = computed<CSSProperties>(() => {
    const style: CSSProperties = {};
    const isDown = currentDirection.value === 'down';
    const hiddenTransformLate = isDown ? '-100%' : '100%';

    style.maxHeight = props.maxHeight;
    style.minHeight = props.minHeight;
    style.transform = `translateY(${active.value ? 0 : hiddenTransformLate})`;
    style[isDown ? 'top' : 'bottom'] = 0;
    // 进行Y轴位移,展开状态时,恢复原位。收起状态时,往上位移100%(或下),进行隐藏
    style.transitionDuration = `${Number(props.duration) / 1000}s`;

    if (isDown) {
      style.borderRadius = `0 0 ${$u.addUnit(props.borderRadius)} ${$u.addUnit(props.borderRadius)}`;
    } else {
      style.borderRadius = `${$u.addUnit(props.borderRadius)} ${$u.addUnit(props.borderRadius)} 0 0`;
    }
    return style;
  });

  // 生命周期
  onMounted(() => {
    getContentHeight();
  });

  /**
   * 打开下拉菜单
   * @param direction 展开方向 'down' | 'up'
   */
  function open(direction?: 'down' | 'up') {
    currentDirection.value = direction || props.direction;

    // 重新计算高度,因为方向可能改变
    getContentHeight();

    // 设置展开状态
    active.value = true;

    // 展开时,设置下拉内容的样式
    contentStyle.value = {
      zIndex: 11,
      opacity: 1
    };
    vShow.value = true;
    emit('open');
  }

  /**
   * 关闭下拉菜单
   */
  function close() {
    // 下拉内容的样式进行调整,不透明度设置为0
    active.value = false;
    contentStyle.value = {
      ...contentStyle.value,
      opacity: 0
    };

    // 等待过渡动画结束后隐藏 z-index
    vShow.value = false;
    setTimeout(() => {
      contentStyle.value = {
        zIndex: -1,
        opacity: 0
      };
      emit('close');
    }, Number(props.duration));
  }

  /**
   * 点击遮罩
   */
  function maskClick() {
    if (!props.closeOnClickMask) return;
    close();
  }

  /**
   * 获取下拉菜单内容的高度
   * @description
   * dropdown组件是相对定位的,下拉内容必须给定高度,
   * 才能让遮罩占满菜单以下直到屏幕底部的高度。
   */
  function getContentHeight() {
    const windowHeight = $u.sys().windowHeight;

    $u.getRect('.u-dropdown__menu', instance).then((res: any) => {
      // 获取菜单实际高度
      menuHeight.value = res.height;

      /**
       * 尺寸计算说明:
       * 在H5端,uniapp获取尺寸存在已知问题:
       * 元素尺寸的top值为导航栏底部到元素的上边沿的距离
       * 但元素的bottom值却是导航栏顶部到元素底部的距离
       * 为避免页面滚动,此处取菜单栏的bottom值进行计算
       */
      if (currentDirection.value === 'up') {
        contentHeight.value = res.top;
      } else {
        contentHeight.value = windowHeight - res.bottom;
      }
    });
  }

  // 暴露方法
  defineExpose({
    close,
    open
  });
</script>

<style scoped lang="scss">
  @import 'uview-pro/libs/css/style.components';

  .u-dropdown {
    flex: 1;
    width: 100%;
    position: relative;
    background-color: #fff;

    &__content {
      position: absolute;
      z-index: 8;
      width: 100%;
      left: 0;
      overflow: hidden;

      &__mask {
        position: absolute;
        z-index: 9;
        background: rgba(0, 0, 0, 0.3);
        width: 100%;
        left: 0;
        top: 0;
        bottom: 0;
      }

      &__popup {
        position: absolute;
        width: 100%;
        z-index: 10;
        transition: all 0.3s;
        transform: translate3D(0, -100%, 0);
        overflow: hidden;
        background-color: var(--gray-2);

        &__close {
          width: 40rpx;
          height: 40rpx;
          display: flex;
          align-items: center;
          justify-content: center;
          position: absolute;
          right: 24rpx;
          top: 30rpx;
          z-index: 9;
        }

        &__header {
          display: flex;
          color: var(--title-1);
          font-size: var(--ft-32);
          font-weight: 500;
          line-height: 44rpx;
          padding: 30rpx 24rpx;
        }

        &__body {
          flex: 1;
          overflow: hidden;
          display: flex;
        }

        &__scroll-view {
          flex: 1;
        }

        &__footer {
          display: flex;
        }
      }
    }
  }
</style>