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>

最终效果:

(初始状态)

(拖动状态)

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

相关推荐
LawrenceLan3 分钟前
Flutter 零基础入门(二十六):StatefulWidget 与状态更新 setState
开发语言·前端·flutter·dart
秋秋小事23 分钟前
TypeScript 模版字面量与类型操作
前端·typescript
2401_892000521 小时前
Flutter for OpenHarmony 猫咪管家App实战 - 添加提醒实现
前端·javascript·flutter
Yolanda941 小时前
【项目经验】vue h5移动端禁止缩放
前端·javascript·vue.js
VX:Fegn08952 小时前
计算机毕业设计|基于springboot + vue酒店管理系统(源码+数据库+文档)
vue.js·spring boot·课程设计
广州华水科技2 小时前
单北斗GNSS形变监测一体机在基础设施安全中的应用与技术优势
前端
EndingCoder2 小时前
案例研究:从 JavaScript 迁移到 TypeScript
开发语言·前端·javascript·性能优化·typescript
Irene19913 小时前
Vue3 中使用的命名规则 和 实际开发命名规范总结
vue.js·命名规范
阿珊和她的猫4 小时前
React 路由:构建单页面应用的导航系统
前端·react.js·状态模式
Amumu121384 小时前
Vue脚手架(二)
前端·javascript·vue.js