picker/picker.vue

<template>
    <div class="ion-picker-cmp" :class="[modeClass,cssClass]">
        <vm-backdrop :enableBackdropDismiss="enableBackdropDismiss" :isActive="isActive"
                     :bdClick="bdClick"></vm-backdrop>
        <transition
                name="picker"
                @before-enter="beforeEnter"
                @after-enter="afterEnter"
                @before-leave="beforeLeave"
                @after-leave="afterLeave">
            <div class="picker-wrapper" v-show="isActive">
                <div class="picker-toolbar">
                    <div v-for="(b,index) in buttons"
                         :key="index"
                         class="picker-toolbar-button"
                         :class="[b.cssRole]">
                        <vm-button @click="btnClick(b)" :class="b.cssClass" class="picker-button" clear>{{b.text}}</vm-button>
                    </div>
                </div>
                <div class="picker-columns">
                    <div class="picker-above-highlight"></div>
                    <PickerCol v-for="(c,index) in columns"
                               :index="index"
                               :key="c.name"
                               :col="c"
                               @onChange="colChange"></PickerCol>
                    <div class="picker-below-highlight"></div>
                </div>
            </div>
        </transition>
    </div>
</template>
<style lang="scss" src="./style.scss"></style>
<script type="text/javascript">
  /**
   * @component Picker
   * @description
   *
   * ## 弹出层组件 / 单多列选择器Picker
   *
   * ### 简介
   *
   * Picker组件适用于单多列数据的选择, 这个不同于Select组件, Select组件只能做单类型数据的选择, 比如选择水果的话不能类别和新鲜度同时选择. 因此, 如果期望能进行城市三级选择, Picker能够解决这个需求, 详情参考Demo.
   *
   * ### 传参说明
   *
   * 因为Picker组件为了满足大多数的需求, 且能根据业务定制化, 比如被Datetime组件/CitySelect业务定制等. 因此Picker传参有点点麻烦. 主要参数如下:
   *
   *
   * #### 1. buttons属性
   *
   名称 / Name | 类型 / Type | 描述 / Description
   ----------|-----------|--------------------------------
   text      | string    | 显示的文本
   role      | string    | 按钮的角色, 取消按钮的role为cancel, 其他的没有
   handler   | function  | 点击按钮的处理方法
   *
   * #### 2. columns属性
   *
   名称 / Name     | 类型 / Type          | 描述 / Description
   --------------|--------------------|---------------------------
   name          | string             | 这个column的名称
   align         | string             | column对齐方式
   selectedIndex | number             | 当前column的初始选的位置(index)
   prefix        | string             | 这一列显示的前缀
   prefixWidth   | string             | 前缀固定宽度, 例如'50px'
   suffix        | string             | 这一列显示的后缀
   suffixWidth   | string             | 后缀固定宽度, 例如'50px'
   cssClass      | string             | 当前column的自定义样式, 使用空格分割多个值
   columnWidth   | string             | 每个column的最大宽度, 默认宽度均分
   optionsWidth  | string             | 每个column中的选项最大宽度
   options       | PickerColumnOption | 当前column的每个选项列表
   *
   *
   * #### 3. columns属性中的options属性
   *
   名称 / Name | 类型 / Type | 描述 / Description
   ----------|-----------|------------------
   text      | string    | 显示的文本
   value     | *         | 对显示文本的值
   disabled  | boolean   | 是否禁用
   *
   *
   * #### 4. onChange钩子
   *
   * 当用户选定后触发钩子, 传递的数据包括三列的详细选择信息.
   *
   * #### 5. onSelect钩子
   *
   * 某一列选择之后触发, 包含单列的选择信息
   *
   * ### 组件自动支持切换alipay组件
   *
   * 如果强制使用H5模式需要在开启options中传入`isH5=true`, alipay组件只支持最多两级且不可联动.
   *
   * ### 如何使用
   *
   * 因为是弹出层组件, 故引入后, `Picker.present(...)`就可打开组件
   *
   * ```
   *  import { Picker } from 'vimo'
   * ```
   *
   * @demo #/picker
   * */
  import { isNumber, isString, isPresent } from '../../util/type'
  import PickerCol from './picker-col.vue'
  import Backdrop from '../backdrop'
  import Button from '../button'
  import popupExtend from '../../util/popup-extend'

  export default {
    name: 'Picker',
    provide () {
      const _this = this
      return {
        pickerComponent: _this
      }
    },
    components: {'vm-backdrop': Backdrop, PickerCol, 'vm-button': Button},
    extends: popupExtend,
    data () {
      return {
        cols: [],               // 每列的数据
        timer: null,            // 计时器
        unreg: null             // 页面切换关闭组件的解绑函数
      }
    },
    props: {
      buttons: {
        type: Array,
        required: true
      },
      columns: {
        type: Array,
        required: true
      },
      mode: {
        type: String,
        default () { return this.$config && this.$config.get('mode') || 'ios' }
      },
      cssClass: String,
      enableBackdropDismiss: {
        type: Boolean,
        default: true
      },
      onChange: Function,
      onSelect: Function,
      onDismiss: Function
    },
    computed: {
      modeClass () {
        return `picker-${this.mode}`
      }
    },
    methods: {
      /**
       * 背景backdrop点击
       * @private
       * */
      bdClick () {
        if (this.enabled && this.enableBackdropDismiss) {
          this.dismiss()
        }
      },
      /**
       * 标题左右的两个按钮点击(取消/确定)
       * @private
       * */
      btnClick (button) {
        if (!this.enabled) {
          return
        }

        let shouldDismiss = true

        if (button.handler) {
          // a handler has been provided, execute it
          // pass the handler the values from the inputs
          if (button.handler(this.getSelected()) === false) {
            // if the return value of the handler is false then do not dismiss
            shouldDismiss = false
          }
        }

        if (shouldDismiss) {
          this.dismiss(button.role)
        }
      },

      /**
       * 获取当前选择的值, 有多少列返回多少列的数据
       * @return {Array} selected - selected
       * @return {String} selected.text - text
       * @return {String} selected.value - value
       * @return {String} selected.columnIndex - columnIndex
       * @private
       * */
      getSelected () {
        let selected = {}
        this.columns.forEach((col, index) => {
          let selectedColumn = col.options[col.selectedIndex]
          selected[col.name] = {
            text: selectedColumn ? selectedColumn.text : null,
            value: selectedColumn ? selectedColumn.value : null,
            columnIndex: index
          }
        })
        return selected
      },

      /**
       * 当选择变化,对外发送事件
       * @private
       * */
      colChange (data) {
        // col发生变化时触发onSelect事件, 传递触发col的信息
        this.$emit('onSelect', data)
        this.onSelect && this.onSelect(data)

        // col发生变化, 则获取当前选中的整体数据, 触发onChange事件
        let selectedData = this.getSelected()
        this.$emit('onChange', selectedData)
        this.onChange && this.onChange(selectedData)
      },

      /**
       * data数据格式化
       *
       * PS: 在this.data原值上操作
       *
       * - this.buttons: 如果传入的string字符串数组, 则button的文本将是这个字符串
       * - this.buttons: 如过role定义了, 则加上cssRole: `picker-toolbar-${button.role}`
       * - columns -> column.options: 如果不是对象, 则将传入的值toString后转给text/value
       *
       * @private
       * */
      normalizeData () {
        // normalize the data
        this.buttons = this.buttons.map(button => {
          if (isString(button)) {
            return {text: button}
          }
          if (button.role) {
            // role: cancel / button
            button.cssRole = `picker-toolbar-${button.role}`
          }
          return button
        })

        // clean up dat data
        this.columns = this.columns.map(column => {
          if (!isPresent(column.options)) {
            column.options = []
          }
          // 取值必须>=0
          column.selectedIndex = Math.max(0, parseInt(column.selectedIndex))
          column.options = column.options.map(inputOpt => {
            // PickerColumnOption
            let opt = {
              text: '',
              value: '',
              disabled: inputOpt.disabled
            }

            if (isPresent(inputOpt)) {
              if (isString(inputOpt) || isNumber(inputOpt)) {
                opt.text = inputOpt.toString()
                opt.value = inputOpt
              } else {
                opt.text = isPresent(inputOpt.text) ? inputOpt.text : inputOpt.value
                opt.value = isPresent(inputOpt.value) ? inputOpt.value : inputOpt.text
              }
            }
            return opt
          })
          return column
        })
      },

      /**
       * 由子组件调用, 用于记录自己
       * @private
       * */
      recordChildComponent (childComponent) {
        this.cols.push(childComponent)
      },

      // ------- 动态添加数据接口 --------
      /**
       * 动态添加修改列数据时,对某一列数据修改并刷新显示
       * */
      resetColumn (index) {
        this.cols[index].reset()
      },

      /**
       * 如果设置的选中值与显示不一致, 使用这个刷新, 他会更新滚动位置
       * @private
       * */
      refresh () {
        this.cols.forEach(column => {
          column.refresh()
        })
      }
    },
    created () {
      this.normalizeData()
    },
    beforeMount () {
      let pickerCmpElements = document.querySelectorAll('.ion-picker-cmp')
      if (pickerCmpElements.length > 0) {
        pickerCmpElements.forEach((ele) => {
          ele.remove()
          console.error('beforeMount find Picker opened!')
        })
      }
    }
  }
</script>