日常学习开发记录-slider组件

news2025/4/16 16:16:33

日常学习开发记录-slider组件

  • 从零开始实现一个优雅的Slider滑块组件
    • 前言
    • 一、基础实现
      • 1. 组件结构设计
      • 2. 基础样式实现
      • 3. 基础交互实现
    • 二、功能增强
      • 1. 添加拖动功能
      • 2. 支持范围选择
      • 3. 添加垂直模式
    • 三、高级特性
      • 1. 键盘操作支持
      • 2. 禁用状态
    • 五、使用示例
    • 六、总结

从零开始实现一个优雅的Slider滑块组件

前言

在Web开发中,滑块组件是一个常见的UI控件,用于数值范围的选择。本文将带领大家从零开始实现一个类似Element UI的Slider组件,我们将采用渐进式开发的方式,从基础功能开始,逐步添加更多特性。

一、基础实现

1. 组件结构设计

首先,我们需要设计一个基础的滑块组件结构:

<template>
  <div class="my-slider">
    <div class="my-slider__runway">
      <div class="my-slider__bar"></div>
      <div class="my-slider__button-wrapper">
        <div class="my-slider__button"></div>
      </div>
    </div>
  </div>
</template>

这个结构包含:

  • my-slider: 组件容器
  • my-slider__runway: 滑块轨道
  • my-slider__bar: 已选择区域的进度条
  • my-slider__button-wrapper: 滑块按钮容器
  • my-slider__button: 可拖动的滑块按钮

2. 基础样式实现

<style lang="scss" scoped>
  .my-slider {
    width: 100%;
    height: 10px;
    cursor: pointer;
    &__runway {
      width: 100%;
      height: 100%;
      border-radius: 5px;
      background-color: #f0f0f0;
      position: relative;
      .my-slider__bar {
        position: absolute;
        top: 0;
        left: 0;
        height: 100%;
        border-radius: 5px;
      }
      .my-slider__button-wrapper {
        height: 36px;
        width: 36px;
        position: absolute;
        top: -13px;
        transform: translateX(-50%);
        display: flex;
        align-items: center;
        justify-content: center;
        .my-slider__button {
          height: 16px;
          width: 16px;
          border-radius: 50%;
          border: 2px solid #007bff;
          background-color: #fff;
          transition: transform 0.2s;
        }
        &:hover {
          cursor: grab;
          .my-slider__button {
            transform: scale(1.2);
          }
        }
      }
    }
  }
</style>

结果:
在这里插入图片描述

3. 基础交互实现

<template>
  <div class="my-slider" :class="{ disabled: disabled }">
    <div class="my-slider__runway" @click="handleSliderClick" ref="slider">
      <div class="my-slider__bar" :style="barStyle"></div>
      <div class="my-slider__button-wrapper" :class="{ disabled: disabled }" :style="wrapperStyle">
        <div class="my-slider__button"></div>
      </div>
    </div>
  </div>
</template>

<script>
  export default {
    name: 'MySlider',
    props: {
      min: {
        type: Number,
        default: 0,
      },
      max: {
        type: Number,
        default: 100,
      },
      value: {
        type: [Array, Number],
        default: 0,
      },
      disabled: {
        type: Boolean,
        default: false,
      },
      step: {
        type: Number,
        default: 1,
      },
    },
    data() {
      return {
        currentValue: this.value,
        sliderSize: 1, // 滑块大小
      }
    },
    computed: {
      // 滑块的样式,高亮展示已移动的区域(单个滑块-左侧,多个滑块-中间高亮)
      barStyle() {
        return {
          width: `${this.currentValue}%`,
          left: `0%`,
        }
      },
      wrapperStyle() {
        return {
          left: `${this.currentValue}%`,
        }
      },
      precision() {
        //确定 min、max 和 step 中最大的小数位数
        let precisions = [this.min, this.max, this.step].map(item => {
          let decimal = ('' + item).split('.')[1]
          return decimal ? decimal.length : 0
        })
        return Math.max.apply(null, precisions)
      },
    },
    mounted() {
      this.resetSliderSize()
    },
    methods: {
      handleSliderClick(event) {
        if (this.disabled) return
        const sliderOffsetLeft = this.$refs.slider.getBoundingClientRect().left
        this.setPosition(((event.clientX - sliderOffsetLeft) / this.sliderSize) * 100)
      },
      setPosition(percentage) {
        //percentage为百分比位置
        this.currentValue = this.min + ((this.max - this.min) * percentage) / 100
        //每步的步长 max 50 min 0 ,每步步长 100 / 50 = 2
        const lengthPerStep = 100 / ((this.max - this.min) / this.step)
        //根据当前滑块的百分比位置(percentage)和每一步的长度(lengthPerStep),计算出当前所在的步数(steps) 四舍五入
        const steps = Math.round(percentage / lengthPerStep)
        //当前显示值 步长 * 步数* 每步的步长+最小值
        let value = steps * lengthPerStep * (this.max - this.min) * 0.01 + this.min

        value = parseFloat(value.toFixed(this.precision))
        this.currentValue = value
        //this.$emit('update:value', this.currentValue)
        //v-model 默认监听的是 input 事件,而不是 update:value 事件
        this.$emit('input', this.currentValue)
      },
      resetSliderSize() {
        this.sliderSize = this.$refs.slider.offsetWidth
      },
    },
  }
</script>



结果:
在这里插入图片描述
实现思路:

1. 模板结构
外层容器:<div class="my-slider">,用于包裹整个滑块组件,支持根据 disabled 属性动态添加禁用样式。
滑道:<div class="my-slider__runway">,表示滑块的背景轨道,点击滑道可以快速定位滑块位置。
滑块高亮区域:<div class="my-slider__bar">,表示滑块已移动的区域,宽度根据 currentValue 动态计算。
滑块按钮:<div class="my-slider__button-wrapper">,包含一个圆形按钮,用于拖动滑块,支持禁用状态样式。
2. Props 属性
min:滑块的最小值,默认 0。
max:滑块的最大值,默认 100。
value:滑块的当前值,支持数字或数组类型,默认 0。
disabled:是否禁用滑块,默认 false。
step:滑块的步长,默认 13. 数据与计算属性
currentValue:滑块的当前值,初始值为 props.value。
sliderSize:滑道的宽度,用于计算滑块的百分比位置。
barStyle:计算滑块的样式,动态设置高亮区域的宽度和位置。
wrapperStyle:计算滑块按钮的样式,动态设置按钮的左侧位置。
precision:计算 min、max 和 step 中最大的小数位数,用于确保数值精度。
4. 方法
handleSliderClick(event):处理滑道点击事件,计算点击位置的百分比并设置滑块位置。
setPosition(percentage):根据百分比位置计算滑块的当前值,并触发 input 事件更新父组件的 v-model 绑定值。
resetSliderSize():在组件挂载时重置滑道的宽度。
5. 样式
滑道:灰色背景,圆角矩形。
高亮区域:蓝色背景,表示滑块已移动的区域。
滑块按钮:圆形按钮,支持悬停放大效果,禁用状态下变为灰色。
禁用状态:滑道和高亮区域变为灰色,按钮不可拖动。
6. 交互逻辑
点击滑道:快速定位滑块到点击位置。
拖动滑块:通过 setPosition 方法动态更新滑块位置,并触发 input 事件。
步长控制:根据 step 属性调整滑块的移动步长,确保滑块位置符合步长要求。
禁用状态:当 disabled 为 true 时,禁止所有交互操作。
7. 事件
input 事件:当滑块值发生变化时触发,用于实现 v-model 双向绑定。

主要是在于动态style的计算达到视觉上的效果。

二、功能增强

1. 添加拖动功能

<template>
  <div class="my-slider" :class="{ disabled: disabled }">
    <div class="my-slider__runway" @click="handleSliderClick" ref="slider">
      <div class="my-slider__bar" :style="barStyle"></div>
      <div
        class="my-slider__button-wrapper"
        :class="{ disabled: disabled, dragging: dragging }"
        :style="wrapperStyle"
        @mousedown="onButtonDown"
        @touchstart="onButtonDown"
        ref="button"
      >
        <div class="my-slider__button"></div>
      </div>
    </div>
  </div>
</template>

<script>
  export default {
    name: 'MySlider',
    ///
    data() {
      return {
        currentValue: this.value, // 当前值
        sliderSize: 1, // 滑块大小
        dragging: false, // 是否正在拖拽
        startX: 0, // 开始拖拽时的 x 坐标
        currentX: 0, // 当前拖拽时的 x 坐标
        startPosition: 0, // 开始拖拽时的位置
        newPosition: null, // 新位置
        oldValue: this.value, // 旧值
      }
    },
    computed: {
      // 滑块的样式,高亮展示已移动的区域(单个滑块-左侧,多个滑块-中间高亮)
      barStyle() {
        return {
          width: `${this.currentValue}%`,
          left: `0%`,
        }
      },
      wrapperStyle() {
        return {
          left: `${this.currentValue}%`,
        }
      },
      precision() {
        //确定 min、max 和 step 中最大的小数位数
        let precisions = [this.min, this.max, this.step].map(item => {
          let decimal = ('' + item).split('.')[1]
          return decimal ? decimal.length : 0
        })
        return Math.max.apply(null, precisions)
      },
    },
    watch: {
      value(val) {
        this.currentValue = val
      },
    },
    mounted() {
      this.resetSliderSize()
    },
    methods: {
      /**
       * 点击滑块
       * @param {Event} event - 事件对象
       */
      handleSliderClick(event) {
        if (this.disabled) return
        // 防止点击滑块按钮时触发
        if (this.$refs.button && this.$refs.button.contains(event.target)) {
          return
        }
        const sliderOffsetLeft = this.$refs.slider.getBoundingClientRect().left
        this.setPosition(((event.clientX - sliderOffsetLeft) / this.sliderSize) * 100)
        this.emitChange()
      },
      onButtonDown(event) {
        if (this.disabled) return
        event.preventDefault() // 阻止默认行为
        this.dragging = true // 标记开始拖动

        // 处理触屏事件
        if (event.type === 'touchstart') {
          event.clientX = event.touches[0].clientX
        }

        // 记录初始位置
        this.startX = event.clientX
        this.startPosition = parseFloat(this.currentValue)
        this.newPosition = this.startPosition

        // 添加全局事件监听
        window.addEventListener('mousemove', this.onDragging)
        window.addEventListener('touchmove', this.onDragging)
        window.addEventListener('mouseup', this.onDragEnd)
        window.addEventListener('touchend', this.onDragEnd)
        window.addEventListener('contextmenu', this.onDragEnd)

        this.resetSliderSize() // 重新计算滑块尺寸
      },
      /**
       * 拖拽中
       */
      onDragging(event) {
        if (this.dragging) {
          // 获取当前鼠标位置
          let clientX
          if (event.type === 'touchmove') {
            clientX = event.touches[0].clientX
          } else {
            clientX = event.clientX
          }

          // 计算移动距离并转换为百分比
          const diff = ((clientX - this.startX) / this.sliderSize) * 100
          // 计算新位置
          this.newPosition = this.startPosition + diff
          // 更新滑块位置
          this.setPosition(this.newPosition)
        }
      },
      /**
       * 拖拽结束
       */
      onDragEnd() {
        if (this.dragging) {
          // 使用setTimeout确保在mouseup事件之后执行
          setTimeout(() => {
            this.dragging = false
            this.setPosition(this.newPosition)
            this.emitChange() // 触发change事件
          }, 0)

          // 移除所有事件监听
          window.removeEventListener('mousemove', this.onDragging)
          window.removeEventListener('touchmove', this.onDragging)
          window.removeEventListener('mouseup', this.onDragEnd)
          window.removeEventListener('touchend', this.onDragEnd)
          window.removeEventListener('contextmenu', this.onDragEnd)
        }
      },
      /**
       * 设置滑块位置
       * @param {number} position - 滑块位置 0-100
       */
      setPosition(position) {
        if (position === null || isNaN(position)) return
        if (position < 0) {
          position = 0
        } else if (position > 100) {
          position = 100
        }

        //每步的步长 max 50 min 0 ,每步步长 100 / 50 = 2
        const lengthPerStep = 100 / ((this.max - this.min) / this.step)
        //根据当前滑块的百分比位置(percentage)和每一步的长度(lengthPerStep),计算出当前所在的步数(steps) 四舍五入
        const steps = Math.round(position / lengthPerStep)
        //当前显示值 步长 * 步数* 每步的步长+最小值
        let value = steps * lengthPerStep * (this.max - this.min) * 0.01 + this.min

        value = parseFloat(value.toFixed(this.precision))
        this.currentValue = value

        // 更新 v-model 绑定值,但不触发 change 事件
        this.$emit('input', this.currentValue)
      },
      emitChange() {
        // 拖动结束时触发 change 事件
        this.$emit('change', this.currentValue)
      },
      resetSliderSize() {
        this.sliderSize = this.$refs.slider.offsetWidth
      },
    },
  }
</script>



效果:
在这里插入图片描述

实现思路:

使用 mousedown/touchstart 开始拖动
使用 mousemove/touchmove 处理拖动过程
使用 mouseup/touchend 结束拖动

2. 支持范围选择

添加range方法,重点是拖动至重合时候的处理,要记住当前拖动的是哪一个滑块

   // 判断当前点击的是哪个滑块
        const target = event.target.closest('.my-slider__button-wrapper')
        if (target === this.$refs.button) {
          this.startPosition = this.firstValue
          this.currentSlider = 'first'
        } else if (target === this.$refs.button1) {
          this.startPosition = this.secondValue
          this.currentSlider = 'second'
        }

3. 添加垂直模式

通过prop属性vertical来判断是否开启垂直模式
在这里插入图片描述

三、高级特性

1. 键盘操作支持

@keydown.left,@keydown.right, @keydown.up,@keydown.down,根据键盘方向事件,更新调用setposition方法直接更新滑块位置

2. 禁用状态

.my-slider {
  &.is-disabled {
    cursor: not-allowed;
    opacity: 0.6;
    
    .my-slider__button-wrapper {
      cursor: not-allowed;
    }
  }
}

五、使用示例

最后实现的效果:

在这里插入图片描述

六、总结

通过这个渐进式的实现过程,我们完成了一个功能完整的Slider组件。主要特点包括:

  1. 基础功能:

    • 单滑块/双滑块支持
    • 自定义数值范围
    • 平滑的拖动效果
  2. 增强功能:

    • 刻度标记
    • 禁用状态
  3. 高级特性:

    • 键盘操作支持
    • 垂直模式
    • 自定义格式化
  4. 性能优化:

    • 防抖处理
    • 计算属性缓存

这个实现不仅满足了基本需求,还考虑到了用户体验、可访问性和性能优化等多个方面。通过这样的渐进式开发,我们可以确保每一步都有坚实的基础,同时逐步增加功能复杂度。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2336075.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Windows 系统如何使用Redis 服务

前言 在学习过程中&#xff0c;我们长期接触到的是Mysql 关系型数据库&#xff0c;也是够我们平时练习项目用的&#xff0c;但是后面肯定会有大型数据的访问就要借助新的新的工具。 一、什么是Redis Redis&#xff08;Remote Dictionary Server&#xff09;是一个基于内存的 键…

【unity游戏开发入门到精通——UGUI】CanvasScaler画布缩放器组件

注意&#xff1a;考虑到UGUI的内容比较多&#xff0c;我将UGUI的内容分开&#xff0c;并全部整合放在【unity游戏开发——UGUI】专栏里&#xff0c;感兴趣的小伙伴可以前往逐一查看学习。 文章目录 一、CanvasScaler画布缩放器组件是什么二、CanvasScaler的三种适配模式1、Cons…

Hugging Face 模型:AI 模型的“拥抱”与开源革命!!!

&#x1f310; Hugging Face 模型&#xff1a;AI 模型的“拥抱”与开源革命 用表情符号、图表和代码&#xff0c;探索开源模型生态的底层逻辑与应用场景&#xff01; &#x1f31f; 名字由来&#xff1a;为什么叫 Hugging Face&#xff1f; “Hugging”&#xff1a;象征 开放…

关于 人工智能(AI)发展简史 的详细梳理,按时间阶段划分,涵盖关键里程碑、技术突破、重要人物及挑战

以下是关于 人工智能&#xff08;AI&#xff09;发展简史 的详细梳理&#xff0c;按时间阶段划分&#xff0c;涵盖关键里程碑、技术突破、重要人物及挑战&#xff1a; 字数&#xff1a;约2500字 逻辑结构&#xff1a;时间线清晰&#xff0c;分阶段描述技术突破、关键事件与挑战…

微服务即时通信系统---(四)框架学习

目录 ElasticSearch 介绍 安装 安装kibana ES客户端安装 头文件包含和编译时链接库 ES核心概念 索引(Index) 类型(Type) 字段(Field) 映射(mapping) 文档(document) ES对比MySQL Kibana访问ES测试 创建索引库 新增数据 查看并搜索数据 删除索引 ES…

Android查看依赖树的方法,简单有效

一、使用命令打印 在工具栏“Terminal”中输入以下命令&#xff0c;即可打印依赖树信息 gradlew xxxx:dependencies (“xxxx”为module名称)二、工具栏双击打印 右侧“Gradle”工具栏打开按下图顺序依次查找到“dependencies”&#xff0c;双击后依赖树就会在控制台中打印出…

GitHub配置密钥

1.生成SSH密钥 1&#xff09;检查 SSH 密钥是否存在 首先&#xff0c;确认是否已经在本地系统中生成了 SSH 密钥对。可以通过以下命令检查&#xff1a; ls -al ~/.ssh 在命令输出中&#xff0c;应该能看到类似 id_rsa 和 id_rsa.pub 这样一对文件。如果这些文件不存在&#…

【2-10】E1与T1

前言 之前我们简单介绍了人类从电话线思维到如今的数据报分组交换思维过渡时期的各种技术产物&#xff0c;今天我们重点介绍 E1/T1技术。 文章目录 前言1. 产生背景2. T13. E14. SONET4.1 OC-14.2 OC-3 及其它 5. SDH5.1. STM-1 6. SONET VS SDH后记修改记录 1. 产生背景 E1/…

【设计模式】适配器模式:让不兼容的接口和谐共处

引言 在软件开发中&#xff0c;我们经常会遇到这样的情况&#xff1a;两个已经存在的接口无法直接协同工作&#xff0c;但我们又希望它们能够无缝对接。这时&#xff0c;适配器模式就派上用场了。适配器模式&#xff08;Adapter Pattern&#xff09;是一种结构型设计模式&…

Pandas进行数据预处理(标准化数据)③

数据标准化处理代码解析 数据标准化处理代码解析课前预习1. 离差标准化&#xff08;Min - Max Scaling&#xff09;结果2. 标准差标准化&#xff08;Standard Scaling&#xff09;结果3. 小数定标标准化&#xff08;Decimal Scaling&#xff09;结果 代码整体概述代码详细解析1…

基于uniapp 实现画板签字

直接上效果图 代码 <template><view class"container"><!-- 签名画布 --><view class"canvas-container"><canvas canvas-id"signCanvas" class"sign-canvas"touchstart"handleTouchStart"touc…

JDBC 初认识、速了解

目录 一. JDBC的简介 1. 数据的持久化 2. 什么是JDBC 二. JDBC中常用的类和接口 1. Driver 接口 2. DriverManager 类 3. Connection 接口 4. Statement 接口 5. PreparedStatement接口 6. ResultSet 接口 三. 总结 前言 从现在开始就来讲解JDBC的相关知识了 本文的…

(2025亲测可用)Chatbox多端一键配置Claude/GPT/DeepSeek-网页端配置

1. 资源准备 API Key&#xff1a;此项配置填写在一步API官网创建API令牌&#xff0c;一键直达API令牌创建页面创建API令牌步骤请参考API Key的获取和使用API Host&#xff1a;此项配置填写https://yibuapi.com/v1查看支持的模型请参考这篇教程模型在线查询 2. ChatBox网页版配…

4.vtk光照vtkLight

文章目录 VTK中的光照1. vtkLight 的两种类型&#xff1a;位置光照和方向光照2. vtkLight 的常用方法3. 方法命名风格4. vtkProp 的可见性与 vtkLight 的开关 示例 VTK中的光照 vtkLight: 用于定义一个或多个光源。每个光源可以有其颜色、位置、焦点等属性。 vtkActor: 每个vtk…

YOLOv2学习笔记

YOLOv2 背景 YOLOv2是YOLO的第二个版本&#xff0c;其目标是显著提高准确性&#xff0c;同时使其更快 相关改进&#xff1a; 添加了BN层——Batch Norm采用更高分辨率的网络进行分类主干网络的训练 Hi-res classifier去除了全连接层&#xff0c;采用卷积层进行模型的输出&a…

【YOLOv8改进 - 卷积Conv】PConv(Pinwheel-shaped Conv): 风车状卷积用于红外小目标检测, 复现!

YOLOv8目标检测创新改进与实战案例专栏 专栏目录: YOLOv8有效改进系列及项目实战目录 包含卷积,主干 注意力,检测头等创新机制 以及 各种目标检测分割项目实战案例 专栏链接: YOLOv8基础解析+创新改进+实战案例 文章目录 YOLOv8目标检测创新改进与实战案例专栏介绍摘要文章链…

Dockerfile项目实战-单阶段构建Vue2项目

单阶段构建镜像-Vue2项目 1 项目层级目录 以下是项目的基本目录结构&#xff1a; 2 Node版本 博主的Windows电脑安装了v14.18.3的node.js版本&#xff0c;所以直接使用本机电脑生成项目&#xff0c;然后拷到了 Centos 7 里面 # 查看本机node版本 node -v3 创建Vue2项目 …

Zabbix 简介+部署+对接Grafana(详细部署!!)

目录 一.Zabbix简介 1.Zabbix是什么 2.Zabbix工作原理&#xff08;重点&#xff09;​ 3.Zabbix 的架构&#xff08;重点&#xff09;​ 1.服务端 2.客户端&#xff1a; 4.Zabbix和Prometheus区别 二.Zabbix 部署 1.前期准备 2.安装zabbix软件源和组件 3.安装数据库…

Ubuntu2404装机指南

因为原来的2204升级到2404后直接嘎了&#xff0c;于是要重新装一下Ubuntu2404 Ubuntu系统下载 | Ubuntuhttps://cn.ubuntu.com/download我使用的是balenaEtcher将iso文件烧录进U盘后&#xff0c;使用u盘安装&#xff0c;默认选的英文版本&#xff0c; 安装后&#xff0c;安装…

Spring Cloud初探之使用load balance包做负载均衡(三)

一、背景说明 基于前一篇文章《Spring Cloud初探之nacos服务注册管理(二)》&#xff0c;我们已经将服务注册到nacos。接下来继续分析如何用Spring cloud的load balance做负载均衡。 load balance是客户端负载均衡组件。本质是调用方拿到所有注册的服务实例列表&#xff0c;然…