423 lines
9.7 KiB
Vue
423 lines
9.7 KiB
Vue
<template>
|
|
<view class="wd-slider-wrapper">
|
|
<view class="wd-slider">
|
|
<view class="wd-slider-rail"></view>
|
|
<view
|
|
class="wd-slider-track"
|
|
:style="{
|
|
left: range ? `${leftPercentage}%` : '0%',
|
|
width: range ? `${rightPercentage - leftPercentage}%` : `${rightPercentage}%`,
|
|
backgroundColor: activeColor,
|
|
}"
|
|
></view>
|
|
<view
|
|
v-if="range"
|
|
class="wd-slider-thumb left-thumb"
|
|
:class="{ 'active-thumb': isLeftActive }"
|
|
:style="{
|
|
left: `${leftPercentage}%`,
|
|
transform: 'translate(-50%, -50%)',
|
|
borderColor: activeColor,
|
|
}"
|
|
@touchstart="handleLeftThumbTouchStart"
|
|
@touchmove="handleLeftThumbTouchMove"
|
|
@touchend="handleLeftThumbTouchEnd"
|
|
>
|
|
<text
|
|
v-if="showThumbValue || isLeftActive"
|
|
class="wd-slider-thumb-value-inner"
|
|
:class="{ 'active-value': isLeftActive }"
|
|
:style="{ color: computedThumbValueColor }"
|
|
>
|
|
{{ formatValue(currentLeftValue) }}
|
|
</text>
|
|
</view>
|
|
<view
|
|
class="wd-slider-thumb right-thumb"
|
|
:class="{ 'active-thumb': isRightActive }"
|
|
:style="{
|
|
left: `${rightPercentage}%`,
|
|
transform: 'translate(-50%, -50%)',
|
|
borderColor: activeColor,
|
|
}"
|
|
@touchstart="handleRightThumbTouchStart"
|
|
@touchmove="handleRightThumbTouchMove"
|
|
@touchend="handleRightThumbTouchEnd"
|
|
>
|
|
<text
|
|
v-if="showThumbValue || isRightActive"
|
|
class="wd-slider-thumb-value-inner"
|
|
:class="{ 'active-value': isRightActive }"
|
|
:style="{ color: computedThumbValueColor }"
|
|
>
|
|
{{ formatValue(currentRightValue) }}
|
|
</text>
|
|
</view>
|
|
</view>
|
|
<view v-if="showLabels" class="wd-slider-labels">
|
|
<text class="wd-slider-label left-label">{{ formatValue(currentLeftValue) }}</text>
|
|
<text class="wd-slider-label right-label">{{ formatValue(currentRightValue) }}</text>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
|
|
import { getCurrentInstance } from 'vue'
|
|
|
|
// 获取当前组件实例
|
|
const instance = getCurrentInstance()
|
|
|
|
const props = defineProps({
|
|
modelValue: {
|
|
type: Array,
|
|
default: () => [0, 0],
|
|
},
|
|
min: {
|
|
type: Number,
|
|
default: 0,
|
|
},
|
|
max: {
|
|
type: Number,
|
|
default: 100,
|
|
},
|
|
step: {
|
|
type: Number,
|
|
default: 1,
|
|
},
|
|
range: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
showLabels: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
showThumbValue: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
activeColor: {
|
|
type: String,
|
|
default: '#4e80ff',
|
|
},
|
|
thumbValueColor: {
|
|
type: String,
|
|
default: '',
|
|
},
|
|
disabled: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
formatValue: {
|
|
type: Function,
|
|
default: (value) => {
|
|
// 处理较小的值,确保显示合适的位数
|
|
if (typeof value === 'number') {
|
|
if (value < 10) {
|
|
return value.toFixed(1)
|
|
}
|
|
}
|
|
return value
|
|
},
|
|
},
|
|
})
|
|
|
|
const emit = defineEmits(['update:modelValue', 'change'])
|
|
|
|
// 当前值
|
|
const currentLeftValue = ref(props.range ? props.modelValue[0] : props.min)
|
|
const currentRightValue = ref(props.range ? props.modelValue[1] : props.modelValue[0])
|
|
|
|
// 控制值显示状态
|
|
const isLeftActive = ref(false)
|
|
const isRightActive = ref(false)
|
|
|
|
// 轨道宽度
|
|
const railWidth = ref(0)
|
|
|
|
// 计算颜色值
|
|
const computedThumbValueColor = computed(() => {
|
|
return props.thumbValueColor || props.activeColor
|
|
})
|
|
|
|
// 初始化时获取轨道宽度
|
|
const initRailWidth = () => {
|
|
setTimeout(() => {
|
|
getRailWidth().then((width) => {
|
|
railWidth.value = width
|
|
})
|
|
}, 100) // 延迟一些时间确保DOM已渲染
|
|
}
|
|
|
|
// 在组件挂载后获取轨道宽度
|
|
onMounted(() => {
|
|
initRailWidth()
|
|
|
|
// 添加窗口大小改变的监听
|
|
uni.onWindowResize(initRailWidth)
|
|
})
|
|
|
|
// 在组件销毁前移除监听
|
|
onBeforeUnmount(() => {
|
|
uni.offWindowResize(initRailWidth)
|
|
})
|
|
|
|
// 计算滑块位置的百分比
|
|
const leftPercentage = computed(() => {
|
|
return ((currentLeftValue.value - props.min) / (props.max - props.min)) * 100
|
|
})
|
|
|
|
const rightPercentage = computed(() => {
|
|
return ((currentRightValue.value - props.min) / (props.max - props.min)) * 100
|
|
})
|
|
|
|
// 监听外部值变化
|
|
watch(
|
|
() => props.modelValue,
|
|
(newVal) => {
|
|
if (props.range) {
|
|
currentLeftValue.value = newVal[0]
|
|
currentRightValue.value = newVal[1]
|
|
} else {
|
|
currentRightValue.value = newVal[0]
|
|
}
|
|
},
|
|
{ immediate: true, deep: true },
|
|
)
|
|
|
|
// 根据位置计算值
|
|
const calculateValueFromPosition = (position, width) => {
|
|
const percentage = Math.max(0, Math.min(100, (position / width) * 100))
|
|
const rawValue = (percentage / 100) * (props.max - props.min) + props.min
|
|
|
|
// 根据步长调整值
|
|
const steps = Math.round((rawValue - props.min) / props.step)
|
|
return props.min + steps * props.step
|
|
}
|
|
|
|
// 触摸事件处理
|
|
let startX = 0
|
|
let startLeftValue = 0
|
|
let startRightValue = 0
|
|
let isDragging = false
|
|
|
|
// 获取滑块轨道宽度
|
|
const getRailWidth = () => {
|
|
return new Promise((resolve) => {
|
|
const query = uni.createSelectorQuery()
|
|
if (instance) {
|
|
query.in(instance.proxy)
|
|
}
|
|
query
|
|
.select('.wd-slider-rail')
|
|
.boundingClientRect((data) => {
|
|
if (data) {
|
|
resolve(data.width)
|
|
} else {
|
|
resolve(0)
|
|
}
|
|
})
|
|
.exec()
|
|
})
|
|
}
|
|
|
|
const handleLeftThumbTouchStart = async (e) => {
|
|
if (props.disabled) return
|
|
|
|
isDragging = true
|
|
isLeftActive.value = true
|
|
startX = e.touches[0].clientX
|
|
startLeftValue = currentLeftValue.value
|
|
|
|
// 使用已存储的轨道宽度或重新获取
|
|
if (!railWidth.value) {
|
|
railWidth.value = await getRailWidth()
|
|
}
|
|
}
|
|
|
|
const handleLeftThumbTouchMove = (e) => {
|
|
if (!isDragging || props.disabled || !railWidth.value) return
|
|
|
|
const moveX = e.touches[0].clientX - startX
|
|
const movePercentage = (moveX / railWidth.value) * 100
|
|
const newLeftValue = calculateValueFromPosition(
|
|
((startLeftValue - props.min) / (props.max - props.min)) * railWidth.value + moveX,
|
|
railWidth.value,
|
|
)
|
|
|
|
// 确保左滑块不超过右滑块
|
|
if (newLeftValue <= currentRightValue.value) {
|
|
currentLeftValue.value = newLeftValue
|
|
emitValueChange()
|
|
}
|
|
}
|
|
|
|
const handleLeftThumbTouchEnd = () => {
|
|
if (props.disabled) return
|
|
|
|
isDragging = false
|
|
isLeftActive.value = false
|
|
emit(
|
|
'change',
|
|
props.range ? [currentLeftValue.value, currentRightValue.value] : [currentRightValue.value],
|
|
)
|
|
}
|
|
|
|
const handleRightThumbTouchStart = async (e) => {
|
|
if (props.disabled) return
|
|
|
|
isDragging = true
|
|
isRightActive.value = true
|
|
startX = e.touches[0].clientX
|
|
startRightValue = currentRightValue.value
|
|
|
|
// 使用已存储的轨道宽度或重新获取
|
|
if (!railWidth.value) {
|
|
railWidth.value = await getRailWidth()
|
|
}
|
|
}
|
|
|
|
const handleRightThumbTouchMove = (e) => {
|
|
if (!isDragging || props.disabled || !railWidth.value) return
|
|
|
|
const moveX = e.touches[0].clientX - startX
|
|
const movePercentage = (moveX / railWidth.value) * 100
|
|
const newRightValue = calculateValueFromPosition(
|
|
((startRightValue - props.min) / (props.max - props.min)) * railWidth.value + moveX,
|
|
railWidth.value,
|
|
)
|
|
|
|
// 确保右滑块不小于左滑块
|
|
if (props.range) {
|
|
if (newRightValue >= currentLeftValue.value) {
|
|
currentRightValue.value = newRightValue
|
|
emitValueChange()
|
|
}
|
|
} else {
|
|
currentRightValue.value = newRightValue
|
|
emitValueChange()
|
|
}
|
|
}
|
|
|
|
const handleRightThumbTouchEnd = () => {
|
|
if (props.disabled) return
|
|
|
|
isDragging = false
|
|
isRightActive.value = false
|
|
emit(
|
|
'change',
|
|
props.range ? [currentLeftValue.value, currentRightValue.value] : [currentRightValue.value],
|
|
)
|
|
}
|
|
|
|
// 发送值变化事件
|
|
const emitValueChange = () => {
|
|
emit(
|
|
'update:modelValue',
|
|
props.range ? [currentLeftValue.value, currentRightValue.value] : [currentRightValue.value],
|
|
)
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.wd-slider-wrapper {
|
|
box-sizing: border-box;
|
|
width: 100%;
|
|
}
|
|
|
|
.wd-slider {
|
|
position: relative;
|
|
height: 30px;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.wd-slider-rail {
|
|
position: absolute;
|
|
width: 100%;
|
|
height: 4px;
|
|
background-color: #e9e9e9;
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.wd-slider-track {
|
|
position: absolute;
|
|
height: 4px;
|
|
background-color: v-bind('activeColor');
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.wd-slider-thumb {
|
|
position: absolute;
|
|
|
|
width: 66rpx;
|
|
height: 42rpx;
|
|
border-radius: 12rpx 12rpx 12rpx 12rpx;
|
|
background-color: #fff;
|
|
|
|
border: 2px solid v-bind('activeColor');
|
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
|
z-index: 1;
|
|
top: 50%;
|
|
transition:
|
|
transform 0.2s,
|
|
width 0.2s,
|
|
height 0.2s,
|
|
background-color 0.2s;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
overflow: hidden;
|
|
user-select: none;
|
|
}
|
|
|
|
.active-thumb {
|
|
transform: translate(-50%, -50%) scale(0.7) !important;
|
|
z-index: 2;
|
|
border-color: v-bind('activeColor');
|
|
background-color: #f5f8ff;
|
|
width: 42px;
|
|
height: 42px;
|
|
}
|
|
|
|
.wd-slider-thumb-value-inner {
|
|
font-size: 12px;
|
|
color: v-bind('computedThumbValueColor');
|
|
text-align: center;
|
|
transition: all 0.2s;
|
|
line-height: 1;
|
|
font-weight: 500;
|
|
white-space: nowrap; /* 防止数字换行 */
|
|
padding: 0 2px;
|
|
}
|
|
|
|
.active-value {
|
|
font-weight: bold;
|
|
color: v-bind('computedThumbValueColor');
|
|
font-size: 13px;
|
|
}
|
|
|
|
.wd-slider-labels {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin-top: 10px;
|
|
font-size: 12px;
|
|
color: #666;
|
|
}
|
|
|
|
.wd-slider-label {
|
|
transform: translateX(-50%);
|
|
}
|
|
|
|
.left-label {
|
|
position: relative;
|
|
left: v-bind(leftPercentage + '%');
|
|
}
|
|
|
|
.right-label {
|
|
position: relative;
|
|
left: v-bind(rightPercentage + '%');
|
|
}
|
|
</style>
|