Vue 自定义滑块组件

由于项目需求,需要自定义滑块组件,支持以下功能:

支持鼠标和触摸操作

支持拖拽控制

支持步长设置

支持禁用状态

响应式更新

支持事件触发(startDrag、input、change、stopDrag)

以下为示例:

template部分:

html 复制代码
<template>
  <!-- 滑块容器,绑定鼠标按下和触摸开始事件 -->
  <div ref="sliderRef" class="slider" @mousedown="startDrag" @touchstart="startDrag">
    <!-- 滑块轨道,根据滑块位置动态设置宽度 -->
    <div class="slider-track" :style="{ width: trackWidth + '%' }"></div>
    <!-- 滑块按钮,显示当前值的位置,拖拽时添加特殊样式 -->
    <div
      ref="thumb"
      class="slider-thumb"
      :class="{ 'is-dragging': state.dragging }"
      :style="{ left: state.thumbLeft + '%' }"
    ></div>
  </div>
</template>

js部分:

javascript 复制代码
<script setup>
import { ref, reactive, watch, computed } from 'vue'

// 滑块容器的引用,用于获取DOM元素的位置信息
const sliderRef = ref(null)

// 定义组件属性
const props = defineProps({
  // 当前滑块的值
  value: {
    type: Number,
    default: 0,
  },
  // 滑块最小值
  min: {
    type: Number,
    default: 0,
  },
  // 滑块最大值
  max: {
    type: Number,
    default: 100,
  },
  // 步长,每次移动的最小单位
  step: {
    type: Number,
    default: 1,
  },
  // 是否禁用滑块
  disabled: {
    type: Boolean,
    default: false,
  },
})

// 计算轨道宽度,等于滑块按钮的左边距百分比
const trackWidth = computed(() => {
  return state.thumbLeft
})

// 根据props.value计算滑块按钮应该在的左边距百分比
const calculateThumbLeft = computed(() => {
  // 将value限制在min和max范围内
  const clampedValue = Math.max(props.min, Math.min(props.value, props.max))

  // 将值转换为百分比位置
  return ((clampedValue - props.min) / (props.max - props.min)) * 100
})

// 滑块组件的状态
const state = reactive({
  dragging: false, // 是否正在拖拽
  thumbLeft: calculateThumbLeft.value, // 滑块按钮的左边距百分比
})

// 定义组件触发的事件
const emit = defineEmits(['startDrag', 'input', 'change', 'stopDrag'])

// 监听props.value的变化,更新滑块位置
watch(
  () => props.value,
  (newVal, oldVal) => {
    if (newVal !== oldVal) {
      state.thumbLeft = calculateThumbLeft.value
    }
  },
)

/**
 * 根据滑块位置计算对应的值
 * @param {number} thumbLeft - 滑块的左边距百分比
 * @returns {number} 对应的值
 */
const calculateValue = (thumbLeft) => {
  // 将百分比转换为实际值
  const value = props.min + (thumbLeft / 100) * (props.max - props.min)
  // 按照步长取整
  const stepValue = Math.round(value / props.step) * props.step
  // 将值限制在min和max范围内
  return Math.max(props.min, Math.min(stepValue, props.max))
}

/**
 * 开始拖拽事件处理函数
 * @param {Event} event - 鼠标按下或触摸开始事件
 */
const startDrag = (event) => {
  // 如果滑块被禁用,则不执行拖拽操作
  if (props.disabled) return

  // 设置拖拽状态为true
  state.dragging = true

  // 添加鼠标移动和鼠标松开事件监听器
  document.addEventListener('mousemove', onDrag)
  document.addEventListener('mouseup', stopDrag)

  // 添加触摸移动和触摸结束事件监听器(移动端支持)
  document.addEventListener('touchmove', onDrag)
  document.addEventListener('touchend', stopDrag)

  // 阻止默认行为,防止选中文本
  event.preventDefault()

  // 触发startDrag事件
  emit('startDrag')
}

/**
 * 拖拽过程中的事件处理函数
 * @param {Event} event - 鼠标移动或触摸移动事件
 */
const onDrag = (event) => {
  // 如果没有在拖拽状态,则直接返回
  if (!state.dragging) return

  if (sliderRef.value) {
    // 获取滑块容器的位置和尺寸信息
    const sliderRect = sliderRef.value.getBoundingClientRect()

    // 获取鼠标或触摸点的X坐标
    const clientX = event.clientX || event.touches[0].clientX

    // 计算滑块按钮的新位置(百分比)
    let newLeft = ((clientX - sliderRect.left) / sliderRect.width) * 100

    // 将位置限制在0%到100%之间
    newLeft = Math.max(0, Math.min(newLeft, 100))

    // 更新滑块按钮的位置
    state.thumbLeft = newLeft

    // 根据新位置计算对应的值并触发input事件
    const newValue = calculateValue(newLeft)
    emit('input', newValue)
  }
}

/**
 * 停止拖拽事件处理函数
 */
const stopDrag = () => {
  // 设置拖拽状态为false
  state.dragging = false

  // 移除事件监听器
  document.removeEventListener('mousemove', onDrag)
  document.removeEventListener('mouseup', stopDrag)
  document.removeEventListener('touchmove', onDrag)
  document.removeEventListener('touchend', stopDrag)

  // 触发change事件,传递最终值
  emit('change', calculateValue(state.thumbLeft))

  // 触发stopDrag事件
  emit('stopDrag')
}
</script>

css部分:

css 复制代码
<style scoped>
/* 滑块容器基础样式 */
.slider {
  position: relative;
  width: 200px;
  height: 3px;
  background: rgba(0, 0, 0, 0.3);
  user-select: none;
  opacity: 1;
  transition: opacity 0.3s;
  box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.3);
  border-radius: 2px;
}

/* 禁用状态下的滑块样式 */
.slider.disabled {
  opacity: 0.5;
}

/* 滑块轨道样式 */
.slider-track {
  position: absolute;
  height: 3px;
  background: rgba(0, 0, 0, 0.3);
  border-radius: 2px;
}

/* 滑块按钮样式 */
.slider-thumb {
  position: absolute;
  top: -6.5px;
  width: 16px;
  height: 16px;
  background: rgb(255, 0, 0);
  box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.3);
  border-radius: 50%;
  user-select: none;
  transform: translateX(-50%);
  transition:
    width 0.1s ease,
    height 0.1s ease,
    top 0.1s ease,
    box-shadow 0.1s ease;

  /* 拖拽状态下的样式 */
  &.is-dragging {
    box-shadow: 0 0 0 4px rgba(255, 0, 0, 0.3);
  }
}
</style>

最终效果:

(初始状态)

(拖动状态)

以上部分也可以根据实际需求进行修改。

相关推荐
时光少年1 天前
ExoPlayer MediaCodec视频解码Buffer模式GPU渲染加速
前端
华仔啊1 天前
JavaScript 中如何正确判断 null 和 undefined?
前端·javascript
weibkreuz1 天前
函数柯里化@11
前端·javascript·react.js
king王一帅1 天前
Incremark 0.3.0 发布:双引擎架构 + 完整插件生态,AI 流式渲染的终极方案
前端·人工智能·开源
转转技术团队1 天前
HLS 流媒体技术:畅享高清视频,忘却 MP4 卡顿的烦恼!
前端
程序员的程1 天前
我做了一个前端股票行情 SDK:stock-sdk(浏览器和 Node 都能跑)
前端·npm·github
KlayPeter1 天前
前端数据存储全解析:localStorage、sessionStorage 与 Cookie
开发语言·前端·javascript·vue.js·缓存·前端框架
沉默-_-1 天前
从小程序前端到Spring后端:新手上路必须理清的核心概念图
java·前端·后端·spring·微信小程序
裴嘉靖1 天前
前端获取二进制文件并预览的完整指南
前端·pdf