最终效果
完整代码:
css
<template>
<div class="slider-container">
<div class="slider-track" ref="trackRef">
<div
class="slider-thumb"
ref="thumbRef"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
:style="{ left: thumbPosition }"
></div>
<div class="slider-progress" :style="{ width: progressWidth }"></div>
<div
v-for="n in 10"
:key="n"
class="slider-mark"
:style="{ left: `${(n - 1) * 100 / 9}%` }"
></div>
<!-- 添加单独的点击区域 -->
<div
v-for="n in 10"
:key="'click-' + n"
class="slider-click-area"
:style="{ left: `${(n - 1) * 100 / 9}%` }"
@click="jumpToValue(n)"
@touchstart="jumpToValue(n)"
></div>
<div
v-for="n in 10"
:key="'label-' + n"
class="slider-label"
:style="{ left: `${(n - 1) * 100 / 9}%` }"
>
{{ n }}
</div>
</div>
<div class="slider-value">当前值: {{ currentValue }}</div>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
const trackRef = ref<HTMLDivElement | null>(null)
const thumbRef = ref<HTMLDivElement | null>(null)
const currentValue = ref(1)
const isDragging = ref(false)
const startX = ref(0)
const thumbStartLeft = ref(0)
const progressWidth = computed(() => {
return `${(currentValue.value - 1) * 100 / 9}%`
})
const thumbPosition = computed(() => {
return `${(currentValue.value - 1) * 100 / 9}%`
})
function handleTouchStart(e: TouchEvent) {
isDragging.value = true
startX.value = e.touches[0].clientX
const thumbRect = thumbRef.value?.getBoundingClientRect()
thumbStartLeft.value = thumbRect?.left || 0
e.preventDefault()
}
function handleTouchMove(e: TouchEvent) {
if (!isDragging.value) return
const trackRect = trackRef.value?.getBoundingClientRect()
const trackWidth = trackRect?.width || 0
let moveX = e.touches[0].clientX - startX.value
let newLeft = thumbStartLeft.value + moveX - (trackRect?.left || 0)
// 限制在轨道范围内
newLeft = Math.max(0, Math.min(newLeft, trackWidth))
// 计算最接近的刻度位置
const percent = newLeft / trackWidth
const closestValue = Math.round(percent * 9) + 1
// 只有当移动距离足够大时才更新值,避免抖动
if (Math.abs(percent - (currentValue.value - 1)/9) > 0.03) {
currentValue.value = Math.max(1, Math.min(10, closestValue))
}
e.preventDefault()
}
function handleTouchEnd() {
// 滑动结束时确保对准最近的刻度
currentValue.value = Math.round(currentValue.value)
isDragging.value = false
}
function jumpToValue(value: number) {
currentValue.value = value;
}
</script>
<style scoped>
.slider-container {
/* padding: 20px; */
width: 85%;
max-width: 500px;
margin: 0 auto;
}
.slider-track {
position: relative;
height: 4px;
background-color: #e0e0e0;
border-radius: 2px;
margin: 30px 0 40px;
}
.slider-progress {
position: absolute;
height: 100%;
background-color: #42b983;
border-radius: 2px;
pointer-events: none;
}
.slider-thumb {
position: absolute;
width: 24px;
height: 24px;
background-color: #42b983;
border-radius: 50%;
top: 50%;
transform: translate(-50%, -50%);
cursor: pointer;
z-index: 4; /* 确保在最上层 */
touch-action: none;
}
.slider-mark {
position: absolute;
width: 8px;
height: 8px;
background-color: #fff;
border: 2px solid #e0e0e0;
border-radius: 50%;
top: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
z-index: 1;
}
.slider-label {
position: absolute;
top: 20px;
transform: translateX(-50%);
font-size: 12px;
color: #666;
pointer-events: none; /* 标签本身不接受事件 */
z-index: 2;
user-select: none;
}
/* 新增的点击区域 */
.slider-click-area {
position: absolute;
width: 30px;
height: 40px;
top: -10px;
transform: translateX(-50%);
z-index: 3; /* 在标签和滑块之间 */
cursor: pointer;
}
</style>
使用:
html
<NumberSlider />
import NumberSlider from '@/components/m/numberSlider.vue';