基于Taro + React 实现微信小程序半圆滑块组件、半圆进度条、弧形进度条、半圆滑行轨道(附源码)

效果:

功能点:

1、四个档位

2、可点击加减切换档位

3、可以点击区域切换档位

4、可以滑动切换档位

目的:

给大家提供一些实现思路,找了一圈,一些文章基本不能直接用,错漏百出,代码还藏着掖着,希望可以帮到大家

代码

ts的写法风格

index.tsx

复制代码
import { View, ITouchEvent, BaseTouchEvent } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { useState } from 'react'
import styles from './index.module.less'
import classNames from 'classnames'
import { debounce } from '~/utils/util'

enum ANGLES {
  ANGLES_135 = -135,
  ANGLES_90 = -90,
  ANGLES_45 = -45,
  ANGLES_0 = 0
}

enum MODE_VALUE {
  MODE_1 = 1,
  MODE_2 = 2,
  MODE_3 = 3,
  MODE_4 = 4
}

const HalfCircle = () => {
  const [state, setState] = useState({
    originAngle: ANGLES.ANGLES_135,
    isTouch: false,
    val: MODE_VALUE.MODE_1,
    originX: 0,
    originY: 0
  })

  /** 半圆的半径 */
  const RADIUS = 150
  /** 半径的一半 */
  const RADIUS_HALF = RADIUS / 2
  /** 4/3 圆的直径 */
  const RADIUS_THIRD = RADIUS_HALF * 3
  /** 直径 */
  const RADIUS_DOUBLE = RADIUS * 2
  /** 误差 */
  const DEVIATION = 25

  /** 是否开启点击振动 */
  const isVibrateShort = true

  const getAngle = () => {
    return {
      transform: `rotate(${state.originAngle}deg)`,
      transition: `all ${state.isTouch ? ' 0.2s' : ' 0.55s'}`
    }
  }

  /**
   * 根据坐标判断是否在半圆轨道上,半圆为RADIUS,误差为DEVIATION
   * @param pageX
   * @param pageY
   */
  const isInHalfCircleLine = (pageX: number, pageY: number, deviation?: number) => {
    const DEVIATION_VALUE = deviation || DEVIATION
    const squareSum = (pageX - RADIUS) * (pageX - RADIUS) + (pageY - RADIUS) * (pageY - RADIUS)
    const min = (RADIUS - DEVIATION_VALUE) * (RADIUS - DEVIATION_VALUE)
    const max = (RADIUS + DEVIATION_VALUE) * (RADIUS + DEVIATION_VALUE)
    return squareSum >= min && squareSum <= max
  }

  /** 根据做标点,获取档位 0 -> 4, -45 -> 3, -90 -> 2, -135 -> 1,从而获取旋转的角度 */
  const setGear = (pageX: number, pageY: number) => {
    let val = state.val
    let originAngle = state.originAngle
    if (isInHalfCircleLine(pageX, pageY)) {
      if (pageX > 0 && pageX <= RADIUS_HALF) {
        val = MODE_VALUE.MODE_1
        originAngle = ANGLES.ANGLES_135
      } else if (pageX > RADIUS_HALF && pageX <= RADIUS) {
        val = MODE_VALUE.MODE_2
        originAngle = ANGLES.ANGLES_90
      } else if (pageX > RADIUS && pageX <= RADIUS_THIRD) {
        val = MODE_VALUE.MODE_3
        originAngle = ANGLES.ANGLES_45
      } else {
        val = MODE_VALUE.MODE_4
        originAngle = ANGLES.ANGLES_0
      }
    }

    if (state.val === val) return
    setState((old) => {
      return {
        ...old,
        originAngle,
        val
      }
    })

    if (isVibrateShort) {
      setTimeout(() => {
        Taro.vibrateShort()
      }, 200)
    }
  }

  /**
   * 滑动比较细腻,根据x轴坐标,calcX判断是否前进还是后退
   * @param pageX
   * @param pageY
   */
  const setGearSibler = (pageX: number, pageY: number) => {
    let val = state.val
    let originAngle = state.originAngle
    const calcX = pageX - state.originX
    /** 把误差值增加,方便滑动 */
    if (isInHalfCircleLine(pageX, pageY, 50)) {
      if (pageX > 0 && pageX <= RADIUS_HALF) {
        if (calcX > 0) {
          /** 向前滑动,就前进一个档位 */
          val = MODE_VALUE.MODE_2
          originAngle = ANGLES.ANGLES_90
        } else {
          /** 向后滑动,就后退一个档位 */
          val = MODE_VALUE.MODE_1
          originAngle = ANGLES.ANGLES_135
        }
      } else if (pageX > RADIUS_HALF && pageX <= RADIUS) {
        if (calcX > 0) {
          val = MODE_VALUE.MODE_2
          originAngle = ANGLES.ANGLES_90
        } else {
          val = MODE_VALUE.MODE_1
          originAngle = ANGLES.ANGLES_135
        }
      } else if (pageX > RADIUS && pageX <= RADIUS_THIRD) {
        if (calcX > 0) {
          val = MODE_VALUE.MODE_3
          originAngle = ANGLES.ANGLES_45
        } else {
          val = MODE_VALUE.MODE_2
          originAngle = ANGLES.ANGLES_90
        }
      } else {
        if (calcX > 0) {
          val = MODE_VALUE.MODE_4
          originAngle = ANGLES.ANGLES_0
        } else {
          val = MODE_VALUE.MODE_3
          originAngle = ANGLES.ANGLES_45
        }
      }
    }
    setState((old) => {
      return {
        ...old,
        originAngle,
        val
      }
    })
  }

  /**
   * 获取正确的坐标点
   * @param pageX
   * @param pageY
   * @returns
   */
  const getRealXY = (
    pageX: number,
    pageY: number
  ): Promise<{
    realX: number
    realY: number
  }> => {
    return new Promise((resolve) => {
      Taro.createSelectorQuery()
        .select('#sliderBgcId')
        .boundingClientRect((rect) => {
          const { left, top } = rect
          /** 获取真实的做标点 */
          const realX = pageX - left
          const realY = pageY - top
          resolve({
            realX,
            realY
          })
        })
        .exec()
    })
  }

  const onTouchEnd = (event: BaseTouchEvent<any>) => {
    setState((old) => {
      return {
        ...old,
        isTouch: false
      }
    })
  }

  const onTouchMove = debounce(async (event: BaseTouchEvent<any>) => {
    const { pageX, pageY } = event.changedTouches[0]
    const { realX, realY } = await getRealXY(pageX, pageY)
    if (isInHalfCircleLine(realX, realY)) {
      setGearSibler(realX, realY)
    }
  }, 100)

  const onTouchStart = async (event: BaseTouchEvent<any>) => {
    const { pageX, pageY } = event.changedTouches[0]
    const { realX, realY } = await getRealXY(pageX, pageY)
    setState((old) => {
      return {
        ...old,
        originX: realX,
        originY: realY,
        isTouch: true
      }
    })
  }

  /** 点击设置档位 */
  const onHandleFirstTouch = async (event: BaseTouchEvent<any>) => {
    const { pageX, pageY } = event.changedTouches[0]
    const { realX, realY } = await getRealXY(pageX, pageY)
    if (isInHalfCircleLine(realX, realY)) {
      setGear(realX, realY)
    }
  }

  const lose = () => {
    if (state.isTouch) return
    if (state.val === 1) return Taro.showToast({
        title: '最低只能1挡',
        icon: 'error',
        duration: 2000
      })
    setState((old) => {
      return {
        ...old,
        originAngle: state.originAngle - 45,
        val: state.val - 1
      }
    })

    if (isVibrateShort) {
      Taro.vibrateShort()
    }
  }

  const add = () => {
    if (state.isTouch) return
    if (state.val === 4) return Taro.showToast({
        title: '最高只能4挡',
        icon: 'error',
        duration: 2000
      })
    setState((old) => {
      return {
        ...old,
        originAngle: state.originAngle + 45,
        val: state.val + 1
      }
    })

    if (isVibrateShort) {
      Taro.vibrateShort()
    }
  }

  return (
    <View
      className={styles.slider}
      // onTouchEnd={(event) => onTouchEnd(event)}
      // onTouchMove={(event) => onTouchMove(event)}
      // onTouchStart={(event) => onTouchStart(event)}
      onClick={onHandleFirstTouch}
    >
      <View className={styles.activeSliderSet}>
        <View className={styles.activeSlider} style={getAngle()} />
      </View>
      <View className={styles.origin} id="origin">
        <View className={styles.long} style={getAngle()}>
          <View
            className={styles.circle}
            onTouchMove={(event) => onTouchMove(event as BaseTouchEvent<any>)}
            onTouchStart={(event) => onTouchStart(event as BaseTouchEvent<any>)}
            onTouchEnd={(event) => onTouchEnd(event as BaseTouchEvent<any>)}
          />
        </View>
      </View>
      {/* 背景 */}
      <View className={styles.sliderBgc} id="sliderBgcId" />
      {/* 刻度 */}
      {/* <View className={styles.scaleBgc} /> */}
      <View className={styles.centerContent}>
        <View className={styles.centerText}>能量档位</View>
        <View className={styles.btn_air_bar}>
          <View className={classNames(styles.btn_air, styles.btn_air_left)} onClick={lose}>
            -
          </View>
          <View className={styles.val}>
            <View className="val_text">{state.val}</View>
          </View>
          <View className={classNames(styles.btn_air, styles.btn_air_right)} onClick={add}>
            +
          </View>
        </View>
      </View>
    </View>
  )
}

export default HalfCircle

index.module.less

复制代码
@color-brand: #EBC795 ;
@borderColor:#706D6D;
@sliderWidth:10px;
@radius:150px;
@long: 150px;
@border-radius: @long;

.slider {
  position: relative;
  padding-bottom: @sliderWidth / 2;
  background-color: #000;
  width: 100vw;
  display: flex;
  justify-content: center;
  align-items: center;

  // 背景色
  .sliderBgc {
    width: @long*2;
    height: @long;
    border: @sliderWidth solid;
    border-radius: @border-radius  @border-radius 0 0;
    border-color: @borderColor;
    border-bottom: none;
  }

  .scaleBgc {
    width: @long*2 + @sliderWidth *2;
    height: @long + @sliderWidth;
    position: absolute;
    // bottom: 0;
    // left: 0;
    border: @sliderWidth solid;
    border-radius: @border-radius + @sliderWidth  @border-radius + @sliderWidth 0 0;
    border-color: transparent;
    border-bottom: none;
    top: -10px;
    background-clip: padding-box, border-box;
    background-origin: padding-box, border-box;
    background-image: linear-gradient(to right, #000, #000), linear-gradient(90deg, #FFD1B2, #E49E6B);
  }

  // 激活色
  .activeSliderSet {
    position: absolute;
    width: (@long) *2;
    height: @long;
    // left: 0;
    // bottom: 0;
    z-index: 2;
    overflow: hidden;

    .activeSlider {
      bottom: 0;
      left: 0;
      width: @long*2;
      height: @long;
      border: @sliderWidth solid;
      border-color: @color-brand;
      // border-color: transparent !important;
      border-radius: @border-radius  @border-radius 0 0;
      border-bottom: none;
      transform: rotate(-100deg);
      transform-origin: @long @long;
      // background-clip: padding-box, border-box;
      // background-origin: padding-box, border-box;
      // background-image: linear-gradient(to right, #000, #000), linear-gradient(90deg, #FFD1B2, #E49E6B);
    }
  }

  .origin {
    width: 0;
    height: 0;
    position: absolute;
    background-color: rgba(0, 0, 0, 0.1);
    bottom: 0;
    left: 50%;
    z-index: 11;
    transform: translateX(50%);

    .long {
      width: @long - (@sliderWidth / 2);
      height: 0;
      z-index: 9999;
      position: absolute;
      top: 0;
      left: 0;
      transform-origin: 0 0;
  
      .circle {
        width: 16px;
        height: 16px;
        border-radius: 50%;
        position: absolute;
        top: 50%;
        right: 0;
        transform: translate(50%, -50%);
        background-color: #000;
        border: #fff 4px solid;
        z-index: 999;
        padding: 5px;
      }
    }
  }


}

.centerContent {
  position: absolute;
  bottom: 0;
  left: 50%;
  transform: translateX(-50%);
  z-index: 99;
  margin-bottom: 20px;

  .centerText {
    text-align: center;
    color: var(--q-light-color-text-secondary, var(--text-secondary, #8C8C8C));
    font-size: 10px;
    margin-bottom: 25px;
  }

  .btn_air_bar {
    display: flex;
    align-items: center;
  
    .btn_air {
      width: 30px;
      height: 30px;
      border-radius: 50%;
      background-color: wheat;
      font-size: 16px;
      font-weight: 500;
      display: flex;
      align-items: center;
      justify-content: center;
    }

    .btn_air_left {
      background-color: #706D6D;
      color: white;
    }

    .btn_air_right {
      background-color: white;
      color: #706D6D;
    }
  
    .val {
      height: 26px;
      display: flex;
      align-items: center;
      margin: 0 30px;
      font-size: 26px;
      font-weight: 700;
    }
  }
} 

防抖的工具函数debounce 的详细代码:

import { debounce } from '~/utils/util'

复制代码
function debounce<T extends Function>(func: T, delay: number): T {
  let timeout
  return function (this: any, ...args: any[]): void {
    const context = this;
    clearTimeout(timeout);
    timeout = setTimeout(() => {
      func.apply(context, args);
    }, delay);
  } as any;
}
相关推荐
崔庆才丨静觅5 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60616 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了6 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅6 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅6 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅7 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment7 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅7 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊7 小时前
jwt介绍
前端
爱敲代码的小鱼7 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax