diff --git a/src/slider/README.md b/src/slider/README.md index 5ad958b29..c95deda09 100644 --- a/src/slider/README.md +++ b/src/slider/README.md @@ -35,6 +35,32 @@ export default { }; ``` +### Dual thumb + +Add `range` attribute to open dual thumb mode + +```html + +``` + +```js +import { Toast } from 'vant'; + +export default { + data() { + return { + // value must be an Array + value: [10, 50], + }; + }, + methods: { + onChange(value) { + Toast('current value:' + value); + }, + }, +}; +``` + ### Range ```html @@ -64,9 +90,7 @@ export default { ```html @@ -91,15 +115,44 @@ export default { ``` +### Vertical, Dual thumb mode + +Add `range` and `vertical` attributes at the same time, and make sure that the value of `value` is an array + +```html +
+ +
+``` + +```js +import { Toast } from 'vant'; + +export default { + data() { + return { + // value must be an array + value: [10, 50], + }; + }, + methods: { + onChange(value) { + Toast('Current value:' + value); + }, + }, +}; +``` + ## API ### Props | Attribute | Description | Type | Default | | --- | --- | --- | --- | -| value | Current value | _number_ | `0` | +| value | Current value | _number \| array_ | `0` | | max | Max value | _number \| string_ | `100` | | min | Min value | _number \| string_ | `0` | +| range | Dual thumb mode | _boolean_ | `false` | | step | Step size | _number \| string_ | `1` | | bar-height | Height of bar | _number \| string_ | `2px` | | button-size `v2.4.5` | Button size | _number \| string_ | `24px` | diff --git a/src/slider/README.zh-CN.md b/src/slider/README.zh-CN.md index 421c226eb..4eb86f6da 100644 --- a/src/slider/README.zh-CN.md +++ b/src/slider/README.zh-CN.md @@ -35,6 +35,32 @@ export default { }; ``` +### 双滑块 + +添加`range`属性就可以开启双滑块模式,确保`value`的值是一个数组 + +```html + +``` + +```js +import { Toast } from 'vant'; + +export default { + data() { + return { + // 双滑块模式时,值必须是数组 + value: [10, 50], + }; + }, + methods: { + onChange(value) { + Toast('当前值:' + value); + }, + }, +}; +``` + ### 指定选择范围 ```html @@ -64,9 +90,7 @@ export default { ```html @@ -88,20 +112,49 @@ export default { Slider 垂直展示时,高度为 100% 父元素高度 ```html -
+
``` +### 垂直方向,双滑块 + +同时添加`range`和`vertical`属性,并确保`value`的值是一个数组 + +```html +
+ +
+``` + +```js +import { Toast } from 'vant'; + +export default { + data() { + return { + // 双滑块模式时,值必须是数组 + value: [10, 50], + }; + }, + methods: { + onChange(value) { + Toast('当前值:' + value); + }, + }, +}; +``` + ## API ### Props | 参数 | 说明 | 类型 | 默认值 | | --- | --- | --- | --- | -| value | 当前进度百分比 | _number_ | `0` | +| value | 当前进度百分比 | _number \| array_ | `0` | | max | 最大值 | _number \| string_ | `100` | | min | 最小值 | _number \| string_ | `0` | +| range | 双滑块模式 | _boolean_ | `false` | | step | 步长 | _number \| string_ | `1` | | bar-height | 进度条高度,默认单位为`px` | _number \| string_ | `2px` | | button-size `v2.4.5` | 滑块按钮大小,默认单位为`px` | _number \| string_ | `24px` | diff --git a/src/slider/demo/index.vue b/src/slider/demo/index.vue index a510eefc6..eee0e9317 100644 --- a/src/slider/demo/index.vue +++ b/src/slider/demo/index.vue @@ -5,20 +5,24 @@ - + - + - + + + + + - +
- + +
+
+ + +
+
@@ -46,35 +56,41 @@ export default { i18n: { 'zh-CN': { title1: '基础用法', - title2: '指定选择范围', - title3: '禁用', - title4: '指定步长', + title2: '双滑块', + title3: '指定选择范围', + title4: '禁用', + title5: '指定步长', customStyle: '自定义样式', customButton: '自定义按钮', text: '当前值:', vertical: '垂直方向', + vertical2: '垂直方向,双滑块', }, 'en-US': { title1: 'Basic Usage', - title2: 'Range', - title3: 'Disabled', - title4: 'Step size', + title2: 'Dual thumb mode', + title3: 'Range', + title4: 'Disabled', + title5: 'Step size', customStyle: 'Custom Style', customButton: 'Custom Button', text: 'Current value: ', vertical: 'Vertical', + vertical2: 'Vertical, Dual thumb mode', }, }, data() { return { value1: 50, - value2: 0, - value3: 50, + value2: [20, 60], + value3: 0, value4: 50, value5: 50, value6: 50, value7: 50, + value8: 50, + value9: [20, 60], }; }, diff --git a/src/slider/index.js b/src/slider/index.js index 0c9a27888..3a8221804 100644 --- a/src/slider/index.js +++ b/src/slider/index.js @@ -2,6 +2,7 @@ import { ref, computed } from 'vue'; // Utils import { createNamespace, addUnit, getSizeStyle } from '../utils'; +import { deepClone } from '../utils/deep-clone'; import { preventDefault } from '../utils/dom/event'; // Composition @@ -15,6 +16,7 @@ export default createComponent({ props: { disabled: Boolean, vertical: Boolean, + range: Boolean, barHeight: [Number, String], buttonSize: [Number, String], activeColor: String, @@ -32,7 +34,7 @@ export default createComponent({ default: 1, }, modelValue: { - type: Number, + type: [Number, Array], default: 0, }, }, @@ -42,12 +44,13 @@ export default createComponent({ setup(props, { emit, slots }) { let startValue; let currentValue; + let index; const rootRef = ref(); const dragStatus = ref(); const touch = useTouch(); - const range = computed(() => props.max - props.min); + const scope = computed(() => props.max - props.min); const wrapperStyle = computed(() => { const crossAxis = props.vertical ? 'width' : 'height'; @@ -57,10 +60,30 @@ export default createComponent({ }; }); + // 计算选中条的长度百分比 + const calcMainAxis = () => { + const { modelValue, min, range } = props; + if (range) { + return `${((modelValue[1] - modelValue[0]) * 100) / scope.value}%`; + } + return `${((modelValue - min) * 100) / scope.value}%`; + }; + + // 计算选中条的开始位置的偏移量 + const calcOffset = () => { + const { modelValue, min, range } = props; + if (range) { + return `${((modelValue[0] - min) * 100) / scope.value}%`; + } + return `0%`; + }; + const barStyle = computed(() => { const mainAxis = props.vertical ? 'height' : 'width'; return { - [mainAxis]: `${((props.modelValue - props.min) * 100) / range.value}%`, + [mainAxis]: calcMainAxis(), + left: props.vertical ? null : calcOffset(), + top: props.vertical ? calcOffset() : null, background: props.activeColor, transition: dragStatus.value ? 'none' : null, }; @@ -72,14 +95,31 @@ export default createComponent({ return Math.round(value / step) * step; }; - const updateValue = (value, end) => { - value = format(value); + const isSameValue = (newValue, oldValue) => { + return JSON.stringify(newValue) === JSON.stringify(oldValue); + }; - if (value !== props.modelValue) { + // 处理两个滑块重叠之后的情况 + const handleOverlap = (value) => { + if (value[0] > value[1]) { + value = deepClone(value); + return value.reverse(); + } + return value; + }; + + const updateValue = (value, end) => { + if (props.range) { + value = handleOverlap(value).map(format); + } else { + value = format(value); + } + + if (!isSameValue(value, props.modelValue)) { emit('update:modelValue', value); } - if (end && value !== startValue) { + if (end && !isSameValue(value, startValue)) { emit('change', value); } }; @@ -91,15 +131,26 @@ export default createComponent({ return; } - const { min, vertical, modelValue } = props; + const { min, vertical, modelValue, range } = props; const rect = useRect(rootRef); const delta = vertical ? event.clientY - rect.top : event.clientX - rect.left; const total = vertical ? rect.height : rect.width; - const value = +min + (delta / total) * range.value; + let value = +min + (delta / total) * scope.value; + + if (range) { + let left = modelValue[0]; + let right = modelValue[1]; + const middle = (left + right) / 2; + if (value <= middle) { + left = value; + } else { + right = value; + } + value = [left, right]; + } - startValue = modelValue; updateValue(value, true); }; @@ -109,7 +160,12 @@ export default createComponent({ } touch.start(event); - startValue = format(props.modelValue); + currentValue = props.modelValue; + if (props.range) { + startValue = props.modelValue.map(format); + } else { + startValue = format(props.modelValue); + } dragStatus.value = 'start'; }; @@ -129,9 +185,13 @@ export default createComponent({ const rect = useRect(rootRef); const delta = props.vertical ? touch.deltaY.value : touch.deltaX.value; const total = props.vertical ? rect.height : rect.width; - const diff = (delta / total) * range.value; + const diff = (delta / total) * scope.value; - currentValue = startValue + diff; + if (props.range) { + currentValue[index] = startValue[index] + diff; + } else { + currentValue = startValue + diff; + } updateValue(currentValue); }; @@ -148,27 +208,44 @@ export default createComponent({ dragStatus.value = ''; }; - const renderButton = () => ( -
- {slots.button ? ( - slots.button() - ) : ( -
- )} -
- ); + const renderButton = (i) => { + const map = ['left', 'right']; + const getClassName = () => { + if (typeof i === 'number') { + return `button-wrapper-${map[i]}`; + } + return `button-wrapper`; + }; + + return ( +
{ + if (typeof i === 'number') { + // 保存当前按钮的索引 + index = i; + } + onTouchStart(e); + }} + onTouchmove={onTouchMove} + onTouchend={onTouchEnd} + onTouchcancel={onTouchEnd} + onClick={(e) => e.stopPropagation()} + > + {slots.button ? ( + slots.button() + ) : ( +
+ )} +
+ ); + }; // format initial value updateValue(props.modelValue); @@ -185,7 +262,7 @@ export default createComponent({ onClick={onClick} >
- {renderButton()} + {props.range ? [renderButton(0), renderButton(1)] : renderButton()}
); diff --git a/src/slider/index.less b/src/slider/index.less index 28d51adc8..881b5eb05 100644 --- a/src/slider/index.less +++ b/src/slider/index.less @@ -24,7 +24,7 @@ height: 100%; background-color: @slider-active-background-color; border-radius: inherit; - transition: width @animation-duration-fast, height @animation-duration-fast; + transition: all @animation-duration-fast; } &__button { @@ -34,20 +34,31 @@ border-radius: @slider-button-border-radius; box-shadow: @slider-button-box-shadow; - &-wrapper { + &-wrapper, + &-wrapper-right { position: absolute; top: 50%; right: 0; transform: translate3d(50%, -50%, 0); cursor: grab; } + + &-wrapper-left { + position: absolute; + top: 50%; + left: 0; + transform: translate3d(-50%, -50%, 0); + cursor: grab; + } } &--disabled { cursor: not-allowed; opacity: @slider-disabled-opacity; - .van-slider__button-wrapper { + .van-slider__button-wrapper, + .van-slider__button-wrapper-left, + .van-slider__button-wrapper-right { cursor: not-allowed; } } @@ -57,13 +68,21 @@ width: @slider-bar-height; height: 100%; - .van-slider__button-wrapper { + .van-slider__button-wrapper, + .van-slider__button-wrapper-right { top: auto; right: 50%; bottom: 0; transform: translate3d(50%, 50%, 0); } + .van-slider__button-wrapper-left { + top: 0; + right: 50%; + left: auto; + transform: translate3d(50%, -50%, 0); + } + // use pseudo element to expand click area &::before { top: 0;