价格滑动条的实现分析
约 1141 字大约 4 分钟
2026-04-19
在打车、电商等业务场景中,双滑块价格范围选择器是一个常见的交互组件。本文将基于一段 Mpx 框架的小程序代码,拆解其实现思路,帮助你理解如何从零搭建一个具备 “跟手拖动、端点快速选择、状态联动” 的双滑块组件。
组件功能概览
先明确这个组件要解决的核心问题:
- 双滑块拖动:支持两个滑块独立拖动,选择价格区间。
- 端点快速选择:点击轨道两侧的 “最小 / 最大值” 标记,可快速将滑块定位到端点。
- 实时视觉反馈:拖动时,滑块数值、两滑块间的填充条实时更新。
- 状态联动:通过 Store 管理全局价格状态,并向父组件触发事件。
DOM结构
点我查看代码
<view class="slider-container">
<view class="slider-track" id="track">
<!-- 1. 填充区域(两滑块之间的蓝色条) -->
<view class="slider-progress" style="left: {{progressLeft}}%; width: {{progressWidth}}%;"></view>
<!-- 2. 双滑块(带数值显示) -->
<view class="slider-thumb" data-index="1">
<text>{{ value1 }}</text>
</view>
<view class="slider-thumb" data-index="2">
<text>{{ value2 }}</text>
</view>
<!-- 3. 两侧端点标记(点击快速选最小/最大值) -->
<view class="slider-bump slider-min-bump" bind:tap="clickMin">
<text>{{ priceConfig.min }}</text>
</view>
<view class="slider-bump slider-max-bump" bind:tap="clickMax">
<text>{{ priceConfig.max }}</text>
</view>
</view>
</view>- 轨道(slider-track):作为整个组件的 “底座”,提供滑动范围的视觉参考。
- 填充条(slider-progress):通过动态 left 和 width 样式,直观展示当前选中的价格区间。
- 滑块(slider-thumb):通过 data-index 区分两个滑块,绑定触摸事件实现拖动。
- 端点标记(slider-bump):固定在轨道两侧,点击可快速将滑块定位到端点。
交互与状态管理
数据定义
data: {
priceConfig: { min: 10, max: 80, step: 1 }, // 价格范围配置
value1: 10, value2: 30, // 两个滑块的当前价格
thumb1Percent: 0, thumb2Percent: 0, // 滑块的位置(像素值)
progressLeft: 0, progressWidth: 0, // 填充条的位置和宽度
trackInfo: { width: 0, left: 0 }, // 轨道的 DOM 尺寸信息
draggingIndex: null, // 当前拖动的滑块索引(1/2)
}初始获取轨道尺寸
在 ready 生命周期中,通过 createSelectorQuery 获取轨道的实际宽度和左偏移,为后续 “位置转价格” 计算提供依据。
ready() {
this.createSelectorQuery().select('#track').boundingClientRect((rect) => {
this.trackInfo = { width: rect.width, left: rect.left };
this.updateSlider(); // 初始化滑块位置
}).exec();
}价格与位置的转换
组件的核心逻辑在于 “价格” 与 “滑块位置” 的相互转换,这里封装了三个关键方法:
(1)价格 → 百分比(priceToPercent)
将价格值转换为在轨道上的百分比位置,用于计算滑块和填充条的样式。
priceToPercent(price) {
const { min, max } = this.priceConfig;
return ((price - min) / (max - min)) * 100;
}(2)位置 → 价格(positionToRawPrice /positionToPrice)
- positionToRawPrice:将触摸位置转换为连续的价格值(带小数),用于实现 “滑块跟手” 的流畅效果。
- positionToPrice:将触摸位置转换为取整后的价格值,用于最终的价格更新。
// 连续价格(用于跟手)
positionToRawPrice(clientX) {
const { width, left } = this.trackInfo;
const { min, max } = this.priceConfig;
let position = clientX - left;
position = Math.max(0, Math.min(width, position)); // 限制在轨道内
return min + (position / width) * (max - min);
}
// 取整价格(用于最终更新)
positionToPrice(clientX) {
const rawPrice = this.positionToRawPrice(clientX);
return Math.round(rawPrice);
}拖动事件处理
拖动逻辑的关键在于 “实时更新滑块位置,但只在价格变化≥1 时更新数值”,既保证跟手性,又避免频繁触发状态更新。
(1)触摸开始(onThumbStart) 记录当前拖动的滑块索引,并设置 “无过渡动画” 状态,避免拖动时卡顿。
onThumbStart(e) {
this.draggingIndex = e.currentTarget.dataset.index;
}(2)触摸移动(onThumbMove) 这是最核心的逻辑,分为三步:
- 用连续价格实时更新滑块位置(保证跟手)。
- 只有当取整价格与当前价格差≥1时,才更新价格数值。
- 实时计算填充条的位置和宽度。
onThumbMove(e) {
if (!this.draggingIndex) return;
// 1. 用连续价格更新滑块位置(跟手)
const clientX = e.touches[0].clientX;
const rawPrice = this.positionToRawPrice(clientX);
const newPercent = this.priceToPercent(rawPrice);
this[`thumb${this.draggingIndex}Percent`] = (newPercent * this.trackInfo.width) / 100;
// 2. 价格差≥1时,才更新数值
const newPrice = Math.round(rawPrice);
const oldPrice = this.draggingIndex == '1' ? this.value1 : this.value2;
if (Math.abs(newPrice - oldPrice) >= 1) {
this[`value${this.draggingIndex}`] = newPrice;
}
// 3. 更新填充条
const val1 = this.draggingIndex == '1' ? newPrice : this.value1;
const val2 = this.draggingIndex == '2' ? newPrice : this.value2;
this.calcProgress(val1, val2);
}(3)触摸结束(onThumbEnd)
清空拖动状态,并将最终价格同步到 Store 和父组件。
onThumbEnd() {
this.draggingIndex = null;
planStore.setNoTransition(false);
planStore.handleSliderPriceChange(this.minVal, this.maxVal);
this.triggerEvent('priceChange');
}