slider-range.vue 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. <template>
  2. <view
  3. class="slider-range"
  4. :class="{ disabled: disabled }"
  5. :style="{ paddingLeft: '60rpx', paddingRight: '60rpx' }"
  6. >
  7. <view class="slider-range-inner" :style="{ height: height + 'rpx' }">
  8. <view
  9. class="slider-bar"
  10. :style="{
  11. height: barHeight + 'rpx',
  12. }"
  13. >
  14. <!-- 背景条 -->
  15. <view
  16. class="slider-bar-bg"
  17. :style="{
  18. backgroundColor: backgroundColor,
  19. }"
  20. ></view>
  21. <!-- 滑块实际区间 -->
  22. <view
  23. class="slider-bar-inner"
  24. :style="{
  25. width: ((values[1] - values[0]) / (max - min)) * 100 + '%',
  26. left: lowerHandlePosition + '%',
  27. backgroundColor: activeColor,
  28. }"
  29. ></view>
  30. </view>
  31. <!-- 滑动块-左 -->
  32. <view
  33. class="slider-handle-block"
  34. :class="{ decoration: decorationVisible }"
  35. :style="{
  36. backgroundColor: blockColor,
  37. width: blockSize + 'rpx',
  38. height: blockSize + 'rpx',
  39. left: lowerHandlePosition + '%',
  40. }"
  41. @touchstart="_onTouchStart"
  42. @touchmove="_onBlockTouchMove"
  43. @touchend="_onBlockTouchEnd"
  44. data-tag="lowerBlock"
  45. v-if="showLeftBlock"
  46. ></view>
  47. <!-- 滑动块-右 -->
  48. <view
  49. class="slider-handle-block"
  50. :class="{ decoration: decorationVisible }"
  51. :style="{
  52. backgroundColor: blockColor,
  53. width: blockSize + 'rpx',
  54. height: blockSize + 'rpx',
  55. left: higherHandlePosition + '%',
  56. }"
  57. @touchstart="_onTouchStart"
  58. @touchmove="_onBlockTouchMove"
  59. @touchend="_onBlockTouchEnd"
  60. data-tag="higherBlock"
  61. ></view>
  62. <!-- 滑块值提示 -->
  63. <view v-if="tipVisible" class="range-tip" :style="lowerTipStyle">{{ format(values[0]) }}</view>
  64. <view v-if="tipVisible" class="range-tip" :style="higherTipStyle">{{ format(values[1]) }}</view>
  65. </view>
  66. </view>
  67. </template>
  68. <script>
  69. export default {
  70. components: {},
  71. props: {
  72. //滑块区间当前取值
  73. value: {
  74. type: Array,
  75. default: function() {
  76. return [0, 100]
  77. },
  78. },
  79. //最小值
  80. min: {
  81. type: Number,
  82. default: 0,
  83. },
  84. //最大值
  85. max: {
  86. type: Number,
  87. default: 100,
  88. },
  89. step: {
  90. type: Number,
  91. default: 1,
  92. },
  93. format: {
  94. type: Function,
  95. default: function(val) {
  96. return val
  97. },
  98. },
  99. disabled: {
  100. type: Boolean,
  101. default: false,
  102. },
  103. showLeftBlock: {
  104. type: Boolean,
  105. default: true,
  106. },
  107. //滑块容器高度
  108. height: {
  109. height: Number,
  110. default: 50,
  111. },
  112. //区间进度条高度
  113. barHeight: {
  114. type: Number,
  115. default: 8,
  116. },
  117. //背景条颜色
  118. backgroundColor: {
  119. type: String,
  120. default: '#e9e9e9',
  121. },
  122. //已选择的颜色
  123. activeColor: {
  124. type: String,
  125. default: '#1aad19',
  126. },
  127. //滑块大小
  128. blockSize: {
  129. type: Number,
  130. default: 36,
  131. },
  132. blockColor: {
  133. type: String,
  134. default: '#fff',
  135. },
  136. tipVisible: {
  137. type: Boolean,
  138. default: false,
  139. },
  140. decorationVisible: {
  141. type: Boolean,
  142. default: true,
  143. },
  144. },
  145. data() {
  146. return {
  147. values: [this.min, this.max],
  148. startDragPos: 0, // 开始拖动时的坐标位置
  149. startVal: 0, //开始拖动时较小点的值
  150. }
  151. },
  152. computed: {
  153. // 较小点滑块的坐标
  154. lowerHandlePosition() {
  155. return ((this.values[0] - this.min) / (this.max - this.min)) * 100
  156. },
  157. // 较大点滑块的坐标
  158. higherHandlePosition() {
  159. return ((this.values[1] - this.min) / (this.max - this.min)) * 100
  160. },
  161. lowerTipStyle() {
  162. if (this.lowerHandlePosition < 90) {
  163. return `left: ${this.lowerHandlePosition}%;`
  164. }
  165. return `right: ${100 - this.lowerHandlePosition}%;transform: translate(50%, -100%);`
  166. },
  167. higherTipStyle() {
  168. if (this.higherHandlePosition < 90) {
  169. return `left: ${this.higherHandlePosition}%;`
  170. }
  171. return `right: ${100 - this.higherHandlePosition}%;transform: translate(50%, -100%);`
  172. },
  173. },
  174. created: function() {},
  175. onLoad: function(option) {},
  176. watch: {
  177. //滑块当前值
  178. value: {
  179. immediate: true,
  180. handler(newVal, oldVal) {
  181. if (this._isValuesValid(newVal) && (newVal[0] !== this.values[0] || newVal[1] !== this.values[1])) {
  182. this._updateValue(newVal)
  183. }
  184. },
  185. },
  186. },
  187. methods: {
  188. _updateValue(newVal) {
  189. // 步长大于区间差,或者区间最大值和最小值相等情况
  190. if (this.step >= this.max - this.min) {
  191. throw new RangeError('Invalid slider step or slider range')
  192. }
  193. let newValues = []
  194. if (Array.isArray(newVal)) {
  195. newValues = [newVal[0], newVal[1]]
  196. }
  197. if (typeof newValues[0] !== 'number') {
  198. newValues[0] = this.values[0]
  199. } else {
  200. newValues[0] = Math.round((newValues[0] - this.min) / this.step) * this.step + this.min
  201. }
  202. if (typeof newValues[1] !== 'number') {
  203. newValues[1] = this.values[1]
  204. } else {
  205. newValues[1] = Math.round((newValues[1] - this.min) / this.step) * this.step + this.min
  206. }
  207. // 新值与原值相等,不做处理
  208. if (this.values[0] === newValues[0] && this.values[1] === newValues[1]) {
  209. return
  210. }
  211. // 左侧滑块值小于最小值时,设置为最小值
  212. if (newValues[0] < this.min) {
  213. newValues[0] = this.min
  214. }
  215. // 右侧滑块值大于最大值时,设置为最大值
  216. if (newValues[1] > this.max) {
  217. newValues[1] = this.max
  218. }
  219. // 两个滑块重叠或左右交错,使两个滑块保持最小步长的间距
  220. if (newValues[0] >= newValues[1]) {
  221. // 左侧未动,右侧滑块滑到左侧滑块之左
  222. if (newValues[0] === this.values[0]) {
  223. if (this.showLeftBlock){
  224. newValues[1] = newValues[0] + this.step
  225. }else{
  226. newValues[1] = newValues[0]
  227. }
  228. } else {
  229. // 右侧未动, 左侧滑块滑到右侧之右
  230. newValues[0] = newValues[1] - this.step
  231. }
  232. }
  233. this.values = newValues
  234. this.$emit('change', this.values)
  235. },
  236. _onTouchStart: function(event) {
  237. if (this.disabled) {
  238. return
  239. }
  240. this.isDragging = true
  241. let tag = event.target.dataset.tag
  242. //兼容h5平台及某版本微信
  243. let e = event.changedTouches ? event.changedTouches[0] : event
  244. this.startDragPos = e.pageX
  245. this.startVal = tag === 'lowerBlock' ? this.values[0] : this.values[1]
  246. },
  247. _onBlockTouchMove: function(e) {
  248. if (this.disabled) {
  249. return
  250. }
  251. this._onDrag(e)
  252. },
  253. _onBlockTouchEnd: function(e) {
  254. if (this.disabled) {
  255. return
  256. }
  257. this.isDragging = false
  258. this._onDrag(e)
  259. },
  260. _onDrag(event) {
  261. if (!this.isDragging) {
  262. return
  263. }
  264. let view = uni
  265. .createSelectorQuery()
  266. .in(this)
  267. .select('.slider-range-inner')
  268. view
  269. .boundingClientRect(data => {
  270. let sliderWidth = data.width
  271. const tag = event.target.dataset.tag
  272. let e = event.changedTouches ? event.changedTouches[0] : event
  273. let diff = ((e.pageX - this.startDragPos) / sliderWidth) * (this.max - this.min)
  274. let nextVal = this.startVal + diff
  275. if (tag === 'lowerBlock') {
  276. this._updateValue([nextVal, null])
  277. } else {
  278. this._updateValue([null, nextVal])
  279. }
  280. })
  281. .exec()
  282. },
  283. _isValuesValid: function(values) {
  284. return Array.isArray(values) && values.length == 2
  285. },
  286. },
  287. }
  288. </script>
  289. <style scoped>
  290. .slider-range {
  291. position: relative;
  292. }
  293. .slider-range-inner {
  294. position: relative;
  295. width: 100%;
  296. }
  297. .slider-range.disabled .slider-bar-inner {
  298. opacity: 0.35;
  299. }
  300. .slider-range.disabled .slider-handle-block {
  301. cursor: not-allowed;
  302. }
  303. .slider-bar {
  304. position: absolute;
  305. top: 50%;
  306. left: 0;
  307. right: 0;
  308. transform: translateY(-50%);
  309. }
  310. .slider-bar-bg {
  311. position: absolute;
  312. width: 100%;
  313. height: 100%;
  314. border-radius: 10000px;
  315. z-index: 10;
  316. }
  317. .slider-bar-inner {
  318. position: absolute;
  319. width: 100%;
  320. height: 100%;
  321. border-radius: 10000rpx;
  322. z-index: 11;
  323. }
  324. .slider-handle-block {
  325. position: absolute;
  326. top: 50%;
  327. border: 4rpx solid #fff;
  328. transform: translate(-50%, -50%);
  329. border-radius: 50%;
  330. box-shadow: 0 0 6rpx 4rpx rgba(227, 229, 241, 0.5);
  331. z-index: 12;
  332. }
  333. /* .slider-handle-block.decoration::before {
  334. position: absolute;
  335. content: '';
  336. width: 6upx;
  337. height: 24upx;
  338. top: 50%;
  339. left: 29%;
  340. transform: translateY(-50%);
  341. background: #000;
  342. border-radius: 3upx;
  343. z-index: 13;
  344. } */
  345. /* .slider-handle-block.decoration::after {
  346. position: absolute;
  347. content: '';
  348. width: 6upx;
  349. height: 24upx;
  350. top: 50%;
  351. right: 29%;
  352. transform: translateY(-50%);
  353. background: #eeedf2;
  354. border-radius: 3upx;
  355. z-index: 13;
  356. } */
  357. .range-tip {
  358. position: absolute;
  359. top: 0;
  360. font-size: 24upx;
  361. color: #666;
  362. transform: translate(-50%, -100%);
  363. }
  364. </style>