mirror of
https://gitee.com/vant-contrib/vant.git
synced 2025-05-24 15:39:15 +08:00
feat(slider): add dual thumb mode for slider (#7175)
* feat(slider): add dual thumb mode for slider * chore: remove comment * fix: optimize code via CR
This commit is contained in:
parent
0ce48b9239
commit
a745709932
@ -35,6 +35,32 @@ export default {
|
||||
};
|
||||
```
|
||||
|
||||
### Dual thumb
|
||||
|
||||
Add `range` attribute to open dual thumb mode
|
||||
|
||||
```html
|
||||
<van-slider v-model="value" range @change="onChange" />
|
||||
```
|
||||
|
||||
```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
|
||||
<van-slider v-model="value" active-color="#ee0a24">
|
||||
<template #button>
|
||||
<div class="custom-button">
|
||||
{{ value }}
|
||||
</div>
|
||||
<div class="custom-button">{{ value }}</div>
|
||||
</template>
|
||||
</van-slider>
|
||||
|
||||
@ -91,15 +115,44 @@ export default {
|
||||
</div>
|
||||
```
|
||||
|
||||
### 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
|
||||
<div :style="{ height: '120px' }">
|
||||
<van-slider v-model="value" range vertical @change="onChange" />
|
||||
</div>
|
||||
```
|
||||
|
||||
```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` |
|
||||
|
@ -35,6 +35,32 @@ export default {
|
||||
};
|
||||
```
|
||||
|
||||
### 双滑块
|
||||
|
||||
添加`range`属性就可以开启双滑块模式,确保`value`的值是一个数组
|
||||
|
||||
```html
|
||||
<van-slider v-model="value" range @change="onChange" />
|
||||
```
|
||||
|
||||
```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
|
||||
<van-slider v-model="value" active-color="#ee0a24">
|
||||
<template #button>
|
||||
<div class="custom-button">
|
||||
{{ value }}
|
||||
</div>
|
||||
<div class="custom-button">{{ value }}</div>
|
||||
</template>
|
||||
</van-slider>
|
||||
|
||||
@ -88,20 +112,49 @@ export default {
|
||||
Slider 垂直展示时,高度为 100% 父元素高度
|
||||
|
||||
```html
|
||||
<div :style="{ height: '100px' }">
|
||||
<div :style="{ height: '120px' }">
|
||||
<van-slider v-model="value" vertical />
|
||||
</div>
|
||||
```
|
||||
|
||||
### 垂直方向,双滑块
|
||||
|
||||
同时添加`range`和`vertical`属性,并确保`value`的值是一个数组
|
||||
|
||||
```html
|
||||
<div :style="{ height: '120px' }">
|
||||
<van-slider v-model="value" range vertical @change="onChange" />
|
||||
</div>
|
||||
```
|
||||
|
||||
```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` |
|
||||
|
@ -5,20 +5,24 @@
|
||||
</demo-block>
|
||||
|
||||
<demo-block :title="t('title2')">
|
||||
<van-slider v-model="value2" :min="-50" :max="50" @change="onChange" />
|
||||
<van-slider range v-model="value2" @change="onChange" />
|
||||
</demo-block>
|
||||
|
||||
<demo-block :title="t('title3')">
|
||||
<van-slider v-model="value3" disabled />
|
||||
<van-slider v-model="value3" :min="-50" :max="50" @change="onChange" />
|
||||
</demo-block>
|
||||
|
||||
<demo-block :title="t('title4')">
|
||||
<van-slider v-model="value4" :step="10" @change="onChange" />
|
||||
<van-slider v-model="value4" disabled />
|
||||
</demo-block>
|
||||
|
||||
<demo-block :title="t('title5')">
|
||||
<van-slider v-model="value5" :step="10" @change="onChange" />
|
||||
</demo-block>
|
||||
|
||||
<demo-block :title="t('customStyle')">
|
||||
<van-slider
|
||||
v-model="value5"
|
||||
v-model="value6"
|
||||
bar-height="4px"
|
||||
active-color="#ee0a24"
|
||||
@change="onChange"
|
||||
@ -26,16 +30,22 @@
|
||||
</demo-block>
|
||||
|
||||
<demo-block :title="t('customButton')">
|
||||
<van-slider v-model="value6" active-color="#ee0a24">
|
||||
<van-slider v-model="value7" active-color="#ee0a24">
|
||||
<template #button>
|
||||
<div class="custom-button">{{ value6 }}</div>
|
||||
<div class="custom-button">{{ value7 }}</div>
|
||||
</template>
|
||||
</van-slider>
|
||||
</demo-block>
|
||||
|
||||
<demo-block v-if="!isWeapp" :title="t('vertical')">
|
||||
<div :style="{ height: '120px', paddingLeft: '30px' }">
|
||||
<van-slider v-model="value7" vertical @change="onChange" />
|
||||
<van-slider v-model="value8" vertical @change="onChange" />
|
||||
</div>
|
||||
</demo-block>
|
||||
|
||||
<demo-block v-if="!isWeapp" :title="t('vertical2')">
|
||||
<div :style="{ height: '120px', paddingLeft: '30px' }">
|
||||
<van-slider v-model="value9" range vertical @change="onChange" />
|
||||
</div>
|
||||
</demo-block>
|
||||
</demo-section>
|
||||
@ -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],
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -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 = () => (
|
||||
<div
|
||||
role="slider"
|
||||
class={bem('button-wrapper')}
|
||||
tabindex={props.disabled ? -1 : 0}
|
||||
aria-valuemin={props.min}
|
||||
aria-valuenow={props.modelValue}
|
||||
aria-valuemax={props.max}
|
||||
aria-orientation={props.vertical ? 'vertical' : 'horizontal'}
|
||||
onTouchstart={onTouchStart}
|
||||
onTouchmove={onTouchMove}
|
||||
onTouchend={onTouchEnd}
|
||||
onTouchcancel={onTouchEnd}
|
||||
>
|
||||
{slots.button ? (
|
||||
slots.button()
|
||||
) : (
|
||||
<div class={bem('button')} style={getSizeStyle(props.buttonSize)} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
const renderButton = (i) => {
|
||||
const map = ['left', 'right'];
|
||||
const getClassName = () => {
|
||||
if (typeof i === 'number') {
|
||||
return `button-wrapper-${map[i]}`;
|
||||
}
|
||||
return `button-wrapper`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
role="slider"
|
||||
class={bem(getClassName())}
|
||||
tabindex={props.disabled ? -1 : 0}
|
||||
aria-valuemin={props.min}
|
||||
aria-valuenow={props.modelValue}
|
||||
aria-valuemax={props.max}
|
||||
aria-orientation={props.vertical ? 'vertical' : 'horizontal'}
|
||||
onTouchstart={(e) => {
|
||||
if (typeof i === 'number') {
|
||||
// 保存当前按钮的索引
|
||||
index = i;
|
||||
}
|
||||
onTouchStart(e);
|
||||
}}
|
||||
onTouchmove={onTouchMove}
|
||||
onTouchend={onTouchEnd}
|
||||
onTouchcancel={onTouchEnd}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{slots.button ? (
|
||||
slots.button()
|
||||
) : (
|
||||
<div class={bem('button')} style={getSizeStyle(props.buttonSize)} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// format initial value
|
||||
updateValue(props.modelValue);
|
||||
@ -185,7 +262,7 @@ export default createComponent({
|
||||
onClick={onClick}
|
||||
>
|
||||
<div class={bem('bar')} style={barStyle.value}>
|
||||
{renderButton()}
|
||||
{props.range ? [renderButton(0), renderButton(1)] : renderButton()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user