<template> 
 | 
    <view> 
 | 
        <view @click="showPicker"> 
 | 
            <slot></slot> 
 | 
        </view> 
 | 
        <view ref="picker" class="picker-pop" v-if="show" @touchmove="stopEvent"> 
 | 
            <view class="picker-mask" @click="cancel" :style="{height: screenHeight}"></view> 
 | 
            <view class="picker-panel" :style="pickerPanelTranslate"> 
 | 
                <view class="picker-action"> 
 | 
                    <text class="cancel" @click="cancel" :style="pickerStyle.cancel">取消</text> 
 | 
                    <text class="confirm" @click="confirm" :style="pickerStyle.confirm">确定</text> 
 | 
                </view> 
 | 
                <view class="picker-content"> 
 | 
                    <view class="picker-column" v-for="(column, columnIndex) in columns" :key="columnIndex" 
 | 
                         :style="pickerStyle.column[columnIndex]" :data-column="columnIndex" 
 | 
                         @touchstart="touchstart" @touchmove="touchmove" @touchend="touchend"> 
 | 
                        <view class="scroll-wrapper"> 
 | 
                            <view class="scroll-list" :style="column.style"> 
 | 
                                <view class="picker-item" v-for="(data, itemIndex) in column.pickerList" :key="itemIndex"> 
 | 
                                    <text class="picker-item-text">{{data[pickerKey.label]}}</text> 
 | 
                                </view> 
 | 
                            </view> 
 | 
                            <view class="top-cover"></view> 
 | 
                            <view class="top-cover-border"></view> 
 | 
                            <view class="bottom-cover"></view> 
 | 
                            <view class="bottom-cover-border"></view> 
 | 
                        </view> 
 | 
                    </view> 
 | 
                </view> 
 | 
            </view> 
 | 
        </view> 
 | 
    </view> 
 | 
</template> 
 | 
  
 | 
<script> 
 | 
    export default { 
 | 
        props: { 
 | 
            pickerList: { 
 | 
                value: Array, 
 | 
                require: true, 
 | 
                default() { 
 | 
                    return [] 
 | 
                } 
 | 
            }, 
 | 
            pickerKey: { 
 | 
                value: Object, 
 | 
                default() { 
 | 
                    return { 
 | 
                        value: 'value', 
 | 
                        label: 'label', 
 | 
                        children: 'children' 
 | 
                    } 
 | 
                } 
 | 
            }, 
 | 
            pickerStyle: { 
 | 
                value: Object, 
 | 
                default() { 
 | 
                    return { 
 | 
                        cancel: {}, 
 | 
                        confirm: {}, 
 | 
                        column: [] 
 | 
                    } 
 | 
                } 
 | 
            }, 
 | 
            defaultValue: { 
 | 
                value: Array, 
 | 
                default() { 
 | 
                    return [] 
 | 
                } 
 | 
            }, 
 | 
            columnNum: { 
 | 
                value: Number, 
 | 
                default: 0 
 | 
            }, 
 | 
            itemRotateDeg: { 
 | 
                value: Number, 
 | 
                default: 15 
 | 
            }, 
 | 
            beforeSetColumn: { 
 | 
                value: Function, 
 | 
                default: null 
 | 
            }, 
 | 
            speedUpRatio: { 
 | 
                value: Number, 
 | 
                default: 1 
 | 
            }, 
 | 
        }, 
 | 
        data() { 
 | 
            return { 
 | 
                show: false, 
 | 
                reactModel: true, 
 | 
                columns: [], 
 | 
                systemInfo: uni.getSystemInfoSync(), 
 | 
                startScrollTop: 0, 
 | 
                startPickedIndex: 0, 
 | 
                scrollingColumnIndex: 0, 
 | 
            } 
 | 
        }, 
 | 
        watch: { 
 | 
            pickerList() { 
 | 
                this.init() 
 | 
            }, 
 | 
            defaultValue() { 
 | 
                this.init() 
 | 
            }, 
 | 
        }, 
 | 
        computed: { 
 | 
            pickerItemStyle() { 
 | 
                return function(pickedIndex, itemIndex) { 
 | 
                    let distance = Math.abs(pickedIndex - itemIndex) 
 | 
                    if (distance <= 3) { 
 | 
                        return { 
 | 
                            transform: 'rotateX(' + distance * this.itemRotateDeg + 'deg)' 
 | 
                        } 
 | 
                    } else { 
 | 
                        return {} 
 | 
                    } 
 | 
                } 
 | 
            }, 
 | 
            screenHeight() { 
 | 
                return this.systemInfo.screenHeight 
 | 
            }, 
 | 
            pickerItemHeight() { 
 | 
                return Math.floor(68 * this.systemInfo.windowWidth / 750) 
 | 
            }, 
 | 
            pickerPanelTranslate() { 
 | 
                if (this.show) { 
 | 
                    return { 
 | 
                        transform: "translate(0, -" + this.systemInfo.windowBottom + ");" 
 | 
                    } 
 | 
                } else { 
 | 
                    return { 
 | 
                        transform: "translate(0, 100%);" 
 | 
                    } 
 | 
                } 
 | 
            } 
 | 
        }, 
 | 
        mounted() { 
 | 
        }, 
 | 
        methods: { 
 | 
            stopEvent(event) { 
 | 
                event.stopPropagation() 
 | 
            }, 
 | 
            init() { 
 | 
                if (Array.isArray(this.pickerList[0])) { 
 | 
                    this.pickerList.forEach((pickerList, index) => { 
 | 
                        this.setColumn(index, pickerList) 
 | 
                    }) 
 | 
                    this.reactModel = false; 
 | 
                } else { 
 | 
                    this.setColumn(0, this.pickerList) 
 | 
                } 
 | 
            }, 
 | 
            showPicker(event) { 
 | 
                this.stopEvent(event) 
 | 
                this.init() 
 | 
                if (this.inited) { 
 | 
                    this.show = true 
 | 
                } else { 
 | 
                    // #ifdef H5 
 | 
                    let $picker = this.$refs.picker 
 | 
                    document.body.appendChild($picker) 
 | 
                    // #endif 
 | 
                    setTimeout(() => { 
 | 
                        this.show = true 
 | 
                    }, 20) 
 | 
                    this.inited = true 
 | 
                } 
 | 
                this.$emit('click') // 传递click事件 
 | 
            }, 
 | 
            confirm() { 
 | 
                let picked = {index: [], value: [], label: [], indexes: [],values: [], labels: []} 
 | 
                for (let column of this.columns) { 
 | 
                    let columnPicked = this.columnPickedInfo(column) 
 | 
                    if (columnPicked) { 
 | 
                        picked.index = columnPicked.index 
 | 
                        picked.value = columnPicked.value 
 | 
                        picked.label = columnPicked.label 
 | 
  
 | 
                        picked.indexes.push(columnPicked.index) 
 | 
                        picked.values.push(columnPicked.value) 
 | 
                        picked.labels.push(columnPicked.label) 
 | 
                    } else { 
 | 
                        picked.indexes.push(null) 
 | 
                        picked.values.push(null) 
 | 
                        picked.labels.push(null) 
 | 
                    } 
 | 
                } 
 | 
                this.$emit('confirm', picked) 
 | 
                this.hide() 
 | 
            }, 
 | 
            cancel() { 
 | 
                this.$emit('cancel') 
 | 
                this.hide() 
 | 
            }, 
 | 
            hide() { 
 | 
                this.show = false 
 | 
            }, 
 | 
            columnPickedInfo(column) { 
 | 
                if (column.pickerList.length < 1) { 
 | 
                    return null 
 | 
                } 
 | 
                return { 
 | 
                    index: column.pickedIndex, 
 | 
                    value: column.pickerList[column.pickedIndex][this.pickerKey.value], 
 | 
                    label: column.pickerList[column.pickedIndex][this.pickerKey.label], 
 | 
                } 
 | 
            }, 
 | 
            touchstart(e) { 
 | 
                this.scrollingColumnIndex = e.target.dataset.column 
 | 
                this.startScrollTop = e.changedTouches[0].pageY 
 | 
                this.startPickedIndex = this.columns[this.scrollingColumnIndex].pickedIndex 
 | 
  
 | 
                this.columns[this.scrollingColumnIndex].scrollEventQueue = [{ 
 | 
                    index: this.startPickedIndex, 
 | 
                    time: +new Date() 
 | 
                }] 
 | 
            }, 
 | 
            touchmove(e) { 
 | 
                let scrollDistance = this.startScrollTop - e.changedTouches[0].pageY 
 | 
                let scrollIndex = Math.round(scrollDistance/this.pickerItemHeight) 
 | 
                let column = this.columns[this.scrollingColumnIndex] 
 | 
                let currentPickedIndex = column.pickedIndex 
 | 
                this.setColumnIndex(column, this.startPickedIndex + scrollIndex) 
 | 
                if (column.pickedIndex !== currentPickedIndex) { 
 | 
                    this.scrollColumn(column, true) 
 | 
                } 
 | 
            }, 
 | 
            touchend(e) { 
 | 
                let column = this.columns[this.scrollingColumnIndex] 
 | 
                this.scrollColumn(column, false, true) 
 | 
            }, 
 | 
            setColumn(columnIndex, pickerList) { 
 | 
                if (columnIndex === 5 || (this.columnNum > 0 && columnIndex >= this.columnNum)) { 
 | 
                    // limit max 5 columns 
 | 
                    return 
 | 
                } 
 | 
                let columnPickerList = pickerList || [] 
 | 
                if (this.beforeSetColumn) { 
 | 
                    // 在开始渲染列之前使用钩子动态修改pickerList,注意避免对pickerList修改以保证渲染不污染源数据 
 | 
                    columnPickerList = this.beforeSetColumn(columnIndex, columnPickerList) 
 | 
                } 
 | 
                if (columnPickerList.length < 1) { 
 | 
                    if (this.columnNum === 0) { 
 | 
                        // 动态列数,当前列为空,清除后面全部列 
 | 
                        this.columns = this.columns.filter(column => { 
 | 
                            return column.index < columnIndex 
 | 
                        }) 
 | 
                        return 
 | 
                    } else if (columnIndex < this.columnNum) { 
 | 
                        // 固定列数,清除下一列,递归清除后面全部列 
 | 
                        this.setColumn(columnIndex + 1, []) 
 | 
                    } else { 
 | 
                        return 
 | 
                    } 
 | 
                } 
 | 
  
 | 
                let currentColumn = this.columns[columnIndex] || {} 
 | 
                let column = { 
 | 
                    index: columnIndex, 
 | 
                    scrollEventQueue: [], 
 | 
                    pickerList: columnPickerList, 
 | 
                    pickedIndex: 0, 
 | 
                    style: { 
 | 
                        "transition-property": "transform", 
 | 
                        "transition-duration": "200", 
 | 
                        "transform": "translateY(0)" 
 | 
                    } 
 | 
                } 
 | 
                this.setColumnIndex(column, currentColumn.pickedIndex || 0) // 使得column的index维持在当前选择位置 
 | 
                let defaultValue = this.defaultValue && this.defaultValue[columnIndex] !== false ? this.defaultValue[columnIndex] : false 
 | 
                if (currentColumn.pickedIndex === undefined && defaultValue !== false) { 
 | 
                    column.pickerList.map((pickerItem, index) => { 
 | 
                        if (pickerItem[this.pickerKey.value] == defaultValue) { 
 | 
                            column.pickedIndex = index 
 | 
                        } 
 | 
                    }) 
 | 
                } 
 | 
  
 | 
                this.scrollColumn(column) 
 | 
                this.$set(this.columns, columnIndex, column) 
 | 
            }, 
 | 
            setColumnIndex(column, index) { 
 | 
                index = index < 0 ? 0 : index 
 | 
                column.pickedIndex = Math.min(index, column.pickerList.length - 1) 
 | 
            }, 
 | 
            scrollColumn(column, needThrottle = false, needSpeedUp = false) { 
 | 
                let now = +new Date() 
 | 
                let lastScrollEvent = column.scrollEventQueue[column.scrollEventQueue.length-1] 
 | 
                if (needThrottle && lastScrollEvent.time && now < (lastScrollEvent.time + 100)) { 
 | 
                    return 
 | 
                } 
 | 
                let speedUpIndex = 0 
 | 
                if (needSpeedUp && this.speedUpRatio) { 
 | 
                    // 模拟惯性效果,在touch事件接触后,根据最后两次滚动事件的速度生成滑动的距离。在touch过程中,保持触摸距离和滚动距离的一致 
 | 
                    if (column.scrollEventQueue.length > 1) { 
 | 
                        lastScrollEvent = column.scrollEventQueue[column.scrollEventQueue.length-2] 
 | 
                    } 
 | 
                    let speed = (column.pickedIndex - lastScrollEvent.index) / (now - lastScrollEvent.time) 
 | 
                    speedUpIndex = Math.floor(Math.pow(speed, 2) * 800 * this.speedUpRatio) // 使用二次方曲线放大加速效果,其中效果800为默认调试参数 
 | 
                    speedUpIndex = speed > 0 ? speedUpIndex : -speedUpIndex; 
 | 
                    this.setColumnIndex(column, column.pickedIndex + speedUpIndex) 
 | 
                } 
 | 
  
 | 
                column.scrollEventQueue.push({ 
 | 
                    index: column.pickedIndex, 
 | 
                    time: now 
 | 
                }) 
 | 
  
 | 
                let translateY = column.pickedIndex * this.pickerItemHeight 
 | 
                column.style = { 
 | 
                    "transition-property": "transform", 
 | 
                    "transition-duration": "200", 
 | 
                    "transform": "translateY(" + -translateY + ")" 
 | 
                } 
 | 
  
 | 
                if (this.reactModel && column.pickerList[column.pickedIndex]) { 
 | 
                    this.setColumn(column.index + 1, column.pickerList[column.pickedIndex][this.pickerKey.children]) 
 | 
                } 
 | 
  
 | 
                this.$emit('change', column.index, this.columnPickedInfo(column)) 
 | 
            } 
 | 
        }, 
 | 
    }; 
 | 
</script> 
 | 
  
 | 
<style lang="scss" scoped> 
 | 
    .picker-mask { 
 | 
        position: fixed; 
 | 
        top: 0; 
 | 
        left: 0; 
 | 
        right: 0; 
 | 
        bottom: 0; 
 | 
        z-index: 999; 
 | 
        width: 750rpx; 
 | 
        background-color: rgba(0, 0, 0, .6); 
 | 
    } 
 | 
  
 | 
    .picker-panel { 
 | 
        position: fixed; 
 | 
        bottom: 0; 
 | 
        left: 0; 
 | 
        width: 750rpx; 
 | 
        background-color: #fff; 
 | 
        transform: translate(0, 100%); 
 | 
        transition: transform .3s; 
 | 
        flex-direction: column; 
 | 
    } 
 | 
    .picker-action { 
 | 
        width: 750rpx; 
 | 
        height: 96rpx; 
 | 
        position: relative; 
 | 
        justify-content: space-between; 
 | 
    } 
 | 
    .confirm, .cancel { 
 | 
        padding: 30rpx; 
 | 
        font-size: 36rpx; 
 | 
    } 
 | 
    .confirm { 
 | 
        color: #007aff; 
 | 
    } 
 | 
  
 | 
    .picker-content { 
 | 
        width: 750rpx; 
 | 
        height: 476rpx; 
 | 
        overflow: hidden; 
 | 
        position: relative; 
 | 
    } 
 | 
    .picker-column { 
 | 
        flex: 1; 
 | 
        font-size: 32rpx; 
 | 
        overflow: hidden; 
 | 
        flex-direction: column; 
 | 
    } 
 | 
  
 | 
    .scroll-wrapper { 
 | 
        position: relative; 
 | 
        height: 476rpx; 
 | 
        flex-direction: column; 
 | 
    } 
 | 
    .top-cover, .bottom-cover { 
 | 
        width: 750rpx; 
 | 
        height: 204rpx; 
 | 
        position: absolute; 
 | 
  
 | 
        transform: translateZ(0); 
 | 
        background-image: linear-gradient(to top,rgba(245, 245, 245, .2),rgba(245, 245, 245, .9)); 
 | 
    } 
 | 
    .top-cover { 
 | 
        top: 0; 
 | 
    } 
 | 
    .bottom-cover { 
 | 
        bottom: 0; 
 | 
        background-image: linear-gradient(to bottom,rgba(245, 245, 245,.2),rgba(245, 245, 245, .9)); 
 | 
    } 
 | 
    .top-cover-border, .bottom-cover-border { 
 | 
        position: absolute; 
 | 
        width: 750rpx; 
 | 
        height: 1px; 
 | 
        border-color: #ccc; 
 | 
        border-style: solid; 
 | 
    } 
 | 
    .top-cover-border { 
 | 
        top: 204rpx; 
 | 
        border-bottom-width: .5px; 
 | 
    } 
 | 
    .bottom-cover-border { 
 | 
        bottom: 204rpx; 
 | 
        border-top-width: .5px; 
 | 
    } 
 | 
    .scroll-list { 
 | 
        padding: 204rpx 0; 
 | 
        flex-direction: column; 
 | 
        overflow: hidden; 
 | 
    } 
 | 
    .picker-item { 
 | 
        justify-content: center; 
 | 
        align-items: center; 
 | 
        height: 68rpx; 
 | 
    } 
 | 
    .picker-item-text { 
 | 
        flex: 1; 
 | 
        lines: 1; 
 | 
        text-overflow: ellipsis; 
 | 
        /*无法给text设定宽度,此处无效 */ 
 | 
        padding: 10rpx; 
 | 
        text-align: center; 
 | 
        font-size: 32rpx; 
 | 
        color: #333; 
 | 
  
 | 
    } 
 | 
</style> 
 |