volunteer-4/src/pages-sub/components/Slider.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>