taro+react+ts 封装滑动选择器

1,310 阅读2分钟

效果图如下

image.png

具体实现

  • 封装两个工具函数
// 延迟执行函数
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