index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. <template>
  2. <view
  3. class="slider-range-body"
  4. :class="{
  5. disabled: disabled,
  6. padding: tips || scaleInfo.show
  7. }">
  8. <!-- 刻度线 -->
  9. <view class="scale-bar" v-if="scaleInfo.show">
  10. <view
  11. class="scale-item"
  12. v-for="(scaleItem, scaleIndex) in scaleLine" :key="scaleIndex"
  13. :style="{
  14. left: scaleItem.left,
  15. color: scaleItem.color,
  16. fontSize: scaleItem.fontSize,
  17. }">
  18. {{ scaleItem.label }}
  19. <view
  20. class="scale-item-line"
  21. :style="{
  22. backgroundColor: scaleItem.tickColor
  23. }"
  24. />
  25. </view>
  26. </view>
  27. <!-- 滑动提示 -->
  28. <view class="tips-bar" v-if="tips">
  29. <view
  30. class="tips-item"
  31. v-if="dragging == 'firstBlock'"
  32. :style="{
  33. left: getFirstBlockPosition,
  34. zIndex: firstBlock.zIndex
  35. }">
  36. <!-- #ifndef MP-WEIXIN -->
  37. <slot name="tips" :value="firstValue">
  38. <view class="default-tips">{{ firstValue }}</view>
  39. </slot>
  40. <!-- #endif -->
  41. <!-- #ifdef MP-WEIXIN -->
  42. <slot name="firstTips">
  43. <view class="default-tips">{{ firstValue }}</view>
  44. </slot>
  45. <!-- #endif -->
  46. </view>
  47. <view
  48. class="tips-item"
  49. v-if="dragging == 'secondBlock'"
  50. :style="{
  51. left: getSecondBlockPosition,
  52. zIndex: secondBlock.zIndex
  53. }">
  54. <!-- #ifndef MP-WEIXIN -->
  55. <slot name="tips" :value="secondValue">
  56. <view class="default-tips">{{ secondValue }}</view>
  57. </slot>
  58. <!-- #endif -->
  59. <!-- #ifdef MP-WEIXIN -->
  60. <slot name="secondTips">
  61. <view class="default-tips">{{ secondValue }}</view>
  62. </slot>
  63. <!-- #endif -->
  64. </view>
  65. </view>
  66. <!-- 滑动条 -->
  67. <view
  68. class="slider-bar"
  69. :style="[getBackgroundStyle]">
  70. <view
  71. class="slider-active-bar"
  72. :style="[getActiveStyle]"
  73. ></view>
  74. </view>
  75. <!-- 滑块 -->
  76. <view class="block-bar">
  77. <view
  78. class="block-item"
  79. v-if="firstBlock.show"
  80. :style="{
  81. left: getFirstBlockPosition,
  82. zIndex: firstBlock.zIndex
  83. }"
  84. data-sort="firstBlock"
  85. @touchstart.stop.prevent="onTouchStart"
  86. @touchmove.stop.prevent="onTouchMove"
  87. @touchend.stop.prevent="onTouchEnd">
  88. <!-- #ifndef MP-WEIXIN -->
  89. <slot name="block">
  90. <view class="default-block block-font"></view>
  91. </slot>
  92. <!-- #endif -->
  93. <!-- #ifdef MP-WEIXIN -->
  94. <slot name="firstBlock">
  95. <view class="default-block block-font"></view>
  96. </slot>
  97. <!-- #endif -->
  98. </view>
  99. <view
  100. class="block-item"
  101. v-if="secondBlock.show"
  102. :style="{
  103. left: getSecondBlockPosition,
  104. zIndex: secondBlock.zIndex
  105. }"
  106. data-sort="secondBlock"
  107. @touchstart.stop.prevent="onTouchStart"
  108. @touchmove.stop.prevent="onTouchMove"
  109. @touchend.stop.prevent="onTouchEnd">
  110. <!-- #ifndef MP-WEIXIN -->
  111. <slot name="block">
  112. <view class="default-block block-font"></view>
  113. </slot>
  114. <!-- #endif -->
  115. <!-- #ifdef MP-WEIXIN -->
  116. <slot name="secondBlock">
  117. <view class="default-block block-font"></view>
  118. </slot>
  119. <!-- #endif -->
  120. </view>
  121. <!-- 这个元素用来撑高度 -->
  122. <view class="block-placeholder">
  123. <!-- #ifndef MP-WEIXIN -->
  124. <slot name="block">
  125. <view class="default-block block-font"></view>
  126. </slot>
  127. <!-- #endif -->
  128. <!-- #ifdef MP-WEIXIN -->
  129. <slot name="placeholderBlock">
  130. <view class="default-block block-font"></view>
  131. </slot>
  132. <!-- #endif -->
  133. </view>
  134. </view>
  135. </view>
  136. </template>
  137. <script>
  138. /**
  139. * @description 区间滑动选择组件
  140. * @example <slider-range></slider-range>
  141. * @property {number|array<number>} value 初始值,为 number 时表示滑动选择器,为 array<number> 时表示区间滑动选择器
  142. * @property {string} valueType 当 value 为 number 时,可滑动的是哪个值,可选值:min、max,默认 max
  143. * @property {number} min 最小值(最左侧值)默认 0
  144. * @property {number} max 最大值(最右侧值)默认 100
  145. * @property {number} step 步长,为 0 时不设步长,默认为 1
  146. * @property {number|string} height 滑动条高度,可自定义单位,默认单位 rpx
  147. * @property {boolean} disabled 是否禁用,默认 false
  148. * @property {boolean} tips 是否显示滑动提示,默认为 true
  149. * @property {boolean|object} scale 刻度条配置
  150. * @property {object} backgroundStyle 背景条自定义样式
  151. * @property {object} activeStyle 选中条自定义样式
  152. **/
  153. // 补全单位
  154. const completeUnit = (val = 0, unit = 'rpx') => {
  155. if(isNaN(Number(val))) return val
  156. return val + unit
  157. }
  158. // 乘法,防止小数计算失真
  159. const multiplication = (val1, val2) => {
  160. let dotIndex = String(val2).indexOf('.')
  161. if(dotIndex != -1){
  162. let floatLength = String(val2).length - (dotIndex + 1)
  163. return val1 * (val2 * Math.pow(10, floatLength)) / Math.pow(10, floatLength)
  164. }else{
  165. return val1 * val2
  166. }
  167. }
  168. export default {
  169. name:"slider-range",
  170. props: {
  171. value: {
  172. type: [Number, Array],
  173. default: () => []
  174. },
  175. valueType: {
  176. type: String,
  177. default: 'max'
  178. },
  179. min: {
  180. type: Number,
  181. default: 0
  182. },
  183. max: {
  184. type: Number,
  185. default: 100
  186. },
  187. step: {
  188. type: Number,
  189. default: 1
  190. },
  191. height: {
  192. type: [Number, String],
  193. default: 12
  194. },
  195. disabled: {
  196. type: Boolean,
  197. default: false
  198. },
  199. tips: {
  200. type: Boolean,
  201. default: true
  202. },
  203. scale: {
  204. type: [Boolean, Object],
  205. default: true
  206. },
  207. backgroundStyle: {
  208. type: Object,
  209. default: () => {
  210. return {width: '100%'}
  211. }
  212. },
  213. activeStyle: {
  214. type: Object,
  215. default: () => {
  216. return {}
  217. }
  218. }
  219. },
  220. data() {
  221. return {
  222. // 两个值
  223. firstValue: 0,
  224. secondValue: 0,
  225. // 两个滑点
  226. firstBlock: {
  227. show: true,
  228. zIndex: 1,
  229. },
  230. secondBlock: {
  231. show: true,
  232. zIndex: 2,
  233. },
  234. // 滑动中
  235. dragging: '',
  236. // 刻度线设置
  237. scaleInfo: {
  238. show: true,
  239. min: true,
  240. max: true,
  241. interval: 'auto',
  242. color: '#333',
  243. tickColor: '#999',
  244. fontSize: 22,
  245. format: null,
  246. },
  247. scaleLine: [],
  248. }
  249. },
  250. computed: {
  251. getStep(){
  252. if(this.max - this.min < this.step) return this.max - this.min
  253. return this.step
  254. },
  255. // 滑点坐标
  256. getFirstBlockPosition(){
  257. return (this.firstValue - this.min) / (this.max - this.min) * 100 + '%'
  258. },
  259. getSecondBlockPosition(){
  260. return (this.secondValue - this.min) / (this.max - this.min) * 100 + '%'
  261. },
  262. // 背景条样式
  263. getBackgroundStyle(){
  264. return {
  265. ...this.backgroundStyle,
  266. height: completeUnit(this.height)
  267. }
  268. },
  269. // 选中条样式
  270. getActiveStyle(){
  271. let min = Math.min(this.firstValue, this.secondValue)
  272. let max = Math.max(this.firstValue, this.secondValue)
  273. return {
  274. ...this.activeStyle,
  275. left: (min - this.min) / (this.max - this.min) * 100 + '%',
  276. width: (max - min) / (this.max - this.min) * 100 + '%',
  277. }
  278. }
  279. },
  280. mounted() {
  281. this.create()
  282. },
  283. methods: {
  284. create(){
  285. if(this.max <= this.min){
  286. throw '[slider-range] max 属性不应小于或等于 min 属性'
  287. }
  288. if(typeof this.value == 'number'){
  289. let value = this.value
  290. if(this.value > this.max) value = this.max
  291. if(this.value < this.min) value = this.min
  292. if(this.valueType == 'max'){
  293. this.firstBlock.show = false
  294. this.firstValue = this.min
  295. this.secondValue = value
  296. }else{
  297. this.secondBlock.show = false
  298. this.secondValue = this.max
  299. this.firstValue = value
  300. }
  301. }else{
  302. let firstValue = this.value[0] || this.min
  303. if(firstValue > this.max) firstValue = this.max
  304. if(firstValue < this.min) firstValue = this.min
  305. let secondValue = this.value[1] || this.max
  306. if(secondValue > this.max) secondValue = this.max
  307. if(secondValue < this.min) secondValue = this.min
  308. this.firstValue = firstValue
  309. this.secondValue = secondValue
  310. this.firstBlock.show = true
  311. this.secondBlock.show = true
  312. }
  313. // 计算刻度值
  314. if(typeof this.scale != 'object'){
  315. this.scaleInfo.show = !!this.scale
  316. }else{
  317. this.scaleInfo = {...this.scaleInfo, ...this.scale}
  318. }
  319. if(!this.scaleInfo.show) return
  320. let interval = this.scaleInfo.interval
  321. let step = this.getStep > 0 ? this.getStep : 1
  322. if(typeof interval != 'number'){
  323. interval = Math.ceil((this.max - this.min) / step / 10)
  324. }else{
  325. interval = interval + 1
  326. }
  327. let cumsum = this.min
  328. let arr = []
  329. while(cumsum <= this.max){
  330. arr.push({
  331. value: cumsum,
  332. label: cumsum,
  333. color: this.scaleInfo.color,
  334. fontSize: completeUnit(this.scaleInfo.fontSize),
  335. tickColor: this.scaleInfo.tickColor,
  336. left: (cumsum - this.min) / (this.max - this.min) * 100 + '%'
  337. })
  338. cumsum = cumsum + multiplication(interval, step)
  339. }
  340. if(arr[0].value == this.min && !this.scaleInfo.min) arr = arr.slice(1)
  341. if(arr[arr.length -1].value == this.max && !this.scaleInfo.max) arr = arr.slice(0, arr.length -1)
  342. if(arr[arr.length -1].value != this.max && this.scaleInfo.max) arr.push({
  343. value: this.max,
  344. label: this.max,
  345. color: this.scaleInfo.color,
  346. fontSize: completeUnit(this.scaleInfo.fontSize),
  347. tickColor: this.scaleInfo.tickColor,
  348. left: '100%'
  349. })
  350. let format = this.scaleInfo.format
  351. if(typeof format == 'function'){
  352. arr = arr.map((item, index) => {
  353. return format(item, index)
  354. })
  355. }
  356. this.scaleLine = [...arr]
  357. this.$forceUpdate()
  358. },
  359. onTouchStart(e){
  360. if(this.disabled) return
  361. this.dragging = e.currentTarget.dataset.sort
  362. e.currentTarget.dataset.sort == 'firstBlock' ? this.firstBlock.zIndex = this.secondBlock.zIndex + 1 : this.secondBlock.zIndex = this.firstBlock.zIndex + 1
  363. this.startDragPostion = e.changedTouches ? e.changedTouches[0].pageX : e.pageX
  364. this.startValue = e.currentTarget.dataset.sort == 'firstBlock' ? this.firstValue : this.secondValue
  365. this.$emit('start', {
  366. block: e.currentTarget.dataset.sort,
  367. value: this.startValue,
  368. values: [Math.min(this.firstValue, this.secondValue), Math.max(this.firstValue, this.secondValue)]
  369. })
  370. },
  371. onTouchMove(e){
  372. if(this.disabled) return
  373. this.onDrag(e)
  374. },
  375. onTouchEnd(e){
  376. this.dragging = ''
  377. if(this.disabled) return
  378. this.onDrag(e, true)
  379. },
  380. onDrag(e, end = false){
  381. let pageX = e.changedTouches ? e.changedTouches[0].pageX : e.pageX
  382. let view = uni.createSelectorQuery().in(this).select('.block-bar')
  383. view.boundingClientRect(data => {
  384. let diff = ((pageX - this.startDragPostion) / data.width) * (this.max - this.min)
  385. this.onUpdateValue(this.startValue + diff, e.currentTarget.dataset.sort, end)
  386. }).exec()
  387. },
  388. onUpdateValue(value, sort, end = false){
  389. if(value < this.min) value = this.min
  390. if(value > this.max) value = this.max
  391. if(this.getStep > 0 && value != this.min && value != this.max){
  392. value = this.min + multiplication(Math.round((value - this.min) / this.getStep), this.getStep)
  393. }else{
  394. value = Number(value.toFixed(2))
  395. }
  396. if(sort == 'firstBlock'){
  397. this.firstValue = value
  398. }else{
  399. this.secondValue = value
  400. }
  401. this.$emit('change', {
  402. firstValue: this.firstValue,
  403. secondValue: this.secondValue,
  404. values: [Math.min(this.firstValue, this.secondValue), Math.max(this.firstValue, this.secondValue)]
  405. })
  406. if(end){
  407. this.$emit('end', {
  408. block: sort,
  409. value: value,
  410. values: [Math.min(this.firstValue, this.secondValue), Math.max(this.firstValue, this.secondValue)]
  411. })
  412. }
  413. }
  414. }
  415. }
  416. </script>
  417. <style lang="scss" scoped>
  418. @font-face {
  419. font-family: 'block-font';
  420. src: url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAK8AAsAAAAABnAAAAJvAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHFQGYACCcApsgQgBNgIkAwgLBgAEIAWEZwcvG8MFyJ6aPCkSYgMLUMxWfTCChHj4WkPf39sgkItGUKiiTMrOVnmWnRpxV/v99mPaxBdpKnZP/N7cW6x5SV7/UAI0T6aVIZMZ2movuV6lLBp1KX/zBrwzj+/46Y16pUYVDyjxgOQB04slFMJMxEMeBfVaXwOQEc8I1FpWL+CwvXcUYt6WyIEkQezRIGuON11dgjCdlmVaqxYq0b1ZfFNBek+3wNfo8/FfcYSRVBTstpPXNgnOf+KdBYGb5SbFD5vz9dDHEipQYB7IxEV3+UigaJIC1YaDjNLOLPgX/18GIbFrv1B/nd1YD/qp4Z6MnulWawlua6JhYG1UGzHs5uiDw3/gSRZb18/XZ8qc05vx1z2uG3vcbsrWSqdLJiZKpkrvXDVUUzV/ljaCI8mt5X0x7WM8rmu/cb8rjlNrjKHW/BKOQCNAeVLsFYvgQ/Djd7pqIbb26roM/hrt71GyPMmARUH1fsPBT3I+gMRsZEn5WAY5nAyf11br5CjBjrf6Gutpu5l6qNY2IlqlL0Oh2iiVuVlUqLOKStUOUGtO2+E6bVyBItdg0g5BaOZH0ug7Cs0uqMw9o0Knb1Rq9odaVxF9Xp3JYC1CXiJZwuUN1Bl3FNdNkdZhWV2xlkRdYVwWnk5zWJRfWK51yI4sptjgrarFRAoqgtvYLh+TLYujK7ghM8rXiNzqggKl6S35jNvQQpAtITIJWrYB6RjOoQRkKn7fMJlqhWWJAKnxsuDRqXZUJF8hQHU4HGjgTl7xrFIVI0SBFAJnQ+16lsxi4ZDbPMsgY0g+bUTRVa3AISmoI399l/1px6CW7U3hTClUPFqcmQAAAA==') format('woff2');
  421. }
  422. .block-font {
  423. font-family: "block-font" !important;
  424. font-style: normal;
  425. -webkit-font-smoothing: antialiased;
  426. -moz-osx-font-smoothing: grayscale;
  427. &:before{
  428. content: "\e917";
  429. }
  430. }
  431. .slider-range-body{
  432. --main-color: #333333;
  433. --primary-color: #2979ff;
  434. width: 100%;
  435. box-sizing: border-box;
  436. position: relative;
  437. &.disabled{
  438. opacity: .7;
  439. }
  440. &.padding{
  441. padding-top: 72rpx;
  442. }
  443. .slider-bar{
  444. width: 100%;
  445. height: 12rpx;
  446. background-color: #f8f8f8;
  447. border-radius: 999999rpx;
  448. overflow: hidden;
  449. position: relative;
  450. .slider-active-bar{
  451. height: 100%;
  452. background-color: var(--primary-color);
  453. border-radius: 999999rpx;
  454. position: absolute;
  455. }
  456. }
  457. .block-bar{
  458. width: 100%;
  459. margin-top: 8rpx;
  460. position: relative;
  461. .block-item{
  462. position: absolute;
  463. top: 0;
  464. left: 0;
  465. transform: translateX(-50%);
  466. }
  467. .block-placeholder{
  468. opacity: 0!important;
  469. user-select: none;
  470. pointer-events: none;
  471. }
  472. .block-item, .block-placeholder{
  473. .default-block{
  474. color: var(--primary-color);
  475. font-size: 42rpx;
  476. }
  477. }
  478. }
  479. .tips-bar{
  480. width: 100%;
  481. height: 60rpx;
  482. position: absolute;
  483. top: 0;
  484. .tips-item{
  485. position: absolute;
  486. top: 0;
  487. left: 0;
  488. transform: translateX(-50%);
  489. .default-tips{
  490. width: 60rpx;
  491. height: 60rpx;
  492. display: flex;
  493. align-items: center;
  494. justify-content: center;
  495. border-radius: 50%;
  496. background-color: #ffffff;
  497. box-shadow: 0 0 20rpx rgba(0, 0, 0, .4);
  498. color: var(--main-color);
  499. font-size: 22rpx;
  500. font-weight: 500;
  501. }
  502. }
  503. }
  504. .scale-bar{
  505. width: 100%;
  506. height: 60rpx;
  507. position: absolute;
  508. top: 0;
  509. .scale-item{
  510. display: flex;
  511. flex-direction: column;
  512. align-items: center;
  513. color: #333333;
  514. font-size: 22rpx;
  515. position: absolute;
  516. bottom: 0;
  517. transform: translateX(-50%);
  518. .scale-item-line{
  519. width: 2rpx;
  520. height: 8rpx;
  521. background-color: #999999;
  522. }
  523. }
  524. }
  525. }
  526. </style>