Popup 弹出层

593 阅读2分钟

Popup 代码

PkMask组件在其他章节中

HTML

<template>
  <view>
    <PkMask v-if="props.mask" 
      :visible="props.visible"
      :closeOnClickMask="props.closeOnClickMask"
      :zIndex="props.zIndex"
      :lockScroll="props.lockScroll"
      :duration="props.duration"
      @click="onClickMask"
       />
    <transition :name="transitionName"
      @after-enter="onOpened" 
      @after-leave="onClosed">
      <view v-show="props.visible" 
        :class="classes" 
        :style="popStyle" 
        @click="onClick">
        <slot v-if="state.showSlot"></slot>
        <view v-if="state.closed"
        @click="onClickCloseIcon"
          class="pk-popup__close-icon"
          :class="'pk-popup__close-icon--' + closeIconPosition">
          <slot name="close-icon">
            <view class="close"></view>
          </slot>
        </view>
      </view>
    </transition>
  </view>
</template>

script

<script lang="ts" setup>
import PkMask from '@/components/pk-mask/index.vue'
// import type { Props } from './interface'

import { computed, ComputedRef, watchEffect, reactive, watch } from 'vue'

interface Props {
  visible: Boolean;
  zIndex?: Number;
  duration?: Number;
  lockScroll?: Boolean;
  mask?: Boolean;
  closeOnClickMask?: Boolean;
  style?: Object;
  position?: 'center' | 'top' | 'bottom' | 'left' | 'right';
  transition?: String;
  closeable?: Boolean;
  closeIconPosition?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
  destroyOnClose?: Boolean;
  round?: Boolean;
  safeAreaInsetBottom: Boolean;
}

const props = withDefaults(defineProps<Props>(), {
  visible:() => false, // 控制当前组件显示/隐藏
  zIndex: () => 99, // 遮罩层级
  duration: () => 300, // 组件显示/隐藏的动画时长,单位毫秒
  lockScroll: () => true, // 背景是否锁定
  mask: () => true, // 是否显示遮罩
  closeOnClickMask: () => true, // 是否点击遮罩关闭
  position: () => 'center', // 弹出位置
  transition: () => '', // 动画名
  style:() => '', // 自定义弹框样式
  closeable: () => false, // 是否显示关闭按钮
  closeIconPosition: () => 'top-right', // 关闭按钮位置
  destroyOnClose: () => true, // 弹层关闭后 slot内容会不会清空
  round: () => false, // 是否显示圆角
  safeAreaInsetBottom:() => false //是否开启 iphone 系列全面屏底部安全区适配,仅当 position 为 bottom 时有效
})

const initIndex = 2000;
let _zIndex = initIndex;

const emits = defineEmits<{
  (e:'click-pop', value:any): void;
  (e:'click-close-icon', value:any): void;
  (e:'open'): void;
  (e:'close'): void;
  (e:'opend', value:any): void;
  (e:'closed', value:any): void;
  (e:'click-mask', value:any): void;
  (e:'update:visible', value:any): void;
}>()

const state = reactive({
  zIndex:  props.zIndex,
  showSlot: true,
  closed: props.closeable
})

const classes = computed(() => {
  const prefixCls = 'pk-popup'
  return {
    [prefixCls]: true,
    ['round']: props.round,
    [`pk-popup--${props.position}`]: true,
    [`pk-popup--${props.position}--safebottom`]: props.position === 'bottom' && props.safeAreaInsetBottom
  }
})

const popStyle: ComputedRef = computed(() => {
  return {
    zIndex: state.zIndex,
    transitionDuration: `${props.duration}ms`,
    ...props.style
  };
})

const transitionName: ComputedRef = computed(() => {
  return props.transition ? props.transition : `pk-popup-slide-${props.position}`;
})

const open = () => {
  if (props.zIndex !== initIndex) {
    _zIndex = Number(props.zIndex);
  }
  emits('update:visible', true);
  state.zIndex = ++_zIndex;
  if (props.destroyOnClose) {
    state.showSlot = true;
  }
  emits('open');
}

const close = () => {
  emits('update:visible', false);
  emits('close');
  if (props.destroyOnClose) {
    setTimeout(() => {
      state.showSlot = false;
    }, +props.duration);
  }
}

const onClick = (e: Event) => {
  emits('click-pop', e);
};

const onClickCloseIcon = (e: Event) => {
  e.stopPropagation();
  emits('click-close-icon', e);
  emits('update:visible', false);
  // close();
};

const onClickMask = (e: Event) => {
  if (props.closeOnClickMask) {
    emits('click-mask', e);
    emits('update:visible', false);
    // close();
  }
};

const onOpened = (e: Event) => {
  emits('opend', e);
};

const onClosed = (e: Event) => {
  emits('closed', e);
};

watch(
  () => props.visible,
  (val) => {
    props.visible ? open() : close();
  }
);

watchEffect(() => {
  // props.visible ? open() : close();
  state.closed = props.closeable;
});

</script>

style

<style lang="scss" scoped>
.pk-popup {
  position: fixed;
  max-height: 100%;
  overflow-y: auto;
  background: #fff;
  &__close-icon {
    color: #969799;
  }
}

.pk-popup-slide {
  &-center-enter-active,
  &-center-leave-active {
    transition-property: opacity;
    transition-timing-function: ease;
  }

  &-center-enter-from,
  &-center-leave-to {
    opacity: 0;
  }

  &-top-enter-from,
  &-top-leave-active {
    transform: translate(0, -100%);
  }

  &-right-enter-from,
  &-right-leave-active {
    transform: translate(100%, 0);
  }

  &-bottom-enter-from,
  &-bottom-leave-active {
    transform: translate(0, 100%);
  }

  &-left-enter-from,
  &-left-leave-active {
    transform: translate(-100%, 0);
  }
}

.pk-popup--center {
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  &.round {
    border-radius: 20rpx 20rpx 0 0;
  }
}

.pk-popup--bottom {
  bottom: 0;
  left: 0;
  width: 100%;
  &.round {
    border-radius: 20rpx 20rpx 0 0;
  }
  &--safebottom {
    padding-bottom: constant(safe-area-inset-bottom);
    padding-bottom: env(safe-area-inset-bottom);
  }
}

.pk-popup--right {
  top: 0;
  right: 0;

  &.round {
    border-radius: 20rpx 0 0 20rpx;
  }
}

.pk-popup--left {
  top: 0;
  left: 0;

  &.round {
    border-radius: 0 20rpx 20rpx 0;
  }
}

.pk-popup--top {
  top: 0;
  left: 0;
  width: 100%;

  &.round {
    border-radius: 0 0 20rpx 20rpx;
  }
}

.pk-popup {
  position: fixed;
  max-height: 100%;
  overflow-y: auto;
  background-color: #fff;
  -webkit-overflow-scrolling: touch;
  &__close-icon {
    position: absolute !important;
    z-index: 1;
    color: #969799;
    font-size: 18px;
    cursor: pointer;
    width: 30px;
    height: 30px;
    line-height: 30px;
    text-align: center;

    &:active {
      opacity: 0.7;
    }

    &--top-left {
      top: 32rpx;
      left: 32rpx;
    }

    &--top-right {
      top: 32rpx;
      right: 32rpx;
    }

    &--bottom-left {
      bottom: 32rpx;
      left: 32rpx;
    }

    &--bottom-right {
      right: 32rpx;
      bottom: 32rpx;
    }
  }
}

.close {
  position: realtive;
}
.close::before,
.close::after {
  content: '';
  position: absolute;
  background-color: #969799;
  left: 18rpx;
  width: 2rpx;
  height: 36rpx;
  top: 0rpx;
}
.close::before {
  transform: rotate(45deg);
}
.close::after {
  transform: rotate(-45deg);
}


</style>

Popup弹出层使用文档

引用

import pkPopup from '@/components/pk-popup/index.vue';

基础用法

<template>
  <view @click="visible = true">开关</view>
  <pk-popup :style="{ padding: '30px 50px' }" v-model:visible="visible">正文</pk-popup>
</template>
<script lang="ts" setup>
  import { ref, onMounted } from 'vue';
  const visible = ref(false)

</script>

弹出位置

💡 Tips:通过设置 position 的值来控制弹出位置

<template>
  <veiw @click="showTop = true">顶部弹出</veiw>
  <pk-popup position="top" :style="{ height: '20%' }" v-model:visible="showTop"></pk-popup>
  <veiw @click="showBottom = true">底部弹出</veiw>
  <pk-popup position="bottom" :style="{ height: '20%' }" v-model:visible="showBottom"></pk-popup>
  <veiw @click="showLeft = true">左侧弹出</veiw>
  <pk-popup position="left" :style="{ width: '20%', height: '100%' }" v-model:visible="showLeft"></pk-popup>
  <veiw @click="showRight = true">右侧弹出</veiw>
  <pk-popup position="right" :style="{ width: '20%', height: '100%' }" v-model:visible="showRight"></pk-popup>
</template>
<script lang="ts" setup>
  import { ref, onMounted } from 'vue';
  const showTop = ref(false)
  const showBottom = ref(false)
  const showLeft = ref(false)
  const showRight = ref(false)

</script>

图标

💡 Tips:通过 closeable 控制图标是否可关闭,close-icon-position 来设置图标的位置,close-icon 来自定义显示图标

<view @click="showIcon = true">关闭图标</view>
  <pk-popup position="bottom" closeable :style="{ height: '20%' }" v-model:visible="showIcon"></pk-popup>
  <view @click="showIconPosition = true" >图标位置</view>
  <pk-popup position="bottom" closeable close-icon-position="top-left" :style="{ height: '20%' }" v-model:visible="showIconPosition" ></pk-popup>
  <view @click="showCloseIcon = true" >自定义图标</view>
  <pk-popup position="bottom" closeable close-icon-position="top-left" :style="{ height: '20%' }" v-model:visible="showCloseIcon">
    <template #close-icon>
      <Heart></Heart>
    </template>
  </pk-popup>
</template>

<script lang="ts" setup>
  import { ref, onMounted } from 'vue';
  import { Heart } from '@/icon/icons-vue';
  const showIcon = ref(false)
  const showIconPosition = ref(false)
  const showCloseIcon = ref(false)

</script>

圆角弹框

💡 Tips:通过设置 round 来控制是否显示圆角

<template>
  <view @click="showRound = true">圆角弹框</view>
  <pk-popup position="bottom" closeable round :style="{ height: '30%' }" v-model:visible="showRound"></pk-popup>
</template>

<script lang="ts" setup>
  import { ref, onMounted } from 'vue';
  const showRound = ref(false)

</script>

API

参数说明类型默认值
v-model:visible控制当前组件显示/隐藏booleanfalse
zIndex遮罩层级number99
duration组件显示/隐藏的动画时长,单位毫秒number300
lockScroll背景是否锁定booleantrue
mask是否显示遮罩booleantrue
closeOnClickMask是否点击遮罩关闭booleantrue
position弹出位置(top,bottom,left,right,centerstringcenter
transition动画名string
style自定义弹框样式CSSProperties
closeable是否显示关闭按钮booleanfalse
closeIconPosition关闭按钮位置(top-left,top-right,bottom-left,bottom-rightstringtrue
destroyOnClose弹层关闭后 slot内容会不会清空弹层关闭后 slot内容会不会清空true
round是否显示圆角booleanfalse
safeAreaInsetBottom是否开启 iphone 系列全面屏底部安全区适配,仅当 positionbottom 时有效booleanfalse

Events

事件名说明回调参数
click-pop点击弹出层时触发event:Event
click-close-icon点击关闭图标时触发event:Event
open打开弹框时触发
close关闭弹框时触发
opend遮罩打开动画结束时触发event:Event
closed遮罩关闭动画结束时触发event:Event
click-mask点击遮罩触发event:Event

Slots

名称说明
default自定义内嵌内容
close-icon关闭按钮的自定义图标