📅 我们继续 50 个小项目挑战!—— RangeSlider
组件
仓库地址:https://github.com/SunACong/50-vue-projects
项目预览地址:https://50-vue-projects.vercel.app/
使用 Vue 3 的 <script setup>
语法结合 Tailwind CSS 来构建一个视觉效果出众的自定义范围滑块(Range Slider)。这个滑块的独特之处在于,它的数值标签会随着滑块的拖动而动态移动,并始终保持在滑块拇指(thumb)的正上方,提供极佳的用户体验。
让我们开始吧!🚀
📝 应用目标
- 创建一个样式完全自定义的 HTML
<input type="range">
组件 - 实现一个动态数值标签,该标签会跟随滑块拇指的位置移动
- 确保标签始终位于滑块上方且居中对齐
- 使用 Vue 3 的响应式系统和生命周期钩子管理复杂的状态和计算
- 利用 Tailwind CSS 快速构建基础样式,并通过内联样式实现精确控制
🔧 技术实现点
技术点 | 描述 |
---|---|
Vue 3 <script setup> |
使用 ref , reactive , onMounted 管理状态、复杂对象和生命周期 |
v-model 双向绑定 |
将滑块的 value 与 sliderValue 响应式变量绑定 |
@input 事件监听 |
监听滑块值的变化 |
:style 动态绑定 |
使用 JavaScript 对象动态设置元素的 CSS 样式(特别是 left 位置) |
getComputedStyle() |
获取元素渲染后的实际宽度(包括 CSS 样式) |
reactive 对象 |
存储需要响应式更新的复杂样式对象(labelStyle , sliderStyles ) |
数学计算 (scale 函数) |
实现平滑的标签位置偏移,避免标签在滑块两端被截断 |
🖌️ 组件实现
🎨 模板结构 <template>
<template>
<div
class="m-0 flex min-h-screen flex-col items-center justify-center overflow-hidden bg-gradient-to-br from-gray-100 to-gray-300 font-sans">
<h2 class="absolute top-5 font-medium text-gray-700">Custom Range Slider</h2>
<div class="relative mt-10">
<!-- 自定义范围输入滑块 -->
<input
type="range"
min="0"
max="100"
v-model="sliderValue"
@input="handleSliderInput"
class="relative z-10 h-3 w-[300px] cursor-pointer appearance-none rounded-md bg-purple-500 outline-none"
:style="sliderStyles" />
<!-- 动态数值标签 -->
<label
:style="labelStyle"
class="absolute z-0 w-[80px] rounded-md bg-white py-1.5 text-center text-gray-700 shadow-md transition-all duration-300">
{{ sliderValue }}
</label>
</div>
</div>
</template>
模板结构简洁明了:
- 外层容器:一个全屏的
div
,使用渐变背景 (from-gray-100 to-gray-300
) 和 Flexbox 居中内容。 - 标题:固定在顶部的
h2
标签。 - 滑块容器:一个
relative
定位的div
,用于容纳滑块和标签。 - 滑块 (
<input type="range">
):- 使用
v-model
绑定sliderValue
。 @input
事件触发handleSliderInput
函数进行位置计算。- Tailwind 类设置基本样式:紫色背景 (
bg-purple-500
)、圆角 (rounded-md
)、高度 (h-3
)、宽度 (w-[300px]
)、手型光标 (cursor-pointer
) 和移除默认轮廓 (outline-none
)。 appearance-none
移除浏览器默认样式,relative z-10
确保它在标签之上。:style="sliderStyles"
应用内联样式(主要是为了兼容性移除appearance
)。
- 使用
- 标签 (
<label>
):absolute
定位,z-0
确保它在滑块轨道之下(视觉上在滑块上方,因为滑块z-10
)。:style="labelStyle"
动态绑定其left
位置。- Tailwind 类设置标签样式:白色背景 (
bg-white
)、阴影 (shadow-md
)、圆角、内边距 (py-1.5
)、固定宽度 (w-[80px]
)、居中文本 (text-center
) 和平滑过渡 (transition-all duration-300
)。
💻 脚本逻辑 <script setup>
<script setup>
import { ref, reactive, onMounted } from 'vue'
// 响应式状态管理
const sliderValue = ref(50)
const labelStyle = reactive({
left: '110px', // 初始位置(基于 50/100)
top: '-30px', // 向上调整,显示在滑块上方
})
const sliderStyles = reactive({})
// 初始化滑块样式 - 移除原生外观
onMounted(() => {
// 这些样式通常在CSS中设置,这里用:style是为了演示动态应用
// 实际项目中建议写在<style>里
sliderStyles.webkitAppearance = 'none'
sliderStyles.mozAppearance = 'none'
sliderStyles.appearance = 'none'
})
// 处理滑块输入事件 - 核心逻辑
const handleSliderInput = (e) => {
const value = +e.target.value // 当前滑块值
const range = e.target // 滑块DOM元素
// 获取滑块轨道和标签的渲染后宽度
const rangeWidth = getComputedStyle(range).width
const label = e.target.nextElementSibling // 获取<label>元素
const labelWidth = getComputedStyle(label).width
// 提取像素值(去掉'px'后缀)
const numWidth = +rangeWidth.substring(0, rangeWidth.length - 2)
const numLabelWidth = +labelWidth.substring(0, labelWidth.length - 2)
const max = +range.max
const min = +range.min
// 核心计算:确定标签的left位置
// 1. 计算value在0-max范围内的相对位置对应的像素
// 2. 减去标签自身宽度的一半,使其水平居中于该位置
// 3. 加上scale()函数返回的偏移量,实现两端防截断
const left = value * (numWidth / max) - numLabelWidth / 2 + scale(value, min, max, 10, -10)
// 更新响应式样式对象
labelStyle.left = `${left}px`
// 更新滑块值(虽然v-model已绑定,但此处显式更新以确保一致性)
sliderValue.value = value
}
// 工具函数:将一个数值从一个范围线性映射到另一个范围
// 用于在滑块两端为标签位置添加偏移,防止标签被裁剪
const scale = (num, in_min, in_max, out_min, out_max) => {
return ((num - in_min) * (out_max - out_min)) / (in_max - in_min) + out_min
}
</script>
脚本部分是组件的核心:
- 状态初始化:
sliderValue
默认为 50,labelStyle
初始化left
和top
,sliderStyles
为空对象。 onMounted
:组件挂载后,通过修改sliderStyles
对象来应用appearance: none
样式,彻底移除滑块的原生外观。(注意:在实际项目中,这些样式通常写在<style>
标签内更合适,这里是为了演示:style
的用法)。handleSliderInput
事件处理:- 获取当前滑块值 (
value
) 和滑块元素 (range
)。 - 使用
getComputedStyle
获取滑块轨道 (rangeWidth
) 和标签 (labelWidth
) 的实际渲染宽度(像素值)。 - 将获取的字符串宽度(如 “300px”)转换为数字。
- 计算标签的
left
位置:value * (numWidth / max)
:计算value
在滑块总长度上对应的位置(像素)。- numLabelWidth / 2
:减去标签宽度的一半,使其左边缘位于计算位置的左侧,从而实现标签在该点的水平居中。+ scale(value, min, max, 10, -10)
:调用scale
函数,当value
从0
到100
时,返回的偏移量从+10
线性变化到-10
。这使得标签在滑块最左端时右移 10px,在最右端时左移 10px,有效避免了标签在容器边界被截断的问题。
- 将计算出的
left
值(带 ‘px’ 单位)赋给labelStyle.left
,触发标签的重新渲染和位置更新。 - 更新
sliderValue
(尽管v-model
会自动处理,但显式更新更清晰)。
- 获取当前滑块值 (
scale
函数:一个通用的线性插值函数,用于将输入值num
从[in_min, in_max]
范围映射到[out_min, out_max]
范围。
🎨 Tailwind CSS 样式重点
类名 | 作用 |
---|---|
m-0 |
外边距为0 |
flex |
启用 Flexbox 布局 |
min-h-screen |
最小高度为视口高度 |
flex-col |
Flex 方向为垂直 |
items-center / justify-center |
水平和垂直居中 |
overflow-hidden |
隐藏溢出内容 |
bg-gradient-to-br from-gray-100 to-gray-300 |
从左上到右下的灰色渐变背景 |
font-sans |
无衬线字体 |
absolute / relative |
定位上下文和绝对定位 |
top-5 |
距离顶部 1.25rem |
font-medium |
中等粗细字体 |
text-gray-700 |
深灰色文字 |
mt-10 |
上外边距 2.5rem |
h-3 |
高度 0.75rem |
w-[300px] |
宽度 300px (使用任意值语法) |
cursor-pointer |
手型光标 |
appearance-none |
移除元素默认外观 |
rounded-md |
中等圆角 |
bg-purple-500 |
紫色背景 |
outline-none |
移除聚焦轮廓 |
z-10 / z-0 |
控制层叠顺序 (z-index) |
w-[80px] |
标签固定宽度 80px |
py-1.5 |
垂直内边距 (0.375rem + 0.375rem) |
text-center |
文本居中 |
shadow-md |
中等阴影 |
transition-all duration-300 |
所有属性变化在 300ms 内平滑过渡 |
📁 常量定义 + 组件路由
constants/index.js
添加组件预览常量:
{
id: 45,
title: 'NetflixMobileNavigation',
image: 'https://50projects50days.com/img/projects-img/45-netflix-mobile-navigation.png',
link: 'NetflixMobileNavigation',
},
router/index.js
中添加路由选项:
{
path: '/NetflixMobileNavigation',
name: 'NetflixMobileNavigation',
component: () => import('@/projects/NetflixMobileNavigation.vue'),
},
🏁 总结
结合 Vue 3 的响应式能力与原生 JavaScript 的 DOM 操作 (getComputedStyle
),创建了一个功能完整且视觉上吸引人的自定义范围滑块。我们解决了动态元素定位的关键挑战,并通过 scale
函数实现了智能的边界处理。
这个自定义滑块组件有很大的扩展空间:
- ✅ 样式美化:为滑块拇指(thumb)添加自定义样式(通常需要额外的 CSS 伪元素如
::-webkit-slider-thumb
)。 - ✅ 步长 (
step
):添加step
属性控制滑块的增量。 - ✅ 多滑块:创建一个支持双滑块(范围选择)的组件。
- ✅ 垂直滑块:修改样式和计算逻辑,创建垂直方向的滑块。
- ✅键盘支持:添加键盘事件(如左右箭头键)来控制滑块。
- ✅ 单位显示:在标签中添加单位(如
{{ sliderValue }}%
)。
👉 下一篇,我们将完成NetflixMobileNavigation
组件,一个模仿Netflix移动端的导航样式。🚀
感谢阅读,欢迎点赞、收藏和分享 😊