基于Uniapp + VUE3.0 + TS 实现一个支持双滑块的Slider滑块组件
基于Uniapp + VUE3.0 + TS 实现Slider 滑块组件支持双滑块
·
先看图:
<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>
更多推荐
已为社区贡献4条内容
所有评论(0)