先看图:
在这里插入图片描述

<template>
    <view class="v-slider">
        <view class="v-slider-wrapper">
            <view class="v-slider-tap-area">
                <view 
                    class="v-slider-handle-wrapper"
                    :style="`background-color: ${props.backgroundColor};`">
                    <view class="v-slider-step"></view>
                    <view 
                        class="v-slider-track"
                        :style="trackStyle">
                    </view>
                    <view 
                        class="v-slider-thumb thumb-left"
                        :style="`
                            background-color: ${props.blockColor};
                            width: ${thumbSize}rpx;
                            height: ${thumbSize}rpx;
                            margin-left: -${props.blockSize}rpx;
                            margin-top: -${props.blockSize}rpx;
                            left: ${thumbLeft};
                        `"
                        @touchmove="e => handleTouchMove(e, 0)"
                        @touchend="handleTouchEnd">
                    </view>
                    <view 
                        class="v-slider-thumb thumb-right"
                        :style="`
                            background-color: ${props.blockColor};
                            width: ${thumbSize}rpx;
                            height: ${thumbSize}rpx;
                            margin-right: -${props.blockSize}rpx;
                            margin-top: -${props.blockSize}rpx;
                            right: ${thumbRight};
                        `"
                        @touchmove="e => handleTouchMove(e, 1)"
                        @touchend="handleTouchEnd">
                    </view>
                </view>
            </view>
        </view>
        <view 
            v-if="props.showRange"
            class="v-slider-range">
            <text 
                v-for="item of rangeList" 
                :key="item" 
                class="v-slider-range-item"
                :style="`left: ${item.left}%;`">
                <text class="text">{{ item.value }}</text>
            </text>
        </view>
    </view>
</template>
<script lang="ts" setup>
    import { ref, shallowRef, withDefaults, defineProps, defineEmits, watch, computed, onMounted } from 'vue'
	type TypeOjbect = Record<string, any>
    type ModelValue= [number, number]
    interface UniProps { 
        readonly min?: number,              // 最小值, 默认 0
        readonly max?: number,              // 最大值, 默认 100
        readonly step?: number,             // 步长,取值必须大于 0,并且可被(max - min)整除
        readonly modelValue?: ModelValue,   // 当前取值, 默认[0, 100]
        readonly backgroundColor?: string,  // 背景条颜色, 默认 #e9e9e9
        readonly activeColor?: string,      // 已选择的颜色, 默认 #1aad19
        readonly blockSize?: number,        // 滑块的大小, 默认为28, 取值范围为 12 - 28
        readonly blockColor?: string,       // 滑块的颜色, 默认 #ffffff
        readonly showRange?: boolean,       // 是否显示范围列表
        readonly rangeStep?: number         // 范围列表步阶
    }
    interface Emits {
        (event: 'update:modelValue', val: ModelValue): void, // eslint-disable-line
        // 完成一次拖动后触发的事件,event.detail = {value}
        (event: 'change', val: ModelValue): void // eslint-disable-line
        // 拖动过程中触发的事件,event.detail = {value}
        (event: 'changing', val: ModelValue): void // eslint-disable-line
    }

    // 属性初始值
    const props = withDefaults(defineProps<UniProps>(), {
        step: 1,
        backgroundColor: '#e9e9e9',
        activeColor: '#1aad19',
        blockSize: 28,
        blockColor: '#ffffff',
        showRange: true,
        rangeStep: 5
    })
    const emits = defineEmits<Emits>()
    const rangeValue = ref<ModelValue>([0, 100])
    const [min, max] = [shallowRef<number>(0), shallowRef<number>(100)]
    const { pixelRatio } = getApp().globalData
    const checkVal = function(val: number):number {
        if (val > max.value) {
            val = max.value
        } else if (val < min.value) {
            val = min.value
        }
        return val
    }

    // 监听范围最大、最小值
    watch<[number, number], true>(
        () => [props.min, props.max], 
        ([minVal, maxVal]) => {
            if (minVal) min.value = minVal
            if (maxVal) max.value = maxVal
        },
    	{
    	    immediate: true
    	}
    )
    
    // 监听范围值
	watch<ModelValue, true>(
    	() => props.modelValue,
    	val => {
            if (val) {
                let newVal = [...val]
                newVal = newVal.map(v => checkVal(v))
			    rangeValue.value = newVal
            }
    	},
    	{
    	    immediate: true
    	}
	)

    // 获取背景条长度
    let handleLine: TypeObject
    const getLineWidth = function():void {
        const query = uni.createSelectorQuery()
        query.select(".v-slider-handle-wrapper").boundingClientRect((e: DOMRect) => {
            const { left, width, height, right } = e
            handleLine = { left, width, height, right }
	    })
        query.exec()
    }

    // 范围差
    const rangeDiff = computed<number>(() => max.value - min.value)

    // 范围列表
    const rangeList = computed<TypeObject[]>(() => {
        const [minVal, maxVal] = [min.value, max.value]
        const step = (maxVal - minVal) / props.rangeStep
        const range = []
        for (let i = 0; i <= props.rangeStep; i++) {
            const value = Math.ceil(minVal + i * step)
            const left = (value - minVal) * 100 / rangeDiff.value
            range.push({ value, left })
        }
        return range
    })

    // 尺寸
    const thumbSize = computed<number>(() => props.blockSize * pixelRatio)

    // 获取百分比
    const getPercent = function(diff: number): string {
        if (diff < 0) diff = 0
        return Math.floor(diff * 100 / rangeDiff.value) + '%'
    }

    // 滑块thumb左侧浮动值
    const thumbLeft = computed<string>(() => {
        return getPercent(rangeValue.value[0] - min.value)
    })
    // 滑块thumb右侧浮动值
    const thumbRight = computed<string>(() => {
        return getPercent(max.value - rangeValue.value[1])
    })

    // 滑块轨道track css 样式
    const trackStyle = computed<string>(() => {
        const start = rangeValue.value[0]
        const end = rangeValue.value[1]
        const options = {
            "background-color": props.activeColor,
            "width": getPercent(Math.abs(rangeValue.value[1] - rangeValue.value[0]))
        } as TypeObject
        if (start <= end) {
            options.left = getPercent(start - min.value)
        } else {
            options.left = getPercent(end - min.value)
        }
        let style = ''
        Object.entries(options).forEach(option => {
            style += `${option[0]}: ${option[1]};`
        })
        return style
    })

    // 当前取值
    let timer:ReturnType<typeof setTimeout>
    const handleTouchMove = function(e: TouchEvent, index: number):void {
        if (timer) clearTimeout(timer)
        // 防抖
        timer = setTimeout(function() {
            const { pageX } = e.touches[0]
            const { left, width } = handleLine
            let value = (pageX - left) * rangeDiff.value / width  + min.value
            if (value % props.step > 0) {
                value = Math.round(value / props.step) * props.step
            }
            if (value < min.value) {
                value = min.value
            } else if (value > max.value) {
                value = max.value
            }
            rangeValue.value[index] = Math.floor(value * 100) / 100
            emits('update:modelValue', rangeValue.value)
            emits('changing', rangeValue.value)
        }, 10)
    }

    // 滑动停止事件
    const handleTouchEnd = function():void {
        emits('change', rangeValue.value)
    }
    onMounted(() => {
        getLineWidth()
    })
</script>
<style lang="scss">
    .v-slider {
        margin: 16rpx 28rpx;
        padding: 0;
        display: block;
        width: 100%;
        .v-slider-wrapper {
            display: -webkit-flex;
            display: flex;
            -webkit-align-items: center;
            align-items: center;
            min-height: 16px;
        }
        .v-slider-tap-area {
            -webkit-flex: 1;
            flex: 1;
            padding: 8px 0;
        }
        .v-slider-handle-wrapper {
            position: relative;
            z-index: 0;
            height: 2px;
            border-radius: 5px;
            cursor: pointer;
            transition: background-color 0.3s ease;
            -webkit-tap-highlight-color: transparent;
        }
        .v-slider-thumb {
            z-index: 2;
            box-shadow: 0 0 4px rgb(0 0 0 / 20%);
            position: absolute;
            top: 50%;
            cursor: pointer;
            border-radius: 50%;
            transition: border-color 0.3s ease;
        }
        .v-slider-step {
            position: absolute;
            width: 100%;
            height: 2px;
            background: transparent;
            z-index: 1;
        }
        .v-slider-track {
            height: 100%;
            border-radius: 6px;
            transition: background-color 0.3s ease;
            position: absolute;
            z-index: 1;
        }
        .v-slider-range {
            width: 100%;
            margin: 24rpx 0 0;
            padding-bottom: 20rpx;
            position: relative;
            .v-slider-range-item {
                text-align: center;
                font-size: 28rpx;
                position: absolute;
                z-index: 1;
                .text {
                    width: 80rpx;
                    position: absolute;
                    z-index: 2;
                    left: -40rpx;
                }
            }
        }
    }
</style>
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐