封装
<template>
<div class="picker-box">
<div class="list" ref="list">
<ul :style="ulStyle">
<li
v-for="(item, index) in column"
:key="'item' + index"
:class="selectIndex == index ? 'selected' : ''"
>
{{ item.label }}
</li>
</ul>
</div>
</div>
</template>
<script>
import {
getClient,
START_EVENT,
MOVE_EVENT,
END_EVENT,
isPC,
DEFTAULT_ITEM_HEIGHT
} from '@/utils/pickUtils'
const DEFAULT_DURATION = 200
// 惯性滑动思路:
// 在手指离开屏幕时,如果和上一次 move 时的间隔小于 `LIMIT_TIME` 且 move
// 距离大于 `LIMIT_DISTANCE` 时,执行惯性滑动
const LIMIT_TIME = 300
const LIMIT_DISTANCE = 15
const IS_PC = isPC()
export default {
props: {
defaultIndex: {
type: Number,
default: 0
},
column: {
type: Array,
default: () => []
},
itemHeight: {
type: [Number, String],
default: DEFTAULT_ITEM_HEIGHT
},
rowNumber: Number
},
data () {
return {
selectIndex: 0,
ulStyle: {
transform: 'translate3d(0px, 0px, 0px)',
transitionDuration: '0ms',
transitionProperty: 'none',
lineHeight: `${this.itemHeight}px`
}
}
},
computed: {
boxHeight () {
let itemHeight = parseInt(this.itemHeight)
itemHeight = itemHeight || DEFTAULT_ITEM_HEIGHT
return itemHeight * this.getRowNumber
},
getRowNumber () {
if (this.rowNumber < 3) {
return 3
}
return this.rowNumber % 2 === 0 ? this.rowNumber + 1 : this.rowNumber
},
count () {
return this.column.length
},
getRoNumber () {
return Math.floor(this.rowNumber / 2)
}
},
methods: {
init () {
this.setTop(this.defaultIndex)
const halfBox = (this.boxHeight - this.itemHeight) / 2
this.bottom = halfBox + this.itemHeight
this.top = halfBox - this.count * this.itemHeight
},
// 根据index 设置滚动位置
setTop (index = 0) {
const { boxHeight, itemHeight } = this
this.startTop = (boxHeight - itemHeight) / 2 - index * itemHeight + index
this.ulStyle.transform = `translate3d(0px, ${this.startTop}px, 0px)`
this.selectIndex = index
this.change()
},
handleStart (e) {
this.distStartTop = getClient(e).y
this.touchStartTime = Date.now()
// ----
this.startY = getClient(e).y
this.momentumTop = this.startTop
this.ulStyle.transitionDuration = '0ms'
this.ulStyle.transitionProperty = 'none'
if (IS_PC) {
document.addEventListener(MOVE_EVENT, this.handleMove, false)
document.addEventListener(END_EVENT, this.handleEnd, false)
}
},
handleMove (e) {
e.preventDefault()
e.stopPropagation()
this.disY = getClient(e).y - this.startY
this.startY = getClient(e).y
if (this.startTop >= this.bottom) {
this.startTop = this.bottom
} else if (this.startTop <= this.top) {
this.startTop = this.top
} else {
this.startTop += this.disY
}
console.log(this.startTop)
this.ulStyle.transform = `translate3d(0px, ${this.startTop}px, 0px)`
const now = Date.now()
if (now - this.touchStartTime > LIMIT_TIME) {
this.touchStartTime = now
this.momentumTop = this.startTop
}
},
handleEnd () {
if (IS_PC) {
document.removeEventListener(MOVE_EVENT, this.handleMove, false)
document.removeEventListener(END_EVENT, this.handleEnd, false)
}
const distance = this.startTop - this.momentumTop
const duration = Date.now() - this.touchStartTime
const allowMomentum =
duration < LIMIT_TIME && Math.abs(distance) > LIMIT_DISTANCE
if (allowMomentum) {
this.toMove(distance, duration)
} else {
this.setTranfromTop()
}
},
setTranfromTop () {
this.ulStyle.transitionProperty = 'all'
this.ulStyle.transitionDuration = `${DEFAULT_DURATION}ms`
if (this.startTop >= this.bottom - this.itemHeight) {
this.setTop()
} else if (this.startTop <= this.top + this.itemHeight) {
this.setTop(this.count - 1)
} else {
let index = Math.round(this.startTop / this.itemHeight)
this.startTop = index * this.itemHeight
if (this.startTop > this.bottom) {
this.startTop = this.bottom - this.itemHeight
index = -this.getRoNumber
} else if (this.startTop < this.top) {
this.startTop = this.top + this.itemHeight
index = this.count + 1
}
this.ulStyle.transform = `translate3d(0px, ${this.startTop}px, 0px)`
index = this.getRoNumber - index
if (this.selectIndex !== index) {
this.selectIndex = index
this.change()
}
}
},
toMove (distance, duration) {
const speed = Math.abs(distance / duration)
distance = this.startTop + (speed / 0.002) * (distance < 0 ? -1 : 1)
this.ulStyle.transitionProperty = 'all'
this.ulStyle.transitionDuration = '1000ms'
this.setTop(
Math.min(
Math.max(Math.round(-distance / this.itemHeight), 0),
this.count - 1
)
)
},
change () {
this.$emit('change', this.column[this.selectIndex])
},
mousewheel (e) {
e.preventDefault()
e.stopPropagation()
this.ulStyle.transitionDuration = '0ms'
this.ulStyle.transitionProperty = 'none'
const { deltaX, deltaY } = e
if (Math.abs(deltaX) < Math.abs(deltaY)) {
this.startTop = this.startTop - deltaY
const b = this.bottom - this.itemHeight
const t = this.top + this.itemHeight
let shouldMove = true
if (this.startTop > b) {
this.startTop = b
shouldMove = false
} else if (this.startTop < t) {
this.startTop = t
shouldMove = false
}
this.ulStyle.transform = `translate3d(0px, ${this.startTop}px, 0px)`
if (shouldMove) {
clearInterval(this.wheelTimer)
this.wheelTimer = setTimeout(this.setTranfromTop, 100)
}
}
}
},
mounted () {
this.init()
// 监听开始事件
this.$el.addEventListener(START_EVENT, this.handleStart, false)
if (IS_PC) {
this.$el.addEventListener('wheel', this.mousewheel, false)
} else {
this.$el.addEventListener(MOVE_EVENT, this.handleMove, false)
this.$el.addEventListener(END_EVENT, this.handleEnd, false)
}
},
watch: {
column () {
this.init()
},
defaultIndex () {
this.setTop(this.defaultIndex)
}
},
beforeDestroy () {
this.$el.removeEventListener(START_EVENT, this.handleStart, false)
if (IS_PC) {
this.$el.removeEventListener('wheel', this.mousewheel, false)
this.$el.removeEventListener(MOVE_EVENT, this.handleMove, false)
this.$el.removeEventListener(END_EVENT, this.handleEnd, false)
}
}
}
</script>
<style lang="scss" scoped>
.picker-box {
height: 390px;
overflow: hidden;
.list {
margin-top: -130px;
padding-top: 8px;
// margin: 0;
flex: 1;
width: 572px;
background-image: url('../../assets/image/picker-select.png');
background-size: 572px 122px;
background-position: center 192px;
background-repeat: no-repeat;
ul {
margin: 0;
padding: 0;
transition-timing-function: cubic-bezier(0.23, 1, 0.68, 1);
line-height: 100px;
}
li {
color: rgba(248, 248, 248, 0.47);
font-size: 40px;
font-family: Source Han Sans CN;
font-weight: 400;
text-align: center;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
&.selected {
font-size: 50px;
background: linear-gradient(180deg, #ffffff 0%, #009ea2 100%);
background-clip: text;
color: transparent;
}
}
}
}
</style>
使用
<template>
<div class="test">
<div class="pick-content">
<PickerList :column="pickData[0]" :rowNumber="4" @change="pickChange" />
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
// import VuePickers from 'vue-pickers'
// import PickerList from 'vue-pickers/src/list.vue'
import PickerList from '@/components/Picker/index.vue'
export default Vue.extend({
components: {
PickerList
},
data () {
const tdata = []
for (let i = 0; i < 20; i++) {
tdata.push({
label: `第${i}行`,
value: i
})
}
return {
pickerVisible: true,
pickData: [tdata],
result: ''
}
},
methods: {
cancel () {
console.log('cancel')
this.result = 'click cancel result: null'
},
pickChange (e) {
console.log(e)
},
confirm (res) {
this.result = JSON.stringify(res)
console.log(res)
}
}
})
</script>
<style lang="scss" scoped>
.test {
width: 1251px;
padding-top: 0px;
display: flex;
justify-content: center;
.pick-content {
width: 600px;
height: 251px;
}
}
</style>