index.vue 8.9 KB

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