l-slider.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  1. <template>
  2. <view class="l-slider"
  3. :class="[
  4. 'l-slider--' + (vertical ? 'vertical': 'horizontal'),{
  5. 'l-slider--disabled': disabled}
  6. ]"
  7. :style="[styles]"
  8. ref="sliderRef"
  9. @touchstart.stop="touchstart"
  10. @touchmove.stop="touchmove"
  11. @touchend="touchend">
  12. <view ref="railRef" class="l-slider__rail"
  13. :class="['l-slider__rail--' + (vertical ? 'vertical': 'horizontal')]"
  14. :style="[railStyle]">
  15. <view class="l-slider__track"
  16. :class="['l-slider__track--' + (vertical ? 'vertical': 'horizontal')]"
  17. :style="[progressBarStyle]" ref="progressBarRef"></view>
  18. </view>
  19. <view class="l-slider__thumb-wrapper"
  20. :class="['l-slider__thumb-wrapper--' + (vertical ? 'vertical': 'horizontal')]"
  21. :style="[startThumbStyle]"
  22. ref="startThumbRef"
  23. data-thumb="0">
  24. <slot name="start-thumb">
  25. <view class="l-slider__thumb" :style="[thumbStyle]"></view>
  26. </slot>
  27. </view>
  28. <view class="l-slider__thumb-wrapper"
  29. :class="['l-slider__thumb-wrapper--' + (vertical ? 'vertical': 'horizontal')]"
  30. :style="[endThumbStyle]"
  31. ref="endThumbRef"
  32. data-thumb="1"
  33. v-if="range">
  34. <slot name="end-thumb">
  35. <view class="l-slider__thumb" :style="[thumbStyle]"></view>
  36. </slot>
  37. </view>
  38. </view>
  39. </template>
  40. <script lang="ts">
  41. // @ts-nocheck
  42. import { computed, defineComponent, getCurrentInstance, nextTick, onMounted, onUnmounted ,ref, watch, reactive } from '@/uni_modules/lime-shared/vue';
  43. import sliderProps from './props'
  44. import { SliderValue } from './type';
  45. import { getRect } from '@/uni_modules/lime-shared/getRect';
  46. import { closest } from '@/uni_modules/lime-shared/closest';
  47. import { useTouch } from './touch'
  48. import { format } from './utils'
  49. export default defineComponent({
  50. name: 'l-slider',
  51. props: sliderProps,
  52. emits: ['change', 'update:modelValue', 'change-position'],
  53. setup(props, {emit}) {
  54. const instance = getCurrentInstance().proxy!
  55. const sliderRect = reactive({
  56. width: 0,
  57. height: 0,
  58. 'min-height': '',
  59. 'min-width': '',
  60. left: 0,
  61. top: 0
  62. })
  63. const railRect = reactive({
  64. width: 0,
  65. height: 0,
  66. left: 0,
  67. top: 0
  68. })
  69. const progressBarRect = reactive({
  70. width: null,
  71. height: null,
  72. left: null,
  73. opacity: 1,
  74. top: null
  75. })
  76. const thumbRect = reactive({
  77. width: 0,
  78. height: 0,
  79. left: 0,
  80. top: 0
  81. })
  82. const startThumbStyle = reactive({
  83. left: null,
  84. top: null
  85. })
  86. const endThumbStyle = reactive({
  87. left: null,
  88. top: null
  89. })
  90. const tempValue = ref<number[]>([0, 0]);
  91. const innerValue = computed({
  92. set(value : SliderValue) {
  93. emit('change', value);
  94. emit('update:modelValue', value);
  95. // #ifdef VUE2
  96. emit('input', value);
  97. // #endif
  98. },
  99. get() : SliderValue {
  100. return props.value ?? props.modelValue ?? props.min
  101. }
  102. })
  103. const isRange = computed(() : boolean => props.range && Array.isArray(innerValue.value) && innerValue.value.length == 2);
  104. const direction = computed(() : Map<string, string> => {
  105. const map = new Map<string, string>();
  106. map.set('left', props.vertical ? 'top' : 'left');
  107. map.set('right', props.vertical ? 'bottom' : 'right');
  108. map.set('width', props.vertical ? 'height' : 'width');
  109. map.set('min-height', props.vertical ? 'min-width' : 'min-height');
  110. map.set('vertical', props.vertical ? 'vertical' : 'horizontal');
  111. // map.set('margin-top', props.vertical ? 'margin-left' : 'margin-top');
  112. // map.set('margin-bottom', props.vertical ? 'margin-right' : 'margin-bottom');
  113. return map
  114. })
  115. const getDirection = (key : string) : string => {
  116. return direction.value.get(key) ?? key
  117. }
  118. const styles = computed(()=>{
  119. const style:Record<string, any> = {};
  120. const minHeight = getDirection('min-height')
  121. style[minHeight] = sliderRect[minHeight]
  122. return style
  123. })
  124. const railStyle = computed(() => {
  125. const style:Record<string, any> = {};
  126. if (props.railColor) {
  127. style['background'] = props.railColor!
  128. }
  129. if (props.railSize) {
  130. style[props.vertical ? 'width': 'height'] = props.railSize!
  131. }
  132. return style
  133. })
  134. const thumbStyle = computed(() => {
  135. const style:Record<string, any> = {};
  136. if (props.thumbColor) {
  137. style['background'] = props.thumbColor!
  138. }
  139. if(props.thumbBorderColor) {
  140. style['border-color'] = props.thumbBorderColor!
  141. }
  142. if (props.thumbSize) {
  143. style['width'] = props.thumbSize!
  144. style['height']= props.thumbSize!
  145. }
  146. return style
  147. })
  148. const progressBarStyle = computed(() : Map<string, any> => {
  149. const style:Record<string, any> = {};
  150. if (props.trackColor) {
  151. style['background'] = props.trackColor!
  152. }
  153. const left = getDirection('left')
  154. const width = getDirection('width')
  155. style[left] = progressBarRect[left]
  156. style[width] = progressBarRect[width]
  157. style['opacity'] = progressBarRect.opacity
  158. return style
  159. })
  160. const scope = computed(() : number => props.max - props.min);
  161. // 计算选中条的长度占轨道总长度的比例
  162. const calculateSelectedBarRatio = () : number => {
  163. const { min } = props;
  164. if (isRange.value) {
  165. // 对于范围选择,计算两个值之间的差值相对于总范围的比值
  166. return Math.abs(tempValue.value[1] - tempValue.value[0]) / scope.value;
  167. }
  168. // 对于单个值选择,计算该值相对于总范围的比值
  169. return (tempValue.value[0] - min) / scope.value;
  170. };
  171. // 计算选中条起始位置的偏移量占轨道总长度的比例
  172. const calculateStartOffsetRatio = () : number => {
  173. const { min } = props;
  174. if (isRange.value) {
  175. // 对于范围选择,计算起始值相对于总范围的偏移量比值
  176. const _startValue = Math.min(tempValue.value[0], tempValue.value[1]);
  177. return (_startValue - min) / scope.value;
  178. }
  179. // 对于单个值选择,偏移量为0
  180. return 0;
  181. };
  182. // 计算轨道的最大尺寸(宽或高)
  183. const calculateMaxTrackDimension = () : number[] => {
  184. const railDimension = props.vertical ? railRect.width : railRect.height;
  185. const thumbDimension = props.vertical ? thumbRect.width : thumbRect.height;
  186. return [Math.max(railDimension, thumbRect.width, thumbRect.height), Math.max(railDimension, thumbDimension)];
  187. };
  188. // 计算轨道的总长度
  189. const calculateTrackLength = () : number => {
  190. return props.vertical ? railRect.height : railRect.width
  191. };
  192. // 更新进度条和滑块的位置
  193. const updateProgressBarAndThumb = () => {
  194. let trackLength = calculateTrackLength();
  195. const selectedBarRatio = calculateSelectedBarRatio();
  196. const startOffsetRatio = calculateStartOffsetRatio();
  197. const [start, end] = tempValue.value;
  198. const [maxDimension, thumbDimension] = calculateMaxTrackDimension();
  199. const thumbHalf = maxDimension / 2;
  200. const usableTrackLength = trackLength - maxDimension;
  201. progressBarRect[getDirection('left')] = `${startOffsetRatio * usableTrackLength}px`
  202. progressBarRect[getDirection('width')] = `${(selectedBarRatio * usableTrackLength + maxDimension)}px`
  203. progressBarRect[getDirection('opacity')] = selectedBarRatio == 0 && start == props.min ? 0 : 1
  204. sliderRect[getDirection('min-height')] = `${thumbDimension}px`
  205. // 更新滑块位置
  206. const startThumbPosition = (start - props.min) / scope.value * usableTrackLength + thumbHalf;
  207. startThumbStyle[getDirection('left')] = `${startThumbPosition}px`
  208. if (!isRange.value) return;
  209. const endThumbPosition = (end - props.min) / scope.value * usableTrackLength + thumbHalf;
  210. endThumbStyle[getDirection('left')] = `${endThumbPosition}px`
  211. }
  212. const touch = useTouch()
  213. const getDelta = (startX : number, startY : number) : number => {
  214. // #ifdef WEB
  215. const topHeight = document.querySelector('.uni-page-head').offsetHeight
  216. // #endif
  217. // #ifndef WEB
  218. const topHeight = 0
  219. // #endif
  220. return props.vertical ? startY - sliderRect.top - topHeight : startX - sliderRect.left;
  221. }
  222. let startValue = 0
  223. const currentKey = ref(0)
  224. const dragStatus = ref<DragStatus>('')
  225. const touchstart = (event : UniTouchEvent) => {
  226. if (props.disabled || dragStatus.value != '') return;
  227. // event.preventDefault()
  228. dragStatus.value = 'start'
  229. touch.start(event);
  230. // #ifdef VUE3
  231. const { startX, startY } = touch;
  232. const delta = getDelta(startX.value, startY.value)
  233. // #endif
  234. // #ifdef VUE2
  235. const delta = getDelta(event.touches[0].pageX, event.touches[0].pageY)
  236. // #endif
  237. const total = calculateTrackLength()
  238. let value = format(delta / total * scope.value + props.min, props.min, props.max, props.step)
  239. if (isRange.value) {
  240. currentKey.value = tempValue.value.findIndex((it) : boolean => it == closest(tempValue.value, value))
  241. tempValue.value[currentKey.value] = value
  242. innerValue.value = [...tempValue.value]
  243. } else {
  244. currentKey.value = 0;
  245. innerValue.value = value
  246. }
  247. startValue = value;
  248. }
  249. const touchmove = (event : UniTouchEvent) => {
  250. if (props.disabled) return;
  251. // event.preventDefault()
  252. dragStatus.value = 'dragging'
  253. touch.move(event);
  254. const key = currentKey.value
  255. const delta = props.vertical ? touch.deltaY : touch.deltaX;
  256. const total = calculateTrackLength()
  257. const diff = delta.value / total * scope.value;
  258. const value = format(startValue + diff, props.min, props.max, props.step)
  259. tempValue.value[key] = value
  260. if (isRange.value) {
  261. innerValue.value = [...tempValue.value]
  262. } else {
  263. innerValue.value = value
  264. }
  265. }
  266. const touchend = (_event : UniTouchEvent) => {
  267. if (props.disabled) return;
  268. dragStatus.value = ''
  269. if (!isRange.value) {
  270. currentKey.value = 0
  271. } else {
  272. currentKey.value = -1
  273. }
  274. }
  275. const setValue = (v: SliderValue) => {
  276. // if(dragStatus.value == '') {
  277. if (isRange.value) {
  278. tempValue.value = v.map((value) : number => format(value, props.min, props.max, props.step))
  279. } else {
  280. tempValue.value[0] = format(innerValue.value as number, props.min, props.max, props.step)
  281. }
  282. // }
  283. nextTick(updateProgressBarAndThumb)
  284. }
  285. const stopWatch = watch(innerValue, setValue, { deep: true })
  286. const init = () => {
  287. Promise.all([
  288. getRect('.l-slider', instance),
  289. getRect('.l-slider__rail', instance),
  290. getRect('.l-slider__thumb-wrapper', instance),
  291. ]).then(([slider, rail, thumb])=>{
  292. sliderRect.width = slider.width;
  293. sliderRect.height = slider.height;
  294. sliderRect.left = slider.left;
  295. sliderRect.top = slider.top;
  296. railRect.width = rail.width;
  297. railRect.height = rail.height;
  298. railRect.left = rail.left;
  299. railRect.top = rail.top;
  300. thumbRect.width = thumb.width;
  301. thumbRect.height = thumb.height;
  302. thumbRect.left = thumb.left;
  303. thumbRect.top = thumb.top;
  304. setValue(innerValue.value)
  305. })
  306. }
  307. onMounted(()=>{
  308. nextTick(init)
  309. })
  310. onUnmounted(stopWatch)
  311. return {
  312. styles,
  313. railStyle,
  314. thumbStyle,
  315. progressBarStyle,
  316. startThumbStyle,
  317. endThumbStyle,
  318. touchstart,
  319. touchmove,
  320. touchend
  321. }
  322. }
  323. })
  324. </script>
  325. <style lang="scss">
  326. @import './index-u';
  327. </style>