效果图如下
具体实现
- 封装两个工具函数
// 延迟执行函数
function delay(delayTime = 25): Promise<null> {
return new Promise(resolve => {
setTimeout(() => {
resolve(null);
}, delayTime)
})
}
/**
* 根据传入的selector 获取节点信息
* @param selectorStr class/id
* @param delayTime 延迟时间
*/
function delayQuerySelector(selectorStr: string, delayTime = 500): Promise<any[]> {
return new Promise(resolve => {
const selector: SelectorQuery = Taro.createSelectorQuery();
delay(delayTime).then(() => {
selector
.select(selectorStr)
.boundingClientRect()
.exec((res: any[]) => {
resolve(res);
})
})
})
}
/**
* 将传入的数值按照 给定的递增值分割
* @param num 需要分割的数值
* @param increasingVal 递增值
*/
function increasingDivideNum(num: number, increasingVal: number): number[] {
const maxAim = Math.ceil(num / increasingVal);
const resultArr: number[] = [];
for (let i = 0; i <= maxAim; i++) {
let val = increasingVal * i
resultArr.push(val)
}
return resultArr;
}
封装组件
import { View, Image, Text } from "@tarojs/components";
import { ITouchEvent } from '@tarojs/components/types/common';
import { useState, useRef, useEffect } from "react";
import Taro from '@tarojs/taro';
import { SelectorQuery } from '@tarojs/taro/types/index';
interface PriceRangeProps {
initValue: number[]; //选择器初始值
min: number; // 选择器最小值
max: number; // 选择器最大值
maxScale?: number; // 显示的 最大刻度
increaseScaleVal?: number; // 递增的刻度值
isShowScale?: boolean; // 是否显示刻度
onChange?: (posi: [number, number]) => void; // 随时获取改变值
onAfterChange?: (posi: [number, number]) => void; // 获取最终改变值
}
const PriceRange: React.FC<PriceRangeProps> = (props) => {
const { min = 0, max = 50 } = props;
const deltaValue = max - min;
const [sliderPosition, setSliderPosition] = useState({ sliderAX: 0, sliderBX: 0 }); // 滑块的位置
// 获取刻度数组
const increaseScaleVal = props?.increaseScaleVal ?? 10;
const maxScale = props.maxScale ?? max - increaseScaleVal;
const rangeScale = increasingDivideNum(max, increaseScaleVal);
// 是否显示刻度 默认 true
const isShowScale = props.isShowScale ?? true;
const leftRef = useRef<number>(0);
const containerWRef = useRef<number>(0);
// 获取track 的style--- left和width
const getTrackStyle = () => {
const smallerX = Math.min(sliderPosition.sliderAX, sliderPosition.sliderBX);
const distanceX = Math.abs(sliderPosition.sliderAX - sliderPosition.sliderBX);
return {
left: smallerX + "%",
width: distanceX + "%"
}
}
const handleTouchMove = (e: ITouchEvent, sliderName: "sliderAX" | "sliderBX") => {
e.stopPropagation();
const clientX = e.touches[0].clientX;
updateSliderValue(sliderName, clientX - leftRef.current, "onChange");
}
const handleTouchEnd = () => {
triggerEvent('onAfterChange');
}
/**
* 初始化 滑块的值
* @param value 初始值
*/
const initSilderValue = (value: number[]) => {
const sliderAX = Math.round(((value[0] - min) / deltaValue) * 100);
const sliderBX = Math.round(((value[1] - min) / deltaValue) * 100);
setSliderPosition({
sliderAX, sliderBX
})
}
/**
* 更新滑块 value
* @param sliderName 滑块名称
* @param targetValue
* @param funcName 获取改变后值的函数名称
*/
const updateSliderValue = (sliderName: "sliderAX" | "sliderBX", targetValue: number, funcName: 'onChange' | 'onAfterChange'): void => {
const distance = Math.min(Math.max(targetValue, 0), containerWRef.current);
const sliderValue = Math.floor((distance / containerWRef.current) * 100);
if (funcName) {
setSliderPosition(position => {
const newPosition = { ...position, [sliderName]: sliderValue };
triggerEvent(funcName)
return newPosition;
})
} else {
const newPosition = { ...sliderPosition, [sliderName]: sliderValue };
setSliderPosition(newPosition);
}
}
/**
* 回调更新后的值
* @param funcName 获取改变后值的函数名称
*/
const triggerEvent = (funcName: 'onChange' | 'onAfterChange') => {
const a = Math.round((sliderPosition.sliderAX / 100) * deltaValue) + min;
const b = Math.round((sliderPosition.sliderBX / 100) * deltaValue) + min;
const result = [a, b].sort((x, y) => x - y) as [number, number];
if (funcName === 'onChange') {
props.onChange && props.onChange(result)
} else if (funcName === 'onAfterChange') {
props.onAfterChange && props.onAfterChange(result)
}
}
useEffect(() => {
delayQuerySelector('.range-container', 0).then(rect => {
leftRef.current = Math.round(rect[0].left);
containerWRef.current = Math.round(rect[0].width);
});
}, [])
useEffect(() => {
initSilderValue(props.initValue!)
},[props.initValue])
return (
<View className="range">
{isShowScale && <View className="range-scale">
{
rangeScale.length > 0 && rangeScale.map(scale => {
return (
<Text>{scale > maxScale ? "不限" : scale}</Text>
);
})
}
</View>}
<View className="range-container">
<View className="range-rail"></View>
<View className="range-track" style={getTrackStyle()}></View>
<View className="range-slider" style={{ left: sliderPosition.sliderAX + '%' }} onTouchMove={(e) => handleTouchMove(e, 'sliderAX')} onTouchEnd={handleTouchEnd}>
<Image className="anchor-icon" src={require("@/static/image/icons/anchor-icon.png")}></Image>
</View>
<View className="range-slider" style={{ left: sliderPosition.sliderBX + '%' }} onTouchMove={(e) => handleTouchMove(e, 'sliderBX')} onTouchEnd={handleTouchEnd}>
<Image className="anchor-icon" src={require("@/static/image/icons/anchor-icon.png")}></Image>
</View>
</View>
</View>
);
}
export { PriceRange };
加上样式(scss)
// range --- range slider
.price-slider {
margin-top: 12px;
margin-bottom: 64px;
}
.range,
.range-container {
position: relative;
width: 100%;
box-sizing: border-box;
}
.range-scale {
display: flex;
position: relative;
bottom: -32px;
justify-content: space-between;
&>text {
color: #808080;
font-size: 18px;
font-weight: 500;
}
}
.range-container {
display: flex;
align-items: center;
height: 90px;
.range-rail {
width: 100%;
height: 8px;
background-color: #C9D2D9;
overflow: hidden;
border-radius: 100px;
}
.range-track {
position: absolute;
height: 8px;
background: #FF8253;
left: 33%;
width: 17%;
border-radius: 100px;
}
.range-slider {
position: absolute;
margin-left: -24px;
top: 52px;
// width: 28px;
// height: 28px;
// border-radius: 50%;
// background: #fff;
// box-shadow: 0 0 4PX 0 rgb(0 0 0 / 20%);
.anchor-icon {
width: 48px;
height: 62px;
}
}
}
// range --- range slider