u-slider.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  1. <template>
  2. <view
  3. class="u-slider"
  4. :style="[addStyle(customStyle), {width: vertical ? addUnit(this.blockSize): ''}]"
  5. >
  6. <template v-if="!useNative || isRange">
  7. <view ref="u-slider-inner" class="u-slider-inner" @click="onClick"
  8. @onTouchStart="onTouchStart2($event, 1)" @touchmove="onTouchMove2($event, 1)"
  9. @touchend="onTouchEnd2($event, 1)" @touchcancel="onTouchEnd2($event, 1)"
  10. :class="[disabled ? 'u-slider--disabled' : '']" :style="innerStyleCpu"
  11. >
  12. <view ref="u-slider__base"
  13. class="u-slider__base"
  14. :style="[
  15. {
  16. width: vertical ? addUnit(sizeLocal) : addUnit(length),
  17. height: vertical ? addUnit(length) : addUnit(sizeLocal),
  18. backgroundColor: inactiveColor
  19. }
  20. ]"
  21. >
  22. </view>
  23. <view
  24. @click="onClick"
  25. class="u-slider__gap"
  26. :style="[
  27. barStyle,
  28. {
  29. backgroundColor: activeColor
  30. }
  31. ]"
  32. >
  33. </view>
  34. <view v-if="isRange"
  35. class="u-slider__gap u-slider__gap-0"
  36. :style="[
  37. barStyle0,
  38. {
  39. backgroundColor: inactiveColor
  40. }
  41. ]"
  42. >
  43. </view>
  44. <text v-if="isRange && showValue"
  45. class="u-slider__show-range-value" :style="{left: (getPx(barStyle0.width) + getPx(blockSize)/2) + 'px'}">
  46. {{ this.rangeValue[0] }}
  47. </text>
  48. <text v-if="isRange && showValue"
  49. class="u-slider__show-range-value" :style="{left: (getPx(barStyle.width) + getPx(blockSize)/2) + 'px'}">
  50. {{ this.rangeValue[1] }}
  51. </text>
  52. <template v-if="isRange">
  53. <view class="u-slider__button-wrap u-slider__button-wrap-0" @touchstart="onTouchStart($event, 0)"
  54. @touchmove="onTouchMove($event, 0)" @touchend="onTouchEnd($event, 0)"
  55. @touchcancel="onTouchEnd($event, 0)" :style="touchButtonStyle(barStyle0)">
  56. <slot name="min" v-if="$slots.min || $slots.$min"/>
  57. <view v-else class="u-slider__button" :style="[blockStyle, {
  58. height: getPx(blockSize, true),
  59. width: getPx(blockSize, true),
  60. backgroundColor: blockColor
  61. }]"></view>
  62. </view>
  63. </template>
  64. <view class="u-slider__button-wrap" @touchstart="onTouchStart"
  65. @touchmove="onTouchMove" @touchend="onTouchEnd"
  66. @touchcancel="onTouchEnd" :style="touchButtonStyle(barStyle)">
  67. <slot name="max" v-if="isRange && ($slots.max || $slots.$max)"/>
  68. <slot v-else-if="$slots.default || $slots.$default"/>
  69. <view v-else class="u-slider__button" :style="[blockStyle, {
  70. height: getPx(blockSize, true),
  71. width: getPx(blockSize, true),
  72. backgroundColor: blockColor
  73. }]"></view>
  74. </view>
  75. </view>
  76. <view class="u-slider__show-value" v-if="showValue && !isRange">
  77. {{ modelValue }}
  78. </view>
  79. </template>
  80. <slider
  81. class="u-slider__native"
  82. v-else
  83. :min="min"
  84. :max="max"
  85. :step="step"
  86. :value="modelValue"
  87. :activeColor="activeColor"
  88. :backgroundColor="inactiveColor"
  89. :blockSize="getPx(blockSize)"
  90. :blockColor="blockColor"
  91. :showValue="showValue"
  92. :disabled="disabled"
  93. @changing="changingHandler"
  94. @change="changeHandler"
  95. ></slider>
  96. </view>
  97. </template>
  98. <script>
  99. import { props } from './props'
  100. import { mpMixin } from '../../libs/mixin/mpMixin'
  101. import { mixin } from '../../libs/mixin/mixin'
  102. import { addUnit, addStyle, getPx, sleep } from '../../libs/function/index.js'
  103. // #ifdef APP-NVUE
  104. const dom = uni.requireNativePlugin('dom')
  105. // #endif
  106. /**
  107. * slider 滑块选择器
  108. * @tutorial https://uview-plus.jiangruyi.com/components/slider.html
  109. * @property {Number | String} value 滑块默认值(默认0)
  110. * @property {Number | String} min 最小值(默认0)
  111. * @property {Number | String} max 最大值(默认100)
  112. * @property {Number | String} step 步长(默认1)
  113. * @property {Number | String} blockWidth 滑块宽度,高等于宽(30)
  114. * @property {Number | String} height 滑块条高度,单位rpx(默认6)
  115. * @property {String} inactiveColor 底部条背景颜色(默认#c0c4cc)
  116. * @property {String} activeColor 底部选择部分的背景颜色(默认#2979ff)
  117. * @property {String} blockColor 滑块颜色(默认#ffffff)
  118. * @property {Object} blockStyle 给滑块自定义样式,对象形式
  119. * @property {Boolean} disabled 是否禁用滑块(默认为false)
  120. * @event {Function} changing 正在滑动中
  121. * @event {Function} change 滑动结束
  122. * @example <up-slider v-model="value" />
  123. */
  124. export default {
  125. name: 'u-slider',
  126. mixins: [mpMixin, mixin, props],
  127. emits: ["start", "changing", "change", "update:modelValue"],
  128. data() {
  129. return {
  130. startX: 0,
  131. startY: 0,
  132. status: 'end',
  133. newValue: 0,
  134. distanceX: 0,
  135. distanceY: 0,
  136. startValue0: 0,
  137. startValue: 0,
  138. barStyle0: {},
  139. barStyle: {},
  140. sliderRect: {
  141. left: 0,
  142. top: 0,
  143. width: 0,
  144. height: 0
  145. },
  146. sizeLocal: '2px'
  147. };
  148. },
  149. watch: {
  150. // #ifdef VUE3
  151. modelValue(n) {
  152. // 只有在非滑动状态时,才可以通过value更新滑块值,这里监听,是为了让用户触发
  153. if (this.status == 'end') {
  154. const $crtFmtValue = this.updateValue(this.modelValue, false);
  155. this.$emit('change', $crtFmtValue);
  156. }
  157. },
  158. // #endif
  159. // #ifdef VUE2
  160. value(n) {
  161. // 只有在非滑动状态时,才可以通过value更新滑块值,这里监听,是为了让用户触发
  162. if (this.status == 'end') {
  163. const $crtFmtValue = this.updateValue(this.value, false);
  164. this.$emit('change', $crtFmtValue);
  165. }
  166. },
  167. // #endif
  168. rangeValue:{
  169. handler(n){
  170. if (this.status == 'end') {
  171. this.updateValue(this.rangeValue[0], false, 0);
  172. this.updateValue(this.rangeValue[1], false, 1);
  173. this.$emit('change', this.rangeValue);
  174. }
  175. },
  176. deep:true
  177. }
  178. },
  179. mounted() {
  180. if (this.height != '') {
  181. this.sizeLocal = val
  182. } else {
  183. this.sizeLocal = this.size
  184. }
  185. },
  186. computed: {
  187. touchButtonStyle() {
  188. return (barStyle) => {
  189. let style = {}
  190. if (this.blockSize) {
  191. if (this.vertical) {
  192. style.top = (getPx(barStyle.height) ) + 'px'
  193. } else {
  194. if (barStyle.width) {
  195. style.left = (getPx(barStyle.width) + getPx(this.blockSize)/2) + 'px'
  196. // console.log(style)
  197. }
  198. }
  199. }
  200. return style
  201. }
  202. },
  203. innerStyleCpu() {
  204. let style = {...this.innerStyle};
  205. if (this.vertical) {
  206. style.flexDirection = 'row'
  207. style.height = this.length
  208. style.padding = '0'
  209. style.width = (this.isRange && this.showValue) ? (getPx(this.blockSize) + 24) + 'px' : (getPx(this.blockSize)) + 'px';
  210. } else {
  211. style.flexDirection = 'column'
  212. style.height = (this.isRange && this.showValue) ? (getPx(this.blockSize) + 24) + 'px' : (getPx(this.blockSize)) + 'px';
  213. }
  214. return style;
  215. }
  216. },
  217. async mounted() {
  218. // 获取滑块条的尺寸信息
  219. if (!this.useNative) {
  220. // #ifndef APP-NVUE
  221. this.$uGetRect('.u-slider__base').then(rect => {
  222. this.sliderRect = rect;
  223. // console.log('sliderRect', this.sliderRect)
  224. if (this.sliderRect.width == 0) {
  225. console.info('如在弹窗等元素中使用,请使用v-if来显示滑块,否则无法计算长度。')
  226. }
  227. this.init()
  228. });
  229. // #endif
  230. // #ifdef APP-NVUE
  231. await sleep(30) // 不延迟会出现size获取都为0的问题
  232. const ref = this.$refs['u-slider__base']
  233. ref &&
  234. dom.getComponentRect(ref, (res) => {
  235. // console.log(res)
  236. this.sliderRect = {
  237. top: res.size.top,
  238. left: res.size.left,
  239. width: res.size.width,
  240. height: res.size.height
  241. };
  242. this.init()
  243. })
  244. // #endif
  245. }
  246. },
  247. methods: {
  248. addUnit,
  249. addStyle,
  250. getPx,
  251. init() {
  252. if (this.isRange) {
  253. this.updateValue(this.rangeValue[0], false, 0);
  254. this.updateValue(this.rangeValue[1], false, 1);
  255. } else {
  256. // #ifdef VUE3
  257. this.updateValue(this.modelValue, false);
  258. // #endif
  259. // #ifdef VUE2
  260. this.updateValue(this.value, false);
  261. // #endif
  262. }
  263. },
  264. // native拖动过程中触发
  265. changingHandler(e) {
  266. const {
  267. value
  268. } = e.detail
  269. // 更新v-model的值
  270. // #ifdef VUE3
  271. this.$emit("update:modelValue", value);
  272. // #endif
  273. // #ifdef VUE2
  274. this.$emit("input", value);
  275. // #endif
  276. // 触发事件
  277. this.$emit('changing', value)
  278. },
  279. // native滑动结束时触发
  280. changeHandler(e) {
  281. const {
  282. value
  283. } = e.detail
  284. // 更新v-model的值
  285. // #ifdef VUE3
  286. this.$emit("update:modelValue", value);
  287. // #endif
  288. // #ifdef VUE2
  289. this.$emit("input", value);
  290. // #endif
  291. // 触发事件
  292. this.$emit('change', value);
  293. },
  294. onTouchStart(event, index = 1) {
  295. if (this.disabled) return;
  296. this.startX = 0;
  297. this.startY = 0;
  298. // 触摸点集
  299. let touches = event.touches[0];
  300. // 触摸点到屏幕左边的距离
  301. this.startX = touches.clientX;
  302. this.startY = touches.clientY;
  303. // 此处的this.modelValue虽为props值,但是通过$emit('update:modelValue')进行了修改
  304. if (this.isRange) {
  305. this.startValue0 = this.format(this.rangeValue[0], 0);
  306. this.startValue = this.format(this.rangeValue[1], 1);
  307. } else {
  308. // #ifdef VUE3
  309. this.startValue = this.format(this.modelValue);
  310. // #endif
  311. // #ifdef VUE2
  312. this.startValue = this.format(this.value);
  313. // #endif
  314. }
  315. // 标示当前的状态为开始触摸滑动
  316. this.status = 'start';
  317. // console.log('start', this.startValue)
  318. let clientX = 0;
  319. let clientY = 0;
  320. // #ifndef APP-NVUE
  321. clientX = touches.clientX;
  322. clientY = touches.clientY;
  323. // #endif
  324. // #ifdef APP-NVUE
  325. clientX = touches.screenX;
  326. clientY = touches.screenY;
  327. // #endif
  328. if (this.vertical) {
  329. this.distanceY = clientY - this.sliderRect.top;
  330. // 获得移动距离对整个滑块的值,此为带有多位小数的值,不能用此更新视图
  331. // 否则造成通信阻塞,需要每改变一个step值时修改一次视图
  332. this.newValue = ((this.distanceY / this.sliderRect.height) * (this.max - this.min)) + parseFloat(this.min);
  333. } else {
  334. this.distanceX = clientX - this.sliderRect.left;
  335. // 获得移动距离对整个滑块的值,此为带有多位小数的值,不能用此更新视图
  336. // 否则造成通信阻塞,需要每改变一个step值时修改一次视图
  337. this.newValue = ((this.distanceX / this.sliderRect.width) * (this.max - this.min)) + parseFloat(this.min);
  338. }
  339. this.status = 'moving';
  340. // 发出moving事件
  341. let $crtFmtValue = this.updateValue(this.newValue, true, index);
  342. this.$emit('changing', $crtFmtValue);
  343. },
  344. onTouchMove(event, index = 1) {
  345. if (this.disabled) return;
  346. // 连续触摸的过程会一直触发本方法,但只有手指触发且移动了才被认为是拖动了,才发出事件
  347. // 触摸后第一次移动已经将status设置为moving状态,故触摸第二次移动不会触发本事件
  348. if (this.status == 'start') this.$emit('start');
  349. let touches = event.touches[0];
  350. // console.log('touchs', touches)
  351. // 滑块的左边不一定跟屏幕左边接壤,所以需要减去最外层父元素的左边值
  352. let clientX = 0;
  353. let clientY = 0;
  354. // #ifndef APP-NVUE
  355. clientX = touches.clientX;
  356. clientY = touches.clientY;
  357. // #endif
  358. // #ifdef APP-NVUE
  359. clientX = touches.screenX;
  360. clientY = touches.screenY;
  361. // #endif
  362. if (this.vertical) {
  363. this.distanceY = clientY - this.sliderRect.top;
  364. // 获得移动距离对整个滑块的值,此为带有多位小数的值,不能用此更新视图
  365. // 否则造成通信阻塞,需要每改变一个step值时修改一次视图
  366. this.newValue = ((this.distanceY / this.sliderRect.height) * (this.max - this.min)) + parseFloat(this.min);
  367. } else {
  368. this.distanceX = clientX - this.sliderRect.left;
  369. // 获得移动距离对整个滑块的值,此为带有多位小数的值,不能用此更新视图
  370. // 否则造成通信阻塞,需要每改变一个step值时修改一次视图
  371. this.newValue = ((this.distanceX / this.sliderRect.width) * (this.max - this.min)) + parseFloat(this.min);
  372. }
  373. this.status = 'moving';
  374. // 发出moving事件
  375. let $crtFmtValue = this.updateValue(this.newValue, true, index);
  376. this.$emit('changing', $crtFmtValue);
  377. },
  378. onTouchEnd(event, index = 1) {
  379. if (this.disabled) return;
  380. if (this.status === 'moving') {
  381. let $crtFmtValue = this.updateValue(this.newValue, false, index);
  382. this.$emit('change', $crtFmtValue);
  383. }
  384. this.status = 'end';
  385. },
  386. onTouchStart2(event, index = 1) {
  387. if (!this.isRange) {
  388. // this.onChangeStart(event, index);
  389. }
  390. },
  391. onTouchMove2(event, index = 1) {
  392. if (!this.isRange) {
  393. // this.onTouchMove(event, index);
  394. }
  395. },
  396. onTouchEnd2(event, index = 1) {
  397. if (!this.isRange) {
  398. // this.onTouchEnd(event, index);
  399. }
  400. },
  401. onClick(event) {
  402. // if (this.isRange) return;
  403. if (this.disabled) return;
  404. // 直接点击滑块的情况,计算方式与onTouchMove方法相同
  405. // console.log('click', event)
  406. // #ifndef APP-NVUE
  407. // nvue下暂时无法获取坐标
  408. if (this.vertical) {
  409. let clientY = event.detail.y - this.sliderRect.top
  410. // console.log(this.sliderRect.top, event.detail.y)
  411. this.newValue = ((clientY / this.sliderRect.height) * (this.max - this.min)) + parseFloat(this.min)
  412. this.updateValue(this.newValue, false, 1)
  413. } else {
  414. let clientX = event.detail.x - this.sliderRect.left
  415. this.newValue = ((clientX / this.sliderRect.width) * (this.max - this.min)) + parseFloat(this.min)
  416. this.updateValue(this.newValue, false, 1)
  417. }
  418. // #endif
  419. },
  420. updateValue(value, drag, index = 1) {
  421. // 去掉小数部分,同时也是对step步进的处理
  422. let valueFormat = this.format(value, index)
  423. // 不允许滑动的值超过max最大值
  424. if(valueFormat > this.max ) {
  425. valueFormat = this.max
  426. }
  427. // 设置移动的距离,不能用百分比,因为NVUE不支持。
  428. let sliderLength = 0
  429. let barStyle = {}
  430. if (this.vertical) {
  431. sliderLength = Math.min((valueFormat - this.min) / (this.max - this.min) * this.sliderRect.height, this.sliderRect.height)
  432. barStyle['height'] = addUnit(sliderLength)
  433. barStyle['width'] = addUnit(this.sizeLocal)
  434. barStyle['marginLeft'] = '-' + getPx(this.sizeLocal, true)
  435. } else {
  436. sliderLength = Math.min((valueFormat - this.min) / (this.max - this.min) * this.sliderRect.width, this.sliderRect.width)
  437. barStyle['width'] = addUnit(sliderLength)
  438. barStyle['height'] = addUnit(this.sizeLocal)
  439. barStyle['marginTop'] = '-' + getPx(this.sizeLocal, true)
  440. }
  441. // 移动期间无需过渡动画
  442. if (drag == true) {
  443. barStyle.transition = 'none';
  444. } else {
  445. // 非移动期间,删掉对过渡为空的声明,让css中的声明起效
  446. delete barStyle.transition;
  447. }
  448. // 修改value值
  449. if (this.isRange) {
  450. this.rangeValue[index] = valueFormat;
  451. this.$emit("update:modelValue", this.rangeValue);
  452. } else {
  453. // #ifdef VUE3
  454. this.$emit("update:modelValue", valueFormat);
  455. // #endif
  456. // #ifdef VUE2
  457. this.$emit("input", valueFormat);
  458. // #endif
  459. }
  460. switch (index) {
  461. case 0:
  462. this.barStyle0 = {...barStyle};
  463. break;
  464. case 1:
  465. this.barStyle = {...barStyle};
  466. break;
  467. default:
  468. break;
  469. }
  470. if (this.isRange) {
  471. return this.rangeValue
  472. } else {
  473. return valueFormat
  474. }
  475. },
  476. format(value, index = 1) {
  477. // 将小数变成整数,为了减少对视图的更新,造成视图层与逻辑层的阻塞
  478. if (this.isRange) {
  479. switch (index) {
  480. case 0:
  481. return Math.round(
  482. Math.max(this.min, Math.min(value, this.rangeValue[1] - parseInt(this.step),this.max))
  483. / parseInt(this.step)
  484. ) * parseInt(this.step);
  485. break;
  486. case 1:
  487. return Math.round(
  488. Math.max(this.min, this.rangeValue[0] + parseInt(this.step), Math.min(value, this.max))
  489. / parseInt(this.step)
  490. ) * parseInt(this.step);
  491. break;
  492. default:
  493. break;
  494. }
  495. } else {
  496. return Math.round(
  497. Math.max(this.min, Math.min(value, this.max))
  498. / parseInt(this.step)
  499. ) * parseInt(this.step);
  500. }
  501. }
  502. }
  503. }
  504. </script>
  505. <style lang="scss" scoped>
  506. .u-slider {
  507. position: relative;
  508. display: flex;
  509. flex-direction: row;
  510. align-items: center;
  511. &__native {
  512. flex: 1;
  513. }
  514. &-inner {
  515. flex: 1;
  516. display: flex;
  517. flex-direction: column;
  518. position: relative;
  519. border-radius: 999px;
  520. padding: 10px 18px;
  521. justify-content: center;
  522. }
  523. &__show-value {
  524. margin: 10px 18px 10px 0px;
  525. }
  526. &__show-range-value {
  527. padding-top: 2px;
  528. font-size: 12px;
  529. line-height: 12px;
  530. position: absolute;
  531. bottom: 0;
  532. }
  533. &__base {
  534. background-color: #ebedf0;
  535. }
  536. /* #ifndef APP-NVUE */
  537. &-inner:before {
  538. position: absolute;
  539. right: 0;
  540. left: 0;
  541. content: '';
  542. top: -8px;
  543. bottom: -8px;
  544. z-index: -1;
  545. }
  546. /* #endif */
  547. &__gap {
  548. position: relative;
  549. border-radius: 999px;
  550. transition: width 0.2s;
  551. background-color: #1989fa;
  552. }
  553. &__button {
  554. width: 24px;
  555. height: 24px;
  556. border-radius: 50%;
  557. box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
  558. background-color: #fff;
  559. transform: scale(0.9);
  560. /* #ifdef H5 */
  561. cursor: pointer;
  562. /* #endif */
  563. }
  564. &__button-wrap {
  565. position: absolute;
  566. // transform: translate3d(50%, -50%, 0);
  567. }
  568. &--disabled {
  569. opacity: 0.5;
  570. }
  571. }
  572. </style>