25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.
 
 
 

588 satır
17 KiB

  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. async mounted() {
  180. if (this.height != '') {
  181. this.sizeLocal = val
  182. } else {
  183. this.sizeLocal = this.size
  184. }
  185. // 获取滑块条的尺寸信息
  186. if (!this.useNative) {
  187. // #ifndef APP-NVUE
  188. this.$uGetRect('.u-slider__base').then(rect => {
  189. this.sliderRect = rect;
  190. // console.log('sliderRect', this.sliderRect)
  191. if (this.sliderRect.width == 0) {
  192. console.info('如在弹窗等元素中使用,请使用v-if来显示滑块,否则无法计算长度。')
  193. }
  194. this.init()
  195. });
  196. // #endif
  197. // #ifdef APP-NVUE
  198. await sleep(30) // 不延迟会出现size获取都为0的问题
  199. const ref = this.$refs['u-slider__base']
  200. ref &&
  201. dom.getComponentRect(ref, (res) => {
  202. // console.log(res)
  203. this.sliderRect = {
  204. top: res.size.top,
  205. left: res.size.left,
  206. width: res.size.width,
  207. height: res.size.height
  208. };
  209. this.init()
  210. })
  211. // #endif
  212. }
  213. },
  214. computed: {
  215. touchButtonStyle() {
  216. return (barStyle) => {
  217. let style = {}
  218. if (this.blockSize) {
  219. if (this.vertical) {
  220. style.top = (getPx(barStyle.height) ) + 'px'
  221. } else {
  222. if (barStyle.width) {
  223. style.left = (getPx(barStyle.width) + getPx(this.blockSize)/2) + 'px'
  224. // console.log(style)
  225. }
  226. }
  227. }
  228. return style
  229. }
  230. },
  231. innerStyleCpu() {
  232. let style = {...this.innerStyle};
  233. if (this.vertical) {
  234. style.flexDirection = 'row'
  235. style.height = this.length
  236. style.padding = '0'
  237. style.width = (this.isRange && this.showValue) ? (getPx(this.blockSize) + 24) + 'px' : (getPx(this.blockSize)) + 'px';
  238. } else {
  239. style.flexDirection = 'column'
  240. style.height = (this.isRange && this.showValue) ? (getPx(this.blockSize) + 24) + 'px' : (getPx(this.blockSize)) + 'px';
  241. }
  242. return style;
  243. }
  244. },
  245. methods: {
  246. addUnit,
  247. addStyle,
  248. getPx,
  249. init() {
  250. if (this.isRange) {
  251. this.updateValue(this.rangeValue[0], false, 0);
  252. this.updateValue(this.rangeValue[1], false, 1);
  253. } else {
  254. // #ifdef VUE3
  255. this.updateValue(this.modelValue, false);
  256. // #endif
  257. // #ifdef VUE2
  258. this.updateValue(this.value, false);
  259. // #endif
  260. }
  261. },
  262. // native拖动过程中触发
  263. changingHandler(e) {
  264. const {
  265. value
  266. } = e.detail
  267. // 更新v-model的值
  268. // #ifdef VUE3
  269. this.$emit("update:modelValue", value);
  270. // #endif
  271. // #ifdef VUE2
  272. this.$emit("input", value);
  273. // #endif
  274. // 触发事件
  275. this.$emit('changing', value)
  276. },
  277. // native滑动结束时触发
  278. changeHandler(e) {
  279. const {
  280. value
  281. } = e.detail
  282. // 更新v-model的值
  283. // #ifdef VUE3
  284. this.$emit("update:modelValue", value);
  285. // #endif
  286. // #ifdef VUE2
  287. this.$emit("input", value);
  288. // #endif
  289. // 触发事件
  290. this.$emit('change', value);
  291. },
  292. onTouchStart(event, index = 1) {
  293. if (this.disabled) return;
  294. this.startX = 0;
  295. this.startY = 0;
  296. // 触摸点集
  297. let touches = event.touches[0];
  298. // 触摸点到屏幕左边的距离
  299. this.startX = touches.clientX;
  300. this.startY = touches.clientY;
  301. // 此处的this.modelValue虽为props值,但是通过$emit('update:modelValue')进行了修改
  302. if (this.isRange) {
  303. this.startValue0 = this.format(this.rangeValue[0], 0);
  304. this.startValue = this.format(this.rangeValue[1], 1);
  305. } else {
  306. // #ifdef VUE3
  307. this.startValue = this.format(this.modelValue);
  308. // #endif
  309. // #ifdef VUE2
  310. this.startValue = this.format(this.value);
  311. // #endif
  312. }
  313. // 标示当前的状态为开始触摸滑动
  314. this.status = 'start';
  315. // console.log('start', this.startValue)
  316. let clientX = 0;
  317. let clientY = 0;
  318. // #ifndef APP-NVUE
  319. clientX = touches.clientX;
  320. clientY = touches.clientY;
  321. // #endif
  322. // #ifdef APP-NVUE
  323. clientX = touches.screenX;
  324. clientY = touches.screenY;
  325. // #endif
  326. if (this.vertical) {
  327. this.distanceY = clientY - this.sliderRect.top;
  328. // 获得移动距离对整个滑块的值,此为带有多位小数的值,不能用此更新视图
  329. // 否则造成通信阻塞,需要每改变一个step值时修改一次视图
  330. this.newValue = ((this.distanceY / this.sliderRect.height) * (this.max - this.min)) + parseFloat(this.min);
  331. } else {
  332. this.distanceX = clientX - this.sliderRect.left;
  333. // 获得移动距离对整个滑块的值,此为带有多位小数的值,不能用此更新视图
  334. // 否则造成通信阻塞,需要每改变一个step值时修改一次视图
  335. this.newValue = ((this.distanceX / this.sliderRect.width) * (this.max - this.min)) + parseFloat(this.min);
  336. }
  337. this.status = 'moving';
  338. // 发出moving事件
  339. let $crtFmtValue = this.updateValue(this.newValue, true, index);
  340. this.$emit('changing', $crtFmtValue);
  341. },
  342. onTouchMove(event, index = 1) {
  343. if (this.disabled) return;
  344. // 连续触摸的过程会一直触发本方法,但只有手指触发且移动了才被认为是拖动了,才发出事件
  345. // 触摸后第一次移动已经将status设置为moving状态,故触摸第二次移动不会触发本事件
  346. if (this.status == 'start') this.$emit('start');
  347. let touches = event.touches[0];
  348. // console.log('touchs', touches)
  349. // 滑块的左边不一定跟屏幕左边接壤,所以需要减去最外层父元素的左边值
  350. let clientX = 0;
  351. let clientY = 0;
  352. // #ifndef APP-NVUE
  353. clientX = touches.clientX;
  354. clientY = touches.clientY;
  355. // #endif
  356. // #ifdef APP-NVUE
  357. clientX = touches.screenX;
  358. clientY = touches.screenY;
  359. // #endif
  360. if (this.vertical) {
  361. this.distanceY = clientY - this.sliderRect.top;
  362. // 获得移动距离对整个滑块的值,此为带有多位小数的值,不能用此更新视图
  363. // 否则造成通信阻塞,需要每改变一个step值时修改一次视图
  364. this.newValue = ((this.distanceY / this.sliderRect.height) * (this.max - this.min)) + parseFloat(this.min);
  365. } else {
  366. this.distanceX = clientX - this.sliderRect.left;
  367. // 获得移动距离对整个滑块的值,此为带有多位小数的值,不能用此更新视图
  368. // 否则造成通信阻塞,需要每改变一个step值时修改一次视图
  369. this.newValue = ((this.distanceX / this.sliderRect.width) * (this.max - this.min)) + parseFloat(this.min);
  370. }
  371. this.status = 'moving';
  372. // 发出moving事件
  373. let $crtFmtValue = this.updateValue(this.newValue, true, index);
  374. this.$emit('changing', $crtFmtValue);
  375. },
  376. onTouchEnd(event, index = 1) {
  377. if (this.disabled) return;
  378. if (this.status === 'moving') {
  379. let $crtFmtValue = this.updateValue(this.newValue, false, index);
  380. this.$emit('change', $crtFmtValue);
  381. }
  382. this.status = 'end';
  383. },
  384. onTouchStart2(event, index = 1) {
  385. if (!this.isRange) {
  386. // this.onChangeStart(event, index);
  387. }
  388. },
  389. onTouchMove2(event, index = 1) {
  390. if (!this.isRange) {
  391. // this.onTouchMove(event, index);
  392. }
  393. },
  394. onTouchEnd2(event, index = 1) {
  395. if (!this.isRange) {
  396. // this.onTouchEnd(event, index);
  397. }
  398. },
  399. onClick(event) {
  400. // if (this.isRange) return;
  401. if (this.disabled) return;
  402. // 直接点击滑块的情况,计算方式与onTouchMove方法相同
  403. // console.log('click', event)
  404. // #ifndef APP-NVUE
  405. // nvue下暂时无法获取坐标
  406. if (this.vertical) {
  407. let clientY = event.detail.y - this.sliderRect.top
  408. // console.log(this.sliderRect.top, event.detail.y)
  409. this.newValue = ((clientY / this.sliderRect.height) * (this.max - this.min)) + parseFloat(this.min)
  410. this.updateValue(this.newValue, false, 1)
  411. } else {
  412. let clientX = event.detail.x - this.sliderRect.left
  413. this.newValue = ((clientX / this.sliderRect.width) * (this.max - this.min)) + parseFloat(this.min)
  414. this.updateValue(this.newValue, false, 1)
  415. }
  416. // #endif
  417. },
  418. updateValue(value, drag, index = 1) {
  419. // 去掉小数部分,同时也是对step步进的处理
  420. let valueFormat = this.format(value, index)
  421. // 不允许滑动的值超过max最大值
  422. if(valueFormat > this.max ) {
  423. valueFormat = this.max
  424. }
  425. // 设置移动的距离,不能用百分比,因为NVUE不支持。
  426. let sliderLength = 0
  427. let barStyle = {}
  428. if (this.vertical) {
  429. sliderLength = Math.min((valueFormat - this.min) / (this.max - this.min) * this.sliderRect.height, this.sliderRect.height)
  430. barStyle['height'] = addUnit(sliderLength)
  431. barStyle['width'] = addUnit(this.sizeLocal)
  432. barStyle['marginLeft'] = '-' + getPx(this.sizeLocal, true)
  433. } else {
  434. sliderLength = Math.min((valueFormat - this.min) / (this.max - this.min) * this.sliderRect.width, this.sliderRect.width)
  435. barStyle['width'] = addUnit(sliderLength)
  436. barStyle['height'] = addUnit(this.sizeLocal)
  437. barStyle['marginTop'] = '-' + getPx(this.sizeLocal, true)
  438. }
  439. // 移动期间无需过渡动画
  440. if (drag == true) {
  441. barStyle.transition = 'none';
  442. } else {
  443. // 非移动期间,删掉对过渡为空的声明,让css中的声明起效
  444. delete barStyle.transition;
  445. }
  446. // 修改value值
  447. if (this.isRange) {
  448. this.rangeValue[index] = valueFormat;
  449. this.$emit("update:modelValue", this.rangeValue);
  450. } else {
  451. // #ifdef VUE3
  452. this.$emit("update:modelValue", valueFormat);
  453. // #endif
  454. // #ifdef VUE2
  455. this.$emit("input", valueFormat);
  456. // #endif
  457. }
  458. switch (index) {
  459. case 0:
  460. this.barStyle0 = {...barStyle};
  461. break;
  462. case 1:
  463. this.barStyle = {...barStyle};
  464. break;
  465. default:
  466. break;
  467. }
  468. if (this.isRange) {
  469. return this.rangeValue
  470. } else {
  471. return valueFormat
  472. }
  473. },
  474. format(value, index = 1) {
  475. // 将小数变成整数,为了减少对视图的更新,造成视图层与逻辑层的阻塞
  476. if (this.isRange) {
  477. switch (index) {
  478. case 0:
  479. return Math.round(
  480. Math.max(this.min, Math.min(value, this.rangeValue[1] - parseInt(this.step),this.max))
  481. / parseInt(this.step)
  482. ) * parseInt(this.step);
  483. break;
  484. case 1:
  485. return Math.round(
  486. Math.max(this.min, this.rangeValue[0] + parseInt(this.step), Math.min(value, this.max))
  487. / parseInt(this.step)
  488. ) * parseInt(this.step);
  489. break;
  490. default:
  491. break;
  492. }
  493. } else {
  494. return Math.round(
  495. Math.max(this.min, Math.min(value, this.max))
  496. / parseInt(this.step)
  497. ) * parseInt(this.step);
  498. }
  499. }
  500. }
  501. }
  502. </script>
  503. <style lang="scss" scoped>
  504. .u-slider {
  505. position: relative;
  506. display: flex;
  507. flex-direction: row;
  508. align-items: center;
  509. &__native {
  510. flex: 1;
  511. }
  512. &-inner {
  513. flex: 1;
  514. display: flex;
  515. flex-direction: column;
  516. position: relative;
  517. border-radius: 999px;
  518. padding: 10px 18px;
  519. justify-content: center;
  520. }
  521. &__show-value {
  522. margin: 10px 18px 10px 0px;
  523. }
  524. &__show-range-value {
  525. padding-top: 2px;
  526. font-size: 12px;
  527. line-height: 12px;
  528. position: absolute;
  529. bottom: 0;
  530. }
  531. &__base {
  532. background-color: #ebedf0;
  533. }
  534. /* #ifndef APP-NVUE */
  535. &-inner:before {
  536. position: absolute;
  537. right: 0;
  538. left: 0;
  539. content: '';
  540. top: -8px;
  541. bottom: -8px;
  542. z-index: -1;
  543. }
  544. /* #endif */
  545. &__gap {
  546. position: relative;
  547. border-radius: 999px;
  548. transition: width 0.2s;
  549. background-color: #1989fa;
  550. }
  551. &__button {
  552. width: 24px;
  553. height: 24px;
  554. border-radius: 50%;
  555. box-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
  556. background-color: #fff;
  557. transform: scale(0.9);
  558. /* #ifdef H5 */
  559. cursor: pointer;
  560. /* #endif */
  561. }
  562. &__button-wrap {
  563. position: absolute;
  564. // transform: translate3d(50%, -50%, 0);
  565. }
  566. &--disabled {
  567. opacity: 0.5;
  568. }
  569. }
  570. </style>