blob: 6d21b92982106d27fd27816b867fdf3a8d132e14 [file] [log] [blame]
guangchao.xu070005a2020-12-07 09:56:40 +08001<template>
2 <view class="u-slider" @tap="onClick" :class="[disabled ? 'u-slider--disabled' : '']" :style="{
3 backgroundColor: inactiveColor
4 }">
5 <view
6 class="u-slider__gap"
7 :style="[
8 barStyle,
9 {
10 height: height + 'rpx',
11 backgroundColor: activeColor
12 }
13 ]"
14 >
15 <view class="u-slider__button-wrap" @touchstart="onTouchStart"
16 @touchmove="onTouchMove" @touchend="onTouchEnd"
17 @touchcancel="onTouchEnd">
18 <slot v-if="$slots.default || $slots.$default"/>
19 <view v-else class="u-slider__button" :style="[blockStyle, {
20 height: blockWidth + 'rpx',
21 width: blockWidth + 'rpx',
22 backgroundColor: blockColor
23 }]"></view>
24 </view>
25 </view>
26 </view>
27</template>
28
29<script>
30/**
31 * slider 滑块选择器
32 * @tutorial https://uviewui.com/components/slider.html
33 * @property {Number | String} value 滑块默认值(默认0)
34 * @property {Number | String} min 最小值(默认0)
35 * @property {Number | String} max 最大值(默认100)
36 * @property {Number | String} step 步长(默认1)
37 * @property {Number | String} blockWidth 滑块宽度,高等于宽(30)
38 * @property {Number | String} height 滑块条高度,单位rpx(默认6)
39 * @property {String} inactiveColor 底部条背景颜色(默认#c0c4cc)
40 * @property {String} activeColor 底部选择部分的背景颜色(默认#2979ff)
41 * @property {String} blockColor 滑块颜色(默认#ffffff)
42 * @property {Object} blockStyle 给滑块自定义样式,对象形式
43 * @property {Boolean} disabled 是否禁用滑块(默认为false)
44 * @event {Function} start 滑动触发
45 * @event {Function} moving 正在滑动中
46 * @event {Function} end 滑动结束
47 * @example <u-slider v-model="value" />
48 */
49export default {
50 name: 'u-slider',
51 props: {
52 // 当前进度百分比值,范围0-100
53 value: {
54 type: [Number, String],
55 default: 0
56 },
57 // 是否禁用滑块
58 disabled: {
59 type: Boolean,
60 default: false
61 },
62 // 滑块宽度,高等于宽,单位rpx
63 blockWidth: {
64 type: [Number, String],
65 default: 30
66 },
67 // 最小值
68 min: {
69 type: [Number, String],
70 default: 0
71 },
72 // 最大值
73 max: {
74 type: [Number, String],
75 default: 100
76 },
77 // 步进值
78 step: {
79 type: [Number, String],
80 default: 1
81 },
82 // 滑块条高度,单位rpx
83 height: {
84 type: [Number, String],
85 default: 6
86 },
87 // 进度条的激活部分颜色
88 activeColor: {
89 type: String,
90 default: '#2979ff'
91 },
92 // 进度条的背景颜色
93 inactiveColor: {
94 type: String,
95 default: '#c0c4cc'
96 },
97 // 滑块的背景颜色
98 blockColor: {
99 type: String,
100 default: '#ffffff'
101 },
102 // 用户对滑块的自定义颜色
103 blockStyle: {
104 type: Object,
105 default() {
106 return {};
107 }
108 },
109 },
110 data() {
111 return {
112 startX: 0,
113 status: 'end',
114 newValue: 0,
115 distanceX: 0,
116 startValue: 0,
117 barStyle: {},
118 sliderRect: {
119 left: 0,
120 width: 0
121 }
122 };
123 },
124 watch: {
125 value(n) {
126 // 只有在非滑动状态时,才可以通过value更新滑块值,这里监听,是为了让用户触发
127 if(this.status == 'end') this.updateValue(this.value, false);
128 }
129 },
130 created() {
131 this.updateValue(this.value, false);
132 },
133 mounted() {
134 // 获取滑块条的尺寸信息
135 this.$uGetRect('.u-slider').then(rect => {
136 this.sliderRect = rect;
137 });
138 },
139 methods: {
140 onTouchStart(event) {
141 if (this.disabled) return;
142 this.startX = 0;
143 // 触摸点集
144 let touches = event.touches[0];
145 // 触摸点到屏幕左边的距离
146 this.startX = touches.clientX;
147 // 此处的this.value虽为props值,但是通过$emit('input')进行了修改
148 this.startValue = this.format(this.value);
149 // 标示当前的状态为开始触摸滑动
150 this.status = 'start';
151 },
152 onTouchMove(event) {
153 if (this.disabled) return;
154 // 连续触摸的过程会一直触发本方法,但只有手指触发且移动了才被认为是拖动了,才发出事件
155 // 触摸后第一次移动已经将status设置为moving状态,故触摸第二次移动不会触发本事件
156 if (this.status == 'start') this.$emit('start');
157 let touches = event.touches[0];
158 // 滑块的左边不一定跟屏幕左边接壤,所以需要减去最外层父元素的左边值
159 this.distanceX = touches.clientX - this.sliderRect.left;
160 // 获得移动距离对整个滑块的百分比值,此为带有多位小数的值,不能用此更新视图
161 // 否则造成通信阻塞,需要每改变一个step值时修改一次视图
162 this.newValue = (this.distanceX / this.sliderRect.width) * 100;
163 this.status = 'moving';
164 // 发出moving事件
165 this.$emit('moving');
166 this.updateValue(this.newValue, true);
167 },
168 onTouchEnd() {
169 if (this.disabled) return;
170 if (this.status === 'moving') {
171 this.updateValue(this.newValue, false);
172 this.$emit('end');
173 }
174 this.status = 'end';
175 },
176 updateValue(value, drag) {
177 // 去掉小数部分,同时也是对step步进的处理
178 const width = this.format(value);
179 // 不允许滑动的值超过max最大值,百分比也不能超过100
180 if(width > this.max || width > 100) return;
181 // 设置移动的百分比值
182 let barStyle = {
183 width: width + '%'
184 };
185 // 移动期间无需过渡动画
186 if (drag == true) {
187 barStyle.transition = 'none';
188 } else {
189 // 非移动期间,删掉对过渡为空的声明,让css中的声明起效
190 delete barStyle.transition;
191 }
192 // 修改value值
193 this.$emit('input', width);
194 this.barStyle = barStyle;
195 },
196 format(value) {
197 // 将小数变成整数,为了减少对视图的更新,造成视图层与逻辑层的阻塞
198 return Math.round(Math.max(this.min, Math.min(value, this.max)) / this.step) * this.step;
199 },
200 onClick(event) {
201 if (this.disabled) return;
202 // 直接点击滑块的情况,计算方式与onTouchMove方法相同
203 const value = ((event.detail.x - this.sliderRect.left) / this.sliderRect.width) * 100;
204 this.updateValue(value, false);
205 }
206 }
207};
208</script>
209
210<style lang="scss" scoped>
211@import "../../libs/css/style.components.scss";
212
213.u-slider {
214 position: relative;
215 border-radius: 999px;
216 border-radius: 999px;
217 background-color: #ebedf0;
218}
219
220.u-slider:before {
221 position: absolute;
222 right: 0;
223 left: 0;
224 content: '';
225 top: -8px;
226 bottom: -8px;
227 z-index: -1;
228}
229
230.u-slider__gap {
231 position: relative;
232 border-radius: inherit;
233 transition: width 0.2s;
234 transition: width 0.2s;
235 background-color: #1989fa;
236}
237
238.u-slider__button {
239 width: 24px;
240 height: 24px;
241 border-radius: 50%;
242 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
243 background-color: #fff;
244 cursor: pointer;
245}
246
247.u-slider__button-wrap {
248 position: absolute;
249 top: 50%;
250 right: 0;
251 transform: translate3d(50%, -50%, 0);
252}
253
254.u-slider--disabled {
255 opacity: 0.5;
256}
257</style>