# wheel
# 介绍
wheel 插件,是实现类似 IOS Picker 组件的基石。
# 安装
npm install @better-scroll/wheel --save
// or
yarn add @better-scroll/wheel
# 使用
首先引入 wheel 插件,并通过静态方法 BScroll.use()
注册插件。
import BScroll from '@better-scroll/core'
import Wheel from '@better-scroll/wheel'
BScroll.use(Wheel)
接着在 options
传入正确的配置
let bs = new BScroll('.bs-wrapper', {
wheel: true // wheel options 为 true
})
TIP
wheel options 是 true 或者对象,否则插件功能失效,具体请参考 wheel options。
危险
BetterScroll 结合 wheel 插件只是实现 Picker 效果的 JS 逻辑部分,还有 DOM 模版是需要用户去实现,所幸,对于大多数的 Picker 场景,我们给出了相对应的示例。
基本使用
<template> <div class="container"> <ul class="example-list"> <li class="example-item" @click="show"> <span class="open">{{selectedText}}</span> </li> </ul> <transition name="picker-fade"> <div class="picker" v-show="state===1" @touchmove.prevent @click="_cancel"> <transition name="picker-move"> <div class="picker-panel" v-show="state===1" @click.stop> <div class="picker-choose border-bottom-1px"> <span class="cancel" @click="_cancel">Cancel</span> <span class="confirm" @click="_confirm">Confirm</span> <h1 class="picker-title">Title</h1> </div> <div class="picker-content"> <div class="mask-top border-bottom-1px"></div> <div class="mask-bottom border-top-1px"></div> <div class="wheel-wrapper" ref="wheelWrapper"> <div class="wheel"> <ul class="wheel-scroll"> <li v-for="(item, index) in pickerData" :key="index" :class="{'wheel-disabled-item':item.disabled}" class="wheel-item">{{item.text}}</li> </ul> </div> </div> </div> <div class="picker-footer"></div> </div> </transition> </div> </transition> </div> </template>
<script type="text/ecmascript-6"> import BScroll from '@better-scroll/core' import Wheel from '@better-scroll/wheel' BScroll.use(Wheel) const STATE_HIDE = 0 const STATE_SHOW = 1 const COMPONENT_NAME = 'picker' const EVENT_SELECT = 'select' const EVENT_CANCEL = 'cancel' const EVENT_CHANGE = 'change' const DATA = [ { text: 'Venomancer', value: 1, disabled: 'wheel-disabled-item' }, { text: 'Nerubian Weaver', value: 2 }, { text: 'Spectre', value: 3 }, { text: 'Juggernaut', value: 4 }, { text: 'Karl', value: 5 }, { text: 'Zeus', value: 6 }, { text: 'Witch Doctor', value: 7 }, { text: 'Lich', value: 8 }, { text: 'Oracle', value: 9 }, { text: 'Earthshaker', value: 10 } ] export default { name: COMPONENT_NAME, data() { return { state: STATE_HIDE, selectedIndex: 2, selectedText: 'open', pickerData: DATA } }, methods: { _confirm() { if (this._isMoving()) { return } this.hide() const currentSelectedIndex = this.wheel.getSelectedIndex() this.selectedIndex = currentSelectedIndex this.selectedText = this.pickerData[this.selectedIndex].text this.$emit(EVENT_SELECT, currentSelectedIndex) }, _cancel() { this.hide() this.$emit(EVENT_CANCEL) }, _isMoving() { return this.wheel.pending }, show() { if (this.state === STATE_SHOW) { return } this.state = STATE_SHOW if (!this.wheel) { // waiting for DOM rendered this.$nextTick(() => { const wrapper = this.$refs.wheelWrapper.children[0] this._createWheel(wrapper) }) } else { this.wheel.enable() this.wheel.wheelTo(this.selectedIndex) } }, hide() { this.state = STATE_HIDE // if wheel is in animation, clear timer in it this.wheel.disable() }, refresh() { this.$nextTick(() => { this.wheel.refresh() }) }, _createWheel(wheelWrapper) { if (!this.wheel) { this.wheel = new BScroll(wheelWrapper, { wheel: { selectedIndex: this.selectedIndex, wheelWrapperClass: 'wheel-scroll', wheelItemClass: 'wheel-item', wheelDisabledItemClass: 'wheel-disabled-item' }, useTransition: false, probeType: 2 }) this.wheel.on('scrollEnd', () => { this.$emit(EVENT_CHANGE, this.wheel.getSelectedIndex()) }) } else { this.wheel.refresh() } return this.wheel } } } </script>
<style scoped lang="stylus" rel="stylesheet/stylus"> /* reset */ ul list-style none padding 0 .example-list display: flex justify-content: space-between flex-wrap: wrap margin: 2rem .example-item background-color white padding: 0.8rem border: 1px solid rgba(0, 0, 0, .1) box-shadow: 0 1px 2px 0 rgba(0,0,0,0.1) text-align: center margin-bottom: 1rem flex: 1 &.placeholder visibility: hidden height: 0 margin: 0 padding: 0 .picker position: fixed left: 0 top: 0 z-index: 100 width: 100% height: 100% overflow: hidden text-align: center font-size: 14px background-color: rgba(37, 38, 45, .4) &.picker-fade-enter, &.picker-fade-leave-active opacity: 0 &.picker-fade-enter-active, &.picker-fade-leave-active transition: all .3s ease-in-out .picker-panel position: absolute z-index: 600 bottom: 0 width: 100% height: 273px background: white &.picker-move-enter, &.picker-move-leave-active transform: translate3d(0, 273px, 0) &.picker-move-enter-active, &.picker-move-leave-active transition: all .3s ease-in-out .picker-choose position: relative height: 60px color: #999 .picker-title margin: 0 line-height: 60px font-weight: normal text-align: center font-size: 18px color: #333 .confirm, .cancel position: absolute top: 6px padding: 16px font-size: 14px .confirm right: 0 color: #007bff &:active color: #5aaaff .cancel left: 0 &:active color: #c2c2c2 .picker-content position: relative top: 20px .mask-top, .mask-bottom z-index: 10 width: 100% height: 68px pointer-events: none transform: translateZ(0) .mask-top position: absolute top: 0 background: linear-gradient(to top, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0.8)) .mask-bottom position: absolute bottom: 1px background: linear-gradient(to bottom, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0.8)) .wheel-wrapper display: flex padding: 0 16px .wheel -ms-flex: 1 1 0.000000001px -webkit-box-flex: 1 -webkit-flex: 1 flex: 1 -webkit-flex-basis: 0.000000001px flex-basis: 0.000000001px width: 1% height: 173px overflow: hidden font-size: 18px .wheel-scroll padding: 0 margin-top: 68px line-height: 36px list-style: none .wheel-item list-style: none height: 36px overflow: hidden white-space: nowrap color: #333 &.wheel-disabled-item opacity: .2; .picker-footer height: 20px </style>
单列 Picker 是一个比较常见的效果。你可以通过
selectedIndex
来配置初始化时选中对应索引的 item,wheelDisabledItemClass
配置想要禁用的 item 项来模拟 Web Select 标签 disable 的效果。多项选择器
<template> <div class="container"> <ul class="example-list"> <li class="example-item" @click="show"> <span class="open">{{selectedText}}</span> </li> </ul> <transition name="picker-fade"> <div class="picker" v-show="state===1" @touchmove.prevent @click="_cancel"> <transition name="picker-move"> <div class="picker-panel" v-show="state===1" @click.stop> <div class="picker-choose border-bottom-1px"> <span class="cancel" @click="_cancel">Cancel</span> <span class="confirm" @click="_confirm">Confirm</span> <h1 class="picker-title">Title</h1> </div> <div class="picker-content"> <div class="mask-top border-bottom-1px"></div> <div class="mask-bottom border-top-1px"></div> <div class="wheel-wrapper" ref="wheelWrapper"> <div class="wheel" v-for="(data, index) in pickerData" :key="index"> <ul class="wheel-scroll"> <li v-for="item in data" :key="item.value" class="wheel-item">{{item.text}}</li> </ul> </div> </div> </div> <div class="picker-footer"></div> </div> </transition> </div> </transition> </div> </template>
<script type="text/ecmascript-6"> import BScroll from '@better-scroll/core' import Wheel from '@better-scroll/wheel' BScroll.use(Wheel) const STATE_HIDE = 0 const STATE_SHOW = 1 const COMPONENT_NAME = 'picker' const EVENT_SELECT = 'select' const EVENT_CANCEL = 'cancel' const EVENT_CHANGE = 'change' const DATA1 = [ { text: 'Venomancer', value: 1 }, { text: 'Nerubian Weaver', value: 2 }, { text: 'Spectre', value: 3 }, { text: 'Juggernaut', value: 4 }, { text: 'Karl', value: 5 }, { text: 'Zeus', value: 6 }, { text: 'Witch Doctor', value: 7 }, { text: 'Lich', value: 8 }, { text: 'Oracle', value: 9 }, { text: 'Earthshaker', value: 10 } ] const DATA2 = [ { text: 'Durable', value: 'a' }, { text: 'Pusher', value: 'b' }, { text: 'Carry', value: 'c' }, { text: 'Nuker', value: 'd' }, { text: 'Support', value: 'e' }, { text: 'Jungle', value: 'f' }, { text: 'Escape', value: 'g' }, { text: 'Initiator', value: 'h' } ] export default { name: COMPONENT_NAME, data() { return { state: STATE_HIDE, selectedIndex: [0, 0], selectedText: 'open', pickerData: [DATA1, DATA2] } }, methods: { _confirm() { if (this._isMoving()) { return } this.hide() const currentSelectedIndex = this.selectedIndex = this.wheels.map(wheel => { return wheel.getSelectedIndex() }) // store array for preventing multi-collecting array dependencies in Vue source code const pickerData = this.pickerData const currentSelectedValue = this.selectedText = pickerData.map((data, index) => { return data[currentSelectedIndex[index]].text }).join('-') this.$emit(EVENT_SELECT, currentSelectedIndex, currentSelectedValue) }, _cancel() { this.hide() this.$emit(EVENT_CANCEL) }, _isMoving() { return this.wheels.some((wheel) => { return wheel.pending }) }, show() { if (this.state === STATE_SHOW) { return } this.state = STATE_SHOW if (!this.wheels) { // waiting for DOM rendered this.$nextTick(() => { this.wheels = [] let wheelWrapper = this.$refs.wheelWrapper for (let i = 0; i < this.pickerData.length; i++) { this._createWheel(wheelWrapper, i) } }) } else { for (let i = 0; i < this.pickerData.length; i++) { this.wheels[i].enable() this.wheels[i].wheelTo(this.selectedIndex[i]) } } }, hide() { this.state = STATE_HIDE for (let i = 0; i < this.pickerData.length; i++) { // if wheel is in animation, clear timer in it this.wheels[i].disable() } }, refresh() { this.$nextTick(() => { this.wheels.forEach((wheel, index) => { wheel.refresh() }) }) }, _createWheel(wheelWrapper, i) { if (!this.wheels[i]) { this.wheels[i] = new BScroll(wheelWrapper.children[i], { wheel: { selectedIndex: this.selectedIndex[i], wheelWrapperClass: 'wheel-scroll', wheelItemClass: 'wheel-item' }, probeType: 3 }) this.wheels[i].on('scrollEnd', () => { this.$emit(EVENT_CHANGE, i, this.wheels[i].getSelectedIndex()) }) } else { this.wheels[i].refresh() } return this.wheels[i] } } } </script>
<style scoped lang="stylus" rel="stylesheet/stylus"> /* reset */ ul list-style none padding 0 .example-list display: flex justify-content: space-between flex-wrap: wrap margin: 2rem .example-item background-color white padding: 0.8rem border: 1px solid rgba(0, 0, 0, .1) box-shadow: 0 1px 2px 0 rgba(0,0,0,0.1) text-align: center margin-bottom: 1rem flex: 1 &.placeholder visibility: hidden height: 0 margin: 0 padding: 0 .picker position: fixed left: 0 top: 0 z-index: 100 width: 100% height: 100% overflow: hidden text-align: center font-size: 14px background-color: rgba(37, 38, 45, .4) &.picker-fade-enter, &.picker-fade-leave-active opacity: 0 &.picker-fade-enter-active, &.picker-fade-leave-active transition: all .3s ease-in-out .picker-panel position: absolute z-index: 600 bottom: 0 width: 100% height: 273px background: white &.picker-move-enter, &.picker-move-leave-active transform: translate3d(0, 273px, 0) &.picker-move-enter-active, &.picker-move-leave-active transition: all .3s ease-in-out .picker-choose position: relative height: 60px color: #999 .picker-title margin: 0 line-height: 60px font-weight: normal text-align: center font-size: 18px color: #333 .confirm, .cancel position: absolute top: 6px padding: 16px font-size: 14px .confirm right: 0 color: #007bff &:active color: #5aaaff .cancel left: 0 &:active color: #c2c2c2 .picker-content position: relative top: 20px .mask-top, .mask-bottom z-index: 10 width: 100% height: 68px pointer-events: none transform: translateZ(0) .mask-top position: absolute top: 0 background: linear-gradient(to top, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0.8)) .mask-bottom position: absolute bottom: 1px background: linear-gradient(to bottom, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0.8)) .wheel-wrapper display: flex padding: 0 16px .wheel -ms-flex: 1 1 0.000000001px -webkit-box-flex: 1 -webkit-flex: 1 flex: 1 -webkit-flex-basis: 0.000000001px flex-basis: 0.000000001px width: 1% height: 173px overflow: hidden font-size: 18px .wheel-scroll padding: 0 margin-top: 68px line-height: 36px list-style: none .wheel-item list-style: none height: 36px overflow: hidden white-space: nowrap color: #333 &.wheel-disabled-item opacity: .2; .picker-footer height: 20px </style>
示例是一个两列的选择器,JS 逻辑部分与单列选择器没有多大的区别,你会发现这个两列选择器之间是没有任何关联,因为它们是两个不同的 BetterScroll 实例。如果你想要实现省市联动的效果,那么得加上一部分代码,让这两个 BetterScroll 实例能够关联起来。请看下一个例子:
城市联动选择器
<template> <div class="container"> <ul class="example-list"> <li class="example-item" @click="show"> <span class="open">{{selectedText}}</span> </li> </ul> <transition name="picker-fade"> <div class="picker" v-show="state===1" @touchmove.prevent @click="_cancel"> <transition name="picker-move"> <div class="picker-panel" v-show="state===1" @click.stop> <div class="picker-choose border-bottom-1px"> <span class="cancel" @click="_cancel">Cancel</span> <span class="confirm" @click="_confirm">Confirm</span> <h1 class="picker-title">Title</h1> </div> <div class="picker-content"> <div class="mask-top border-bottom-1px"></div> <div class="mask-bottom border-top-1px"></div> <div class="wheel-wrapper" ref="wheelWrapper"> <div class="wheel" v-for="(data, index) in pickerData" :key="index"> <ul class="wheel-scroll"> <li v-for="item in data" :key="item.value" :class="{'wheel-disabled-item':item.disabled}" class="wheel-item">{{item.text}}</li> </ul> </div> </div> </div> <div class="picker-footer"></div> </div> </transition> </div> </transition> </div> </template>
<script type="text/ecmascript-6"> import BScroll from '@better-scroll/core' import Wheel from '@better-scroll/wheel' BScroll.use(Wheel) const STATE_HIDE = 0 const STATE_SHOW = 1 const COMPONENT_NAME = 'picker' const EVENT_SELECT = 'select' const EVENT_CANCEL = 'cancel' const EVENT_CHANGE = 'change' const DATA = [ { text: '北京市', value: '110000', children: [ { text: "北京市", value: '110100' } ] }, { text: '天津市', value: '120000', children: [ { text: "天津市", value: '120000' } ] }, { text: '河北省', value: '130000', children: [ { text: '石家庄市', value: '130100' }, { text: '唐山市', value: '130200' }, { text: '秦皇岛市', value: '130300' }, { text: '邯郸市', value: '130400' }, { text: '邢台市', value: '130500' }, { text: '保定市', value: '130600' }, { text: '张家口市', value: '130700' }, { text: '承德市', value: '130800' } ] }, { text: '山西省', value: '140000', children: [ { text: '太原市', value: '140100' }, { text: '大同市', value: '140200' }, { text: '阳泉市', value: '140300' }, { text: '长治市', value: '140400' }, { text: '晋城市', value: '140500' }, { text: '朔州市', value: '140600' }, { text: '晋中市', value: '140700' } ] } ] export default { name: COMPONENT_NAME, data() { return { state: STATE_HIDE, selectedIndex: [0, 0], selectedText: 'open', pickerData: [] } }, created () { // generate data // like [[{text: 'province1', value: '1'}, {text: 'province2', value: '2'}], [{text: 'city1', value: '11'}, {text: 'city2', value: '22'}] // pickerData has two array, the first is province collections, second is city collections this._loadPickerData(this.selectedIndex, undefined /* no prevSelectedIndex due to instantiating */) }, methods: { _loadPickerData (newSelectedIndex, oldSelectedIndex) { let provinces let cities // first instantiated if (!oldSelectedIndex) { provinces = DATA.map(({ value, text }) => ({ value, text })) cities = DATA[newSelectedIndex[0]].children this.pickerData = [provinces, cities] } else { // provinces'index changed, refresh cities data if (newSelectedIndex[0] !== oldSelectedIndex[0]) { cities = DATA[newSelectedIndex[0]].children this.pickerData.splice(1, 1, cities) // Since cities data changed // refresh better-scroll to recaculate scrollHeight this.$nextTick(() => { this.wheels[1].refresh() }) } } }, _confirm() { if (this._isMoving()) { return } this.hide() const currentSelectedIndex = this.selectedIndex = this.wheels.map(wheel => { return wheel.getSelectedIndex() }) // store array for preventing multi-collecting array dependencies in Vue Source code const pickerData = this.pickerData const currentSelectedValue = this.selectedText = pickerData.map((data, index) => { return data[currentSelectedIndex[index]].text }).join('-') this.$emit(EVENT_SELECT, currentSelectedIndex, currentSelectedValue) }, _cancel() { this.hide() this.$emit(EVENT_CANCEL) }, _isMoving() { return this.wheels.some((wheel) => { return wheel.pending }) }, show() { if (this.state === STATE_SHOW) { return } this.state = STATE_SHOW if (!this.wheels) { this.$nextTick(() => { this.wheels = [] let wheelWrapper = this.$refs.wheelWrapper for (let i = 0; i < this.pickerData.length; i++) { this._createWheel(wheelWrapper, i) } }) } else { for (let i = 0; i < this.pickerData.length; i++) { this.wheels[i].enable() this.wheels[i].wheelTo(this.selectedIndex[i]) } } }, hide() { this.state = STATE_HIDE for (let i = 0; i < this.pickerData.length; i++) { this.wheels[i].disable() } }, refresh() { this.$nextTick(() => { this.wheels.forEach((wheel, index) => { wheel.refresh() }) }) }, _createWheel(wheelWrapper, i) { const wheels = this.wheels if (!wheels[i]) { wheels[i] = new BScroll(wheelWrapper.children[i], { wheel: { selectedIndex: this.selectedIndex[i], wheelWrapperClass: 'wheel-scroll', wheelItemClass: 'wheel-item' }, probeType: 3 }) // when any of wheels'scrolling ended , you should refresh data let prevSelectedIndex = this.selectedIndex wheels[i].on('scrollEnd', () => { const currentSelectedIndex = wheels.map(wheel => wheel.getSelectedIndex()) this._loadPickerData(currentSelectedIndex, prevSelectedIndex) prevSelectedIndex = currentSelectedIndex this.$emit(EVENT_CHANGE, i, this.wheels[i].getSelectedIndex()) }) } else { this.wheels[i].refresh() } return this.wheels[i] } } } </script>
<style scoped lang="stylus" rel="stylesheet/stylus"> /* reset */ ul list-style none padding 0 .example-list display: flex justify-content: space-between flex-wrap: wrap margin: 2rem .example-item background-color white padding: 0.8rem border: 1px solid rgba(0, 0, 0, .1) box-shadow: 0 1px 2px 0 rgba(0,0,0,0.1) text-align: center margin-bottom: 1rem flex: 1 &.placeholder visibility: hidden height: 0 margin: 0 padding: 0 .picker position: fixed left: 0 top: 0 z-index: 100 width: 100% height: 100% overflow: hidden text-align: center font-size: 14px background-color: rgba(37, 38, 45, .4) &.picker-fade-enter, &.picker-fade-leave-active opacity: 0 &.picker-fade-enter-active, &.picker-fade-leave-active transition: all .3s ease-in-out .picker-panel position: absolute z-index: 600 bottom: 0 width: 100% height: 273px background: white &.picker-move-enter, &.picker-move-leave-active transform: translate3d(0, 273px, 0) &.picker-move-enter-active, &.picker-move-leave-active transition: all .3s ease-in-out .picker-choose position: relative height: 60px color: #999 .picker-title margin: 0 line-height: 60px font-weight: normal text-align: center font-size: 18px color: #333 .confirm, .cancel position: absolute top: 6px padding: 16px font-size: 14px .confirm right: 0 color: #007bff &:active color: #5aaaff .cancel left: 0 &:active color: #c2c2c2 .picker-content position: relative top: 20px .mask-top, .mask-bottom z-index: 10 width: 100% height: 68px pointer-events: none transform: translateZ(0) .mask-top position: absolute top: 0 background: linear-gradient(to top, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0.8)) .mask-bottom position: absolute bottom: 1px background: linear-gradient(to bottom, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0.8)) .wheel-wrapper display: flex padding: 0 16px .wheel -ms-flex: 1 1 0.000000001px -webkit-box-flex: 1 -webkit-flex: 1 flex: 1 -webkit-flex-basis: 0.000000001px flex-basis: 0.000000001px width: 1% height: 173px overflow: hidden font-size: 18px .wheel-scroll padding: 0 margin-top: 68px line-height: 36px list-style: none .wheel-item list-style: none height: 36px overflow: hidden white-space: nowrap color: #333 &.wheel-disabled-item opacity: .2; .picker-footer height: 20px </style>
城市联动 Picker 的效果,必须通过 JS 部分逻辑将不同 BetterScroll 的实例联系起来,不管是省市,还是省市区的联动,亦是如此。
# wheel 选项对象
# selectedIndex
- 类型:
number
- 默认值:
0
实例化 Wheel,默认选中第 selectedIndex 项,索引从 0 开始。
# rotate
- 类型:
number
- 默认值:
25
当滚动 wheel 时,wheel item 的弯曲程度。
# adjustTime
- 类型:
number
- 默认值:
400
(ms)
当点击某一项的时候,滚动过去的动画时长。
# wheelWrapperClass
- 类型:
string
- 默认值:
wheel-scroll
滚动元素的 className,这里的「滚动元素」 指的就是 BetterScroll 的 content 元素。
# wheelItemClass
- 类型:
string
- 默认值:
wheel-item
滚动元素的子元素的样式。
# wheelDisabledItemClass
- 类型:
string
- 默认值:
wheel-disabled-item
滚动元素中想要禁用的子元素,类似于 select
元素中禁用的 option
效果。wheel 插件的内部根据 wheelDisabledItemClass
配置来判断是否将该项指定为 disabled 状态。
# 实例方法
# getSelectedIndex()
- 返回值:当前选中项的 index,下标从 0 开始
获取当前选中项的索引。
# wheelTo(index = 0, time = 0, [ease])
- 参数:
{ number } index
:选项索引{ number } time
:动画时长{ number } ease<可选>
:动画时长。缓动效果配置,参考 ease.ts,默认是bounce
效果
滚动至对应索引的列表项。