123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387 |
- <template>
- <view class="l-slider" :class="classes" ref="sliderRef" @touchstart="touchstart" @touchmove="touchmove"
- @touchend="touchend">
- <template v-if="isApp && ($slots['start-thumb'] != null || $slots['end-thumb'] != null) || !isApp">
- <view ref="railRef" class="l-slider__rail" :class="railClass" :style="[railStyle]">
- <view class="l-slider__track" :class="trackClass" :style="[trackStyle]" ref="progressBarRef"></view>
- </view>
- <view class="l-slider__thumb-wrapper" :class="thumbClass" ref="startThumbRef" data-thumb="0">
- <slot name="start-thumb">
- <view class="l-slider__thumb" :style="[thumbStyle]"></view>
- </slot>
- </view>
- <view class="l-slider__thumb-wrapper" :class="thumbClass" ref="endThumbRef" data-thumb="1" v-if="range">
- <slot name="end-thumb">
- <view class="l-slider__thumb" :style="[thumbStyle]"></view>
- </slot>
- </view>
- </template>
- </view>
- </template>
- <script lang="uts" setup>
- import { SliderProps, DragStatus } from './type';
- import { isNumber } from '@/uni_modules/lime-shared/isNumber'
- import { closest } from '@/uni_modules/lime-shared/closest'
- import { unitConvert } from '@/uni_modules/lime-shared/unitConvert'
- import { format } from './utils'
- import { useTouch } from './touch'
- const emit = defineEmits(['change', 'update:modelValue', 'change-position'])
- const props = withDefaults(defineProps<SliderProps>(), {
- max: 100,
- min: 0,
- step: 1,
- range: false,
- vertical: false,
- disabled: false
- })
- let dragStatus = ref<DragStatus>('')
- let startValue = 0;
- // #ifdef APP
- const isApp = true;
- // #endif
- // #ifndef APP
- const isApp = false;
- // #endif
- const tempValue = ref<number[]>([0, 0]);
- const innerValue = computed({
- set(value : any) {
- emit('change', value);
- emit('update:modelValue', value);
- },
- get() : any {
- const value = props.value ?? props.modelValue ?? props.min;
- if (isNumber(value) && !props.range) {
- return value as number
- }
- if (Array.isArray(value) && (value as any[]).every((v) : boolean => isNumber(v)) && props.range) {
- return value as number[]
- }
- throw new Error(`l-slider value 必须是 number | number[],当前值为: ${JSON.stringify(value)}`);
- return value
- }
- } as WritableComputedOptions<any>)
- const isRange = computed(() : boolean => props.range && Array.isArray(innerValue.value) && (innerValue.value as any[]).length == 2);
- const direction = computed(() : Map<string, string> => {
- const map = new Map<string, string>();
- map.set('left', props.vertical ? 'top' : 'left');
- map.set('right', props.vertical ? 'bottom' : 'right');
- map.set('width', props.vertical ? 'height' : 'width');
- map.set('min-height', props.vertical ? 'min-width' : 'min-height');
- map.set('vertical', props.vertical ? 'vertical' : 'horizontal');
- // map.set('margin-top', props.vertical ? 'margin-left' : 'margin-top');
- // map.set('margin-bottom', props.vertical ? 'margin-right' : 'margin-bottom');
- return map
- })
- const getDirection = (key : string) : string => {
- return direction.value.get(key) ?? key
- }
- const classes = computed(() : Map<string, any> => {
- const cls = new Map<string, any>();
- cls.set(`l-slider--${getDirection('vertical')}`, true)
- cls.set(`l-slider--disabled`, props.disabled)
- return cls
- })
- const railClass = computed(() : Map<string, any> => {
- const cls = new Map<string, any>();
- cls.set(`l-slider__rail--${getDirection('vertical')}`, true)
- return cls
- })
- const trackClass = computed(() : Map<string, any> => {
- const cls = new Map<string, any>();
- cls.set(`l-slider__track--${getDirection('vertical')}`, true)
- return cls
- })
- const thumbClass = computed(() : Map<string, any> => {
- const cls = new Map<string, any>();
- cls.set(`l-slider__thumb-wrapper--${getDirection('vertical')}`, true)
- return cls
- })
- const railStyle = computed(() : Map<string, any> => {
- const style = new Map<string, any>();
- if (props.railColor != null) {
- style.set('background', props.railColor!)
- }
- if (props.railSize != null) {
- style.set(props.vertical ? 'width': 'height' , props.railSize!)
- }
- return style
- })
- const thumbStyle = computed(() : Map<string, any> => {
- const style = new Map<string, any>();
- if (props.thumbColor != null) {
- style.set('background', props.thumbColor!)
- }
- if(props.thumbBorderColor != null) {
- style.set('border-color', props.thumbBorderColor!)
- }
- if (props.thumbSize != null) {
- style.set('width', props.thumbSize!)
- style.set('height', props.thumbSize!)
- }
- return style
- })
- const trackStyle = computed(() : Map<string, any> => {
- const style = new Map<string, any>();
- if (props.trackColor != null) {
- style.set('background', props.trackColor!)
- }
- return style
- })
- const scope = computed(() : number => props.max - props.min);
- const sliderRef = ref<UniElement | null>(null)
- const railRef = ref<UniElement | null>(null)
- const progressBarRef = ref<UniElement | null>(null)
- const startThumbRef = ref<UniElement | null>(null)
- const endThumbRef = ref<UniElement | null>(null)
- // 计算选中条的长度占轨道总长度的比例
- const calculateSelectedBarRatio = () : number => {
- const { min } = props;
- if (isRange.value) {
- // 对于范围选择,计算两个值之间的差值相对于总范围的比值
- // const _innerValue = innerValue.value as number[]
- // return Math.abs(_innerValue[1] - _innerValue[0]) / scope.value;
- return Math.abs(tempValue.value[1] - tempValue.value[0]) / scope.value;
- }
- // 对于单个值选择,计算该值相对于总范围的比值
- // return (innerValue.value as number - min) / scope.value;
- return (tempValue.value[0] - min) / scope.value;
- };
- // 计算选中条起始位置的偏移量占轨道总长度的比例
- const calculateStartOffsetRatio = () : number => {
- const { min } = props;
- if (isRange.value) {
- // 对于范围选择,计算起始值相对于总范围的偏移量比值
- // const _innerValue = innerValue.value as number[]
- const _startValue = Math.min(tempValue.value[0], tempValue.value[1]);
- return (_startValue - min) / scope.value;
-
- }
- // 对于单个值选择,偏移量为0
- return 0;
- };
- // 计算轨道的最大尺寸(宽或高)
- const calculateMaxTrackDimension = () : number[] => {
- if (railRef.value == null || startThumbRef.value == null) return [0, 0];
- const railBounds = railRef.value!.getBoundingClientRect();
- const thumbBounds = startThumbRef.value!.getBoundingClientRect();
- const railDimension = props.vertical ? railBounds.width : railBounds.height;
- const thumbDimension = props.vertical ? thumbBounds.width : thumbBounds.height;
- // return Math.max(railDimension, thumbDimension);
- return [Math.max(railDimension, thumbBounds.width, thumbBounds.height), Math.max(railDimension, thumbDimension)];
- };
- // 计算轨道的总长度
- const calculateTrackLength = () : number => {
- return (props.vertical ? railRef.value?.offsetHeight : railRef.value?.offsetWidth) ?? (props.vertical ? sliderRef.value?.offsetHeight : sliderRef.value?.offsetWidth) ?? 0;
- };
- // 更新进度条和滑块的位置
- const updateProgressBarAndThumb = () => {
- if(sliderRef.value == null) return;
- let trackLength = calculateTrackLength();
- const selectedBarRatio = calculateSelectedBarRatio();
- const startOffsetRatio = calculateStartOffsetRatio();
- const [start, end] = tempValue.value;
-
- // #ifdef APP
- if (progressBarRef.value == null || startThumbRef.value == null) {
- const ctx = sliderRef.value!.getDrawableContext()!
- const thumbSize = unitConvert(props.thumbSize ?? 12)
- const thumbColor = props.thumbColor ?? 'white'
- const railColor = props.railColor ?? 'rgba(0,0,0,0.06)'
- const railSize = unitConvert(props.railSize ?? 20)
- const trackColor = props.trackColor ?? '#3283ff'
- const thumbDimension = Math.max(railSize, thumbSize)
- const thumbHalf = thumbDimension / 2;
- let offset = 0
-
- sliderRef.value?.style.setProperty(getDirection('min-height'), `${thumbDimension}px`)
- if(thumbSize > railSize) {
- sliderRef.value?.style.setProperty(props.vertical ? 'margin-top' : 'margin-left', `-1px`)
- sliderRef.value?.style.setProperty(props.vertical ? 'margin-bottom' : 'margin-right', `-1px`)
- offset = 1
- trackLength -= 2
- }
-
- const usableTrackLength = trackLength - thumbDimension
- const railHalf = railSize / 2
- const x1 = props.vertical ? thumbHalf : (railHalf + offset)
- const y1 = props.vertical ? (railHalf + offset) : thumbHalf;
-
- const x2 = props.vertical ? thumbHalf : trackLength - railHalf - offset;
- const y2 = props.vertical ? trackLength - railHalf - offset : thumbHalf
-
- const s1 = startOffsetRatio * (trackLength - railSize)
- const w1 = selectedBarRatio * (trackLength - railSize)
-
- const x3 = props.vertical ? thumbHalf : (railHalf + s1 + offset);
- const y3 = props.vertical ? (railHalf + s1 + offset) : thumbHalf;
-
- const x4 = props.vertical ? thumbHalf : (x3 + w1 - offset)
- const y4 = props.vertical ? (y3 + w1 - offset) : thumbHalf;
-
-
- ctx.reset();
- ctx.lineWidth = railSize;
- ctx.strokeStyle = railColor;
- ctx.lineCap = 'round';
-
- // Draw rail
- ctx.beginPath();
- ctx.moveTo(x1, y1);
- ctx.lineTo(x2, y2);
- ctx.stroke();
-
- // Draw track 当不在起点时绘制
- if(w1 != 0 || start != props.min) {
- ctx.strokeStyle = trackColor;
- ctx.beginPath();
- ctx.moveTo(x3, y3);
- ctx.lineTo(x4, y4);
- ctx.stroke();
- }
-
- // Draw thumbs
- const drawThumb = (position: number) => {
- const thumbPosition = props.vertical ? thumbHalf : position;
- const oppositeThumbPosition = props.vertical ? position : thumbHalf;
- ctx.fillStyle = thumbColor;
- ctx.strokeStyle = props.thumbBorderColor ?? 'rgba(0,0,0,0.2)'
- ctx.lineWidth = 1;
- ctx.beginPath();
- ctx.arc(thumbPosition, oppositeThumbPosition, (thumbSize - 2) / 2, 0, Math.PI * 2);
- ctx.stroke();
- ctx.fill();
- };
- const startThumbPosition = (start - props.min) / scope.value * usableTrackLength + thumbHalf;
- drawThumb(startThumbPosition + offset);
- if (isRange.value) {
- const endThumbPosition = (end - props.min) / scope.value * usableTrackLength + thumbHalf;
- drawThumb(endThumbPosition + offset);
- }
- ctx.update();
- return;
- }
- // #endif
- if (progressBarRef.value == null || startThumbRef.value == null) return;
- const [maxDimension, thumbDimension] = calculateMaxTrackDimension();
- const thumbHalf = maxDimension / 2;
- const usableTrackLength = trackLength - maxDimension;
-
- // 更新进度条位置和宽度
- progressBarRef.value?.style.setProperty(getDirection('left'), `${startOffsetRatio * usableTrackLength}px`);
- progressBarRef.value?.style.setProperty(getDirection('width'), `${(selectedBarRatio * usableTrackLength + maxDimension)}px`);
- progressBarRef.value?.style.setProperty('opacity', selectedBarRatio == 0 && start == props.min ? 0 : 1);
- sliderRef.value!.style.setProperty(getDirection('min-height'), `${thumbDimension}px`)
- // 更新滑块位置
- const startThumbPosition = (start - props.min) / scope.value * usableTrackLength + thumbHalf;
- startThumbRef.value?.style.setProperty(getDirection('left'), `${startThumbPosition}px`);
- const position : number[] = [startOffsetRatio * usableTrackLength, 0]
- // emit('change-position', position)
- if (!isRange.value || endThumbRef.value == null) return;
- const endThumbPosition = (end - props.min) / scope.value * usableTrackLength + thumbHalf;
- endThumbRef.value!.style.setProperty(getDirection('left'), `${endThumbPosition}px`);
- };
- const touch = useTouch()
- const getDelta = (startX : number, startY : number) : number => {
- if (sliderRef.value == null) return 0;
- const rect = sliderRef.value!.getBoundingClientRect()
- return props.vertical ? startY - rect.top : startX - rect.left;
- }
- const currentKey = ref(0)
- const touchstart = (event : UniTouchEvent) => {
- if (props.disabled || dragStatus.value != '') return;
- event.preventDefault()
- dragStatus.value = 'start'
- touch.start(event);
- const { startX, startY } = touch;
- const delta = getDelta(startX.value, startY.value)
- const total = calculateTrackLength()
- let value = format(delta / total * scope.value + props.min, props.min, props.max, props.step)
- if (isRange.value) {
- currentKey.value = tempValue.value.findIndex((it) : boolean => it == closest(tempValue.value, value))
- tempValue.value[currentKey.value] = value
- innerValue.value = tempValue.value
- } else {
- currentKey.value = 0;
- innerValue.value = value
- }
- startValue = value;
- }
- const touchmove = (event : UniTouchEvent) => {
- if (props.disabled) return;
- event.preventDefault()
- dragStatus.value = 'dragging'
- touch.move(event);
- const key = currentKey.value
- const delta = props.vertical ? touch.deltaY : touch.deltaX;
- const total = calculateTrackLength()
- const diff = delta.value / total * scope.value;
- const value = format(startValue + diff, props.min, props.max, props.step)
- tempValue.value[key] = value
- if (isRange.value) {
- innerValue.value = tempValue.value
- } else {
- innerValue.value = value
- }
- }
- const touchend = (event : UniTouchEvent) => {
- if (props.disabled) return;
- dragStatus.value = ''
- if (!isRange.value) {
- currentKey.value = 0
- } else {
- currentKey.value = -1
- }
- }
- const stopWatch = watch(innerValue, (v : any) => {
- // if(dragStatus.value == '') {
- if (isRange.value) {
- tempValue.value = (innerValue.value as number[]).map((value) : number => format(value, props.min, props.max, props.step))
- } else {
- tempValue.value[0] = format(innerValue.value as number, props.min, props.max, props.step)
- }
- // }
- updateProgressBarAndThumb()
- }, { immediate: true, deep: true })
- const resizeObserver = new UniResizeObserver((entries : Array<UniResizeObserverEntry>) => {
- updateProgressBarAndThumb()
- })
- const stopWrapWatch = watch(() : UniElement | null => sliderRef.value, (el : UniElement | null) => {
- if (el == null) return
- resizeObserver.observe(el)
- })
- onUnmounted(() => {
- stopWatch()
- stopWrapWatch()
- resizeObserver.disconnect()
- })
- </script>
- <style lang="scss">
- @import './index-u';
- </style>
|