封装一个移动端底部弹出窗口组件

1,629 阅读2分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动

TIP 👉 长江后浪推前浪,一代新人换旧人。《增广昔时贤文》

前言

在我们日常项目开发中,我们可能会会涉及到移动端底部弹出窗口功能,所以封装了这款底部弹出窗口组件。

一、底部弹出窗口组件(只移动端可用)

此组件建议使用js方式调用

popupBottomBox 方法参数

1. title
  • 窗口标题
  • 值为字符串
2. content
  • 内容组件
  • 值为Vue对象,必填
3. contentProps
  • 弹窗内容组件的props
  • 值为Object类型,属性名为组件的props的属性
4. contentEvents
  • 弹窗内容组件的事件
  • 值为Object类型,属性名为组件事件名,属性值为事件回调方法
5. contentWrapperStyle
  • 内容包裹器样式
  • 值为Object类型,属性名为驼峰格式的样式名,属性值为样式值字符串
6. className
  • 弹框自定义样式名
  • 值为字符串
7. showCancel
  • 是否显示取消按钮
  • 值为布尔类型
  • 默认值为true
8. onConfirm
  • 点击“确认”按钮的回调函数
  • 函数参数:value 内容组件获取的值
  • 执行后窗口自动关闭如果函数返回 false,则窗口不关闭
9. onCancel
  • 点击“取消”按钮的回调函数
  • 函数无参数
  • 执行后窗口自动关闭如果函数返回 false,则窗口不关闭

二、底部窗口示例

<script>
import ScrollSelect from '@/components/m/scrollSelect'
import popupBottomBox from '@/components/m/bottomBox'

popupBottomBox({
  title: '请选择',
  content: ScrollSelect,
  contentProps: {
    value: '01',
    options: [
      { value: '01', text: '第一项' },
      { value: '02', text: '第二项' },
      { value: '03', text: '第三项' },
      { value: '04', text: '第四项' },
      { value: '05', text: '第五项' }
    ]
  },
  onConfirm: (val) => {
    console.log('确定的值为', val)
  },
  onCancel: () => {
    console.log('取消')
  }
})

</script>

实现popupBottomBox.vue

<!-- 底部窗口组件 -->
<template>
  <div class="bottombox-wrap">
    <transition name="mask">
      <div class="popup-mask" v-show="visible" @click="handelCancel" @touchmove.prevent="" @mousewheel.prevent=""></div>
    </transition>
    <transition name="pop" :duration="300">
      <div class="bottombox" v-show="visible" :class="className" @touchmove.prevent="" @mousewheel.prevent="">
        <div v-if="showOptionBar" class="option-pane">
          <div class="cancel" v-show="showCancel" @click="handelCancel">取消</div>
          <div class="ok" @click="handelConfirm">确认</div>
          <div class="title" v-show="title">{{title}}</div>
        </div>
        <div class="bottombox-pane">
          <div class="content-wrap" :style="contentWrapperStyle">
            <slot>
              <component v-if="content" :is="content" v-bind="contentProps" v-on="contentEvents" @change="onChange" ref="bottomboxContent"></component>
            </slot>
          </div>
        </div>
      </div>
    </transition>
  </div>
</template>
<script>
export default {
  name: 'BottomBox',
  props: {
    // 是否显示窗口
    value: {
      type: Boolean,
      default: false
    },
    // 标题
    title: {
      type: String,
      default: ''
    },
    // 是否显示顶部操作按钮
    showOptionBar: {
      type: Boolean,
      default: true
    },
    // 是否显示取消按钮
    showCancel: {
      type: Boolean,
      default: true
    },
    // 弹窗内容组件
    content: Object,
    // 弹窗内容组件的props
    contentProps: Object,
    // 弹窗内容组件的事件
    contentEvents: Object,
    // 内容包裹器样式
    contentWrapperStyle: Object,
    /* 点击“取消”按钮时的回调函数 */
    onCancel: {
      type: Function,
      default: () => true
    },
    /* 点击“确定”按钮时的回调函数 */
    onConfirm: {
      type: Function,
      default: () => true
    },
    // 样式名
    className: {
      type: String,
      default: ''
    }
  },
  data () {
    return {
      // 是否显示
      visible: this.value,
      // 当前内容组件的值
      currentContentValue: this.contentProps && this.contentProps.value
    }
  },
  watch: {
    value (val) {
      this.visible = val
    }
  },
  methods: {
    // 内容组件change事件监听
    onChange (val) {
      this.currentContentValue = val
    },
    /**
     * 关闭窗口
     * @param delayTime 延时关闭时间(单位:毫秒)
     */
    close (delayTime) {
      if (delayTime && typeof delayTime === 'number' && delayTime > 0) {
        setTimeout(() => {
          this.visible = false
          this.$emit('input', this.visible)
          this.$emit('close')
        }, delayTime)
      } else {
        this.visible = false
        this.$emit('input', this.visible)
        this.$emit('close')
      }
    },
    // 点击“取消”时的处理方法
    handelCancel () {
      if (this.showCancel && (this.onCancel() !== false)) {
        this.close()
      }
    },
    // 点击“确定”时的处理方法
    handelConfirm () {
      let val = this.currentContentValue
      // 如果内容组件没有初始值,并且未滚动选择值,则通过内容组件的getDefaultValue方法获取默认值
      if (!this.contentProps.value && !this.currentContentValue && this.$refs.bottomboxContent.getDefaultValue) {
        val = this.$refs.bottomboxContent.getDefaultValue()
      }
      if (this.onConfirm(val) !== false) {
        this.close()
      }
    }
  }
}
</script>
<style lang="scss" scoped>
.bottombox-wrap {
  z-index: 999;
}
.popup-mask {
  display: flex;
  align-items: center;
  justify-content: center;
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, .5);
  transition: all .3s ease;
  z-index: 10000;
}
.bottombox {
  position: fixed;
  width: 100%;
  // height: 300px;
  bottom: 0;
  left: 0;
  background-color: #FFF;
  // box-shadow: 0 0 10px rgba(0, 0, 0, .3);
  transition: all .3s ease;
  z-index: 10000;
  .option-pane{
    padding: 0 4em;
    height: $app-title-bar-height;
    line-height: $app-title-bar-height;
    // border-bottom: solid 1px $base-border-color;
    background-color: #f5f5f5;
    font-size: 32px;
    .cancel, .ok {
      position: absolute;
      top: 0;
      @include base-font-color();
    }
    .cancel {
      left: 1em;
    }
    .ok {
      right: 1em;
    }
    .title {
      text-align: center;
      color: $light-font-color;
    }
  }
  .bottombox-pane {
    display: flex;
    flex-direction: column;
    height: 100%;
    .title {
      flex: none;
      height: $app-title-bar-height;
      line-height: $app-title-bar-height;
      border-bottom: solid 1px #E5E5E5;
      font-size: 32px;
      text-align: center;
      padding: 0 3em;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
      color: #666;
    }
    .content-wrap {
      position: relative;
    }
  }
}

.pop-enter-active, .pop-leave {
  transform: translateY(0);
}
.pop-enter, .pop-leave-active {
  transform: translateY(100%);
}
.mask-enter-active, .mask-leave {
  opacity: 1;
}
.mask-enter, .mask-leave-active {
  opacity: 0;
}
</style>

index.js

/**
 * 底部窗口组件
 */
import Vue from 'vue'
import BottomBox from './BottomBox.vue'

// BottomBox构造函数
const BottomBoxConstructor = Vue.extend({
  extends: BottomBox
})

function initInstance (instance, options) {
  // 窗口标题
  instance.title = options.title || ''
  // 是否显示顶部操作按钮
  instance.showOptionBar = typeof options.showOptionBar === 'boolean' ? options.showOptionBar : true
  // 是否显示取消按钮
  instance.showCancel = typeof options.showCancel === 'boolean' ? options.showCancel : true
  // 窗口高度
  // instance.height = typeof options.height === 'number' && options.height >= 100 && options.height <= 1200 ? options.height : 300
  // 窗口内容组件
  instance.content = typeof options === 'object' && options._isVue ? options : options.content
  // 窗口内容组件参数
  instance.contentProps = typeof options.contentProps === 'object' ? options.contentProps : {}
  // 窗口内容组件事件
  instance.contentEvents = typeof options.contentEvents === 'object' ? options.contentEvents : {}
  // 内容包裹器样式
  instance.contentWrapperStyle = options.contentWrapperStyle
  // 自定义样式名
  instance.className = options.className || ''
  // “确定”回调函数
  if (options.onConfirm && typeof options.onConfirm === 'function') {
    instance.onConfirm = options.onConfirm
  }
  // “取消”回调函数
  if (options.onCancel && typeof options.onCancel === 'function') {
    instance.onCancel = options.onCancel
  }
  // 父节点
  let parentElement = options.parentElement || document.body
  // 关闭时移除
  instance.$on('close', () => {
    setTimeout(() => {
      parentElement.removeChild(instance.$el)
      instance.$destroy()
    }, 2000)
  })
  // console.log('instance.$el=', instance.$el)
  // 将节点添加到文档
  parentElement.appendChild(instance.$el)

  instance.visible = true
  instance.closed = false
}

// 显示弹出窗口
export function popupBottomBox (options = {}) {
  let instance = new BottomBoxConstructor({
    el: document.createElement('div')
  })
  initInstance(instance, options)
  return instance
}

export default popupBottomBox

「欢迎在评论区讨论」

希望看完的朋友可以给个赞,鼓励一下