2026-01-05 11:09:24 +08:00

198 lines
4.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="slider-wrapper" :style="wrapperStyle">
<div class="slider-inner" ref="containerRef" :style="innerStyle">
<div class="track-slot"></div>
<div
class="knob"
:style="knobStyle"
@mousedown="startDrag"
@touchstart.prevent="startDrag"
>
<div class="knob-face"></div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue';
const props = defineProps({
modelValue: {
type: Number,
default: 0
},
// 新增 scale 属性,默认为 1 (原始大小)
scale: {
type: Number,
default: 1.0
}
});
const emit = defineEmits(['update:modelValue', 'change']);
// --- 内部常量定义 (基于原始设计尺寸) ---
const ORIGINAL_WIDTH = 40; // 原始宽度 px
const ORIGINAL_HEIGHT = 150; // 原始高度 px
const STEPS = 4;
const STEP_DISTANCE = 40;
const KNOB_HEIGHT = 24;
const TRACK_PADDING_TOP = 2;
// --- 状态 ---
const currentIndex = ref(props.modelValue);
watch(() => props.modelValue, (newVal) => {
currentIndex.value = newVal;
});
// --- 样式计算 ---
// 1. 外层容器样式:计算缩放后实际占用的宽高
const wrapperStyle = computed(() => ({
width: `${ORIGINAL_WIDTH * props.scale}px`,
height: `${ORIGINAL_HEIGHT * props.scale}px`,
}));
// 2. 内层容器样式:固定原始尺寸,使用 scale 缩放
const innerStyle = computed(() => ({
width: `${ORIGINAL_WIDTH}px`,
height: `${ORIGINAL_HEIGHT}px`,
transform: `scale(${props.scale})`,
transformOrigin: 'top left' // 关键:从左上角开始缩放,确保与外层容器对齐
}));
// 3. 旋钮位置样式
const knobStyle = computed(() => {
const topOffset = TRACK_PADDING_TOP + (currentIndex.value * STEP_DISTANCE);
return {
transform: `translateY(${topOffset}px)`
};
});
// --- 拖拽逻辑 (含缩放修正) ---
let isDragging = false;
let startMouseY = 0;
let startKnobTop = 0;
const startDrag = (e) => {
isDragging = true;
startMouseY = e.clientY || e.touches[0].clientY;
startKnobTop = TRACK_PADDING_TOP + (currentIndex.value * STEP_DISTANCE);
window.addEventListener('mousemove', onDrag);
window.addEventListener('mouseup', stopDrag);
window.addEventListener('touchmove', onDrag, { passive: false });
window.addEventListener('touchend', stopDrag);
};
const onDrag = (e) => {
if (!isDragging) return;
if (e.cancelable) e.preventDefault();
const currentMouseY = e.clientY || e.touches[0].clientY;
// [关键修正]
// 鼠标在屏幕上移动的距离 (screen delta)
const screenDeltaY = currentMouseY - startMouseY;
// 转换为组件内部的距离 (internal delta)
// 比如:如果缩放是 0.5,屏幕移动 10px相当于内部移动了 20px
const internalDeltaY = screenDeltaY / props.scale;
let newRawTop = startKnobTop + internalDeltaY;
// 计算吸附
let relativeTop = newRawTop - TRACK_PADDING_TOP;
let newIndex = Math.round(relativeTop / STEP_DISTANCE);
const maxIndex = STEPS - 1;
if (newIndex < 0) newIndex = 0;
if (newIndex > maxIndex) newIndex = maxIndex;
if (newIndex !== currentIndex.value) {
currentIndex.value = newIndex;
emit('update:modelValue', newIndex);
emit('change', newIndex);
}
};
const stopDrag = () => {
isDragging = false;
window.removeEventListener('mousemove', onDrag);
window.removeEventListener('mouseup', stopDrag);
window.removeEventListener('touchmove', onDrag);
window.removeEventListener('touchend', stopDrag);
};
</script>
<style scoped>
/* 样式部分主要针对 slider-inner 内部
保持原本的尺寸定义即可,缩放由内联样式 style 处理
*/
.slider-wrapper {
/* 防止溢出裁剪阴影 */
overflow: visible;
/* 禁止选中 */
user-select: none;
touch-action: none;
/* 方便父级布局 */
display: inline-block;
vertical-align: middle;
}
.slider-inner {
position: relative;
/* 这里的宽高现在由 innerStyle 动态控制,但写上默认值是个好习惯 */
width: 40px;
height: 150px;
}
.track-slot {
position: absolute;
top: 0;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 8px;
background-color: #1a1a1a;
border-radius: 4px;
box-shadow:
inset 1px 1px 3px rgba(0,0,0,0.7),
inset -1px -1px 1px rgba(255,255,255,0.1);
}
.knob {
position: absolute;
top: 0;
left: 50%;
margin-left: -23px;
width: 46px;
height: 24px;
z-index: 10;
cursor: grab;
transition: transform 0.2s cubic-bezier(0.25, 1, 0.5, 1);
backface-visibility: hidden; /* 优化缩放后的渲染 */
}
.knob:active {
cursor: grabbing;
}
.knob-face {
width: 100%;
height: 100%;
background: linear-gradient(to bottom, #ffffff 0%, #e4e4e4 100%);
border-radius: 4px;
box-shadow:
inset 0 1px 0 rgba(255,255,255,1),
inset 0 -1px 0 rgba(0,0,0,0.1),
0 3px 6px rgba(0,0,0,0.35),
0 0 0 1px rgba(0,0,0,0.08);
}
</style>