quanyawei
2023-11-06 1e61215b48e59e94c1ed98e4ef956227d689d6bc
uni_modules/uview-ui/components/u-picker/u-picker.vue
New file
@@ -0,0 +1,283 @@
<template>
   <u-popup
      :show="show"
      @close="closeHandler"
   >
      <view class="u-picker">
         <u-toolbar
            v-if="showToolbar"
            :cancelColor="cancelColor"
            :confirmColor="confirmColor"
            :cancelText="cancelText"
            :confirmText="confirmText"
            :title="title"
            @cancel="cancel"
            @confirm="confirm"
         ></u-toolbar>
         <picker-view
            class="u-picker__view"
            :indicatorStyle="`height: ${$u.addUnit(itemHeight)}`"
            :value="innerIndex"
            :immediateChange="immediateChange"
            :style="{
               height: `${$u.addUnit(visibleItemCount * itemHeight)}`
            }"
            @change="changeHandler"
         >
            <picker-view-column
               v-for="(item, index) in innerColumns"
               :key="index"
               class="u-picker__view__column"
            >
               <text
                  v-if="$u.test.array(item)"
                  class="u-picker__view__column__item u-line-1"
                  v-for="(item1, index1) in item"
                  :key="index1"
                  :style="{
                     height: $u.addUnit(itemHeight),
                     lineHeight: $u.addUnit(itemHeight),
                     fontWeight: index1 === innerIndex[index] ? 'bold' : 'normal'
                  }"
               >{{ getItemText(item1) }}</text>
            </picker-view-column>
         </picker-view>
         <view
            v-if="loading"
            class="u-picker--loading"
         >
            <u-loading-icon mode="circle"></u-loading-icon>
         </view>
      </view>
   </u-popup>
</template>
<script>
/**
 * u-picker
 * @description 选择器
 * @property {Boolean}         show            是否显示picker弹窗(默认 false )
 * @property {Boolean}         showToolbar         是否显示顶部的操作栏(默认 true )
 * @property {String}         title            顶部标题
 * @property {Array}         columns            对象数组,设置每一列的数据
 * @property {Boolean}         loading            是否显示加载中状态(默认 false )
 * @property {String | Number}   itemHeight         各列中,单个选项的高度(默认 44 )
 * @property {String}         cancelText         取消按钮的文字(默认 '取消' )
 * @property {String}         confirmText         确认按钮的文字(默认 '确定' )
 * @property {String}         cancelColor         取消按钮的颜色(默认 '#909193' )
 * @property {String}         confirmColor      确认按钮的颜色(默认 '#3c9cff' )
 * @property {String | Number}   visibleItemCount   每列中可见选项的数量(默认 5 )
 * @property {String}         keyName            选项对象中,需要展示的属性键名(默认 'text' )
 * @property {Boolean}         closeOnClickOverlay   是否允许点击遮罩关闭选择器(默认 false )
 * @property {Array}         defaultIndex      各列的默认索引
 * @property {Boolean}         immediateChange      是否在手指松开时立即触发change事件(默认 false )
 * @event {Function} close      关闭选择器时触发
 * @event {Function} cancel      点击取消按钮触发
 * @event {Function} change      当选择值变化时触发
 * @event {Function} confirm   点击确定按钮,返回当前选择的值
 */
import props from './props.js';
export default {
   name: 'u-picker',
   mixins: [uni.$u.mpMixin, uni.$u.mixin, props],
   data() {
      return {
         // 上一次选择的列索引
         lastIndex: [],
         // 索引值 ,对应picker-view的value
         innerIndex: [],
         // 各列的值
         innerColumns: [],
         // 上一次的变化列索引
         columnIndex: 0,
      }
   },
   watch: {
      // 监听默认索引的变化,重新设置对应的值
      defaultIndex: {
         immediate: true,
         handler(n) {
            this.setIndexs(n, true)
         }
      },
      // 监听columns参数的变化
      columns: {
         immediate: true,
         handler(n) {
            this.setColumns(n)
         }
      },
   },
   methods: {
      // 获取item需要显示的文字,判别为对象还是文本
      getItemText(item) {
         if (uni.$u.test.object(item)) {
            return item[this.keyName]
         } else {
            return item
         }
      },
      // 关闭选择器
      closeHandler() {
         if (this.closeOnClickOverlay) {
            this.$emit('close')
         }
      },
      // 点击工具栏的取消按钮
      cancel() {
         this.$emit('cancel')
      },
      // 点击工具栏的确定按钮
      confirm() {
         this.$emit('confirm', {
            indexs: this.innerIndex,
            value: this.innerColumns.map((item, index) => item[this.innerIndex[index]]),
            values: this.innerColumns
         })
      },
      // 选择器某一列的数据发生变化时触发
      changeHandler(e) {
         const {
            value
         } = e.detail
         let index = 0,
            columnIndex = 0
         // 通过对比前后两次的列索引,得出当前变化的是哪一列
         for (let i = 0; i < value.length; i++) {
            let item = value[i]
            if (item !== (this.lastIndex[i] || 0)) { // 把undefined转为合法假值0
               // 设置columnIndex为当前变化列的索引
               columnIndex = i
               // index则为变化列中的变化项的索引
               index = item
               break // 终止循环,即使少一次循环,也是性能的提升
            }
         }
         this.columnIndex = columnIndex
         const values = this.innerColumns
         // 将当前的各项变化索引,设置为"上一次"的索引变化值
         this.setLastIndex(value)
         this.setIndexs(value)
         this.$emit('change', {
            // #ifndef MP-WEIXIN || MP-LARK
            // 微信小程序不能传递this,会因为循环引用而报错
            picker: this,
            // #endif
            value: this.innerColumns.map((item, index) => item[value[index]]),
            index,
            indexs: value,
            // values为当前变化列的数组内容
            values,
            columnIndex
         })
      },
      // 设置index索引,此方法可被外部调用设置
      setIndexs(index, setLastIndex) {
         this.innerIndex = uni.$u.deepClone(index)
         if (setLastIndex) {
            this.setLastIndex(index)
         }
      },
      // 记录上一次的各列索引位置
      setLastIndex(index) {
         // 当能进入此方法,意味着当前设置的各列默认索引,即为“上一次”的选中值,需要记录,是因为changeHandler中
         // 需要拿前后的变化值进行对比,得出当前发生改变的是哪一列
         this.lastIndex = uni.$u.deepClone(index)
      },
      // 设置对应列选项的所有值
      setColumnValues(columnIndex, values) {
         // 替换innerColumns数组中columnIndex索引的值为values,使用的是数组的splice方法
         this.innerColumns.splice(columnIndex, 1, values)
         // 拷贝一份原有的innerIndex做临时变量,将大于当前变化列的所有的列的默认索引设置为0
         let tmpIndex = uni.$u.deepClone(this.innerIndex)
         for (let i = 0; i < this.innerColumns.length; i++) {
            if (i > this.columnIndex) {
               tmpIndex[i] = 0
            }
         }
         // 一次性赋值,不能单个修改,否则无效
         this.setIndexs(tmpIndex)
      },
      // 获取对应列的所有选项
      getColumnValues(columnIndex) {
         // 进行同步阻塞,因为外部得到change事件之后,可能需要执行setColumnValues更新列的值
         // 索引如果在外部change的回调中调用getColumnValues的话,可能无法得到变更后的列值,这里进行一定延时,保证值的准确性
         (async () => {
            await uni.$u.sleep()
         })()
         return this.innerColumns[columnIndex]
      },
      // 设置整体各列的columns的值
      setColumns(columns) {
         this.innerColumns = uni.$u.deepClone(columns)
         // 如果在设置各列数据时,没有被设置默认的各列索引defaultIndex,那么用0去填充它,数组长度为列的数量
         if (this.innerIndex.length === 0) {
            this.innerIndex = new Array(columns.length).fill(0)
         }
      },
      // 获取各列选中值对应的索引
      getIndexs() {
         return this.innerIndex
      },
      // 获取各列选中的值
      getValues() {
         // 进行同步阻塞,因为外部得到change事件之后,可能需要执行setColumnValues更新列的值
         // 索引如果在外部change的回调中调用getValues的话,可能无法得到变更后的列值,这里进行一定延时,保证值的准确性
         (async () => {
            await uni.$u.sleep()
         })()
         return this.innerColumns.map((item, index) => item[this.innerIndex[index]])
      }
   },
}
</script>
<style lang="scss" scoped>
   @import "../../libs/css/components.scss";
   .u-picker {
      position: relative;
      &__view {
         &__column {
            @include flex;
            flex: 1;
            justify-content: center;
            &__item {
               @include flex;
               justify-content: center;
               align-items: center;
               font-size: 16px;
               text-align: center;
               /* #ifndef APP-NVUE */
               display: block;
               /* #endif */
               color: $u-main-color;
               &--disabled {
                  /* #ifndef APP-NVUE */
                  cursor: not-allowed;
                  /* #endif */
                  opacity: 0.35;
               }
            }
         }
      }
      &--loading {
         position: absolute;
         top: 0;
         right: 0;
         left: 0;
         bottom: 0;
         @include flex;
         justify-content: center;
         align-items: center;
         background-color: rgba(255, 255, 255, 0.87);
         z-index: 1000;
      }
   }
</style>