基于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;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/115086.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

postgresql 触发器如何生成递增序列号,从1开始,并且每天重置

大家好&#xff0c;我是三叔&#xff0c;许久不见&#xff0c;这期给大家介绍一下笔者在开发中遇到的业务处理&#xff1a;pgsql 创建触发器生成每日递增序列&#xff0c;并且第二天重置&#xff0c;根据不同的用户进行不同的控制。 1.创建生成递增序列的 table 表 -- 创建us…

线程池与工厂模式

目录 ♫什么是线程池 ♫线程池的优点 ♫工厂模式 ♫工厂模式的意义 ♫线程池的使用 ♫线程池常见的创建方法 ♫ThreadPoolExecutor ♫实现一个线程池 ♫什么是线程池 线程池是一种管理和复用线程的技术&#xff0c;它在应用程序启动时预先创建一组线程&#xff0c;并将它…

uniapp写一个计算器用于记账(微信小程序,APP)

提要&#xff1a;自己用uniapp写了一个记账小程序&#xff08;目前是小程序&#xff09;&#xff0c;写到计算器部分&#xff0c;在网上找了别人写的计算器&#xff0c;大多数逻辑都是最简单的&#xff0c;都不能满足一个记账计算器的基本逻辑。与其在网上找来找去&#xff0c;…

由CAB/PAB展开的一些思考

又到了一月保底一篇订阅号文章的时候&#xff0c;上周受到Oracle邀请&#xff0c;参加了在上海Oracle办公室举行的CAB&#xff08;The 13th Oracle China Customer Advisory Board Metting&#xff09;/ PAB&#xff08;The 3th Oracle China Partner Advisory Board Metting&…

信息论基础知识1

1.1 自信息定义&#xff1a;把某个消息出现的不确定性大小&#xff0c;用这个消息出现的概率的对数表示&#xff1a; I(X)-logp(x) 1.2 在任何一个信息流通的系统中&#xff0c;都有一个发出信息的发送端&#xff08;信源&#xff09;&#xff0c;有一个接收信息的接收端…

cmake find_package、引用GDAL 初步学习

上次的源码的CMakeLists.txt文件里有 find_package(GDAL REQUIRED) 这句; 从字面意思看此源码需要GDAL库; 查了一下,find_package 指令的基本功能是查找第三方库,并返回其细节; 我当前GDAL安装在D:\GDAL; 先把它的CMakeLists.txt重命名为别的,不使用; 新建一个C…

多线程基础

1. 线程创建的几种方式 2. 锁的类型 在学习JUC之前&#xff0c;加锁、等待、唤醒 分别使用的是 &#xff08;synchronized、lock&#xff09;、wait、notify在学习JUC开始&#xff0c;学会使用lock接口的其他实现类来进行上述操作&#xff0c;比如 ReentrantLock 3. 线程池 …

哪里能找到可以学习的前端实战项目?

前言 下面是我整理的一些关于GitHub上的前端相关的项目&#xff0c;希望对你有所帮助&#xff0c;整理不易&#xff0c;可以的话不要吝啬你的点赞喜欢收藏哈~ 废话少说&#xff0c;我们直接进入正题——> 实用工具向 1.Echarts Star&#xff1a;55.6k Echarts提供了大量…

从零开始的目标检测和关键点检测(二):训练一个Glue的RTMDet模型

从零开始的目标检测和关键点检测&#xff08;二&#xff09;&#xff1a;训练一个Glue的RTMDet模型 一、config文件解读二、开始训练三、数据集分析四、ncnn部署 从零开始的目标检测和关键点检测&#xff08;一&#xff09;&#xff1a;用labelme标注数据集 从零开始的目标检测…

Spring、SpringMVC、Mybatis

一.Spring基础 1.Spring 框架是什么 Spring 是一款开源的轻量级 Java 开发框架&#xff0c;我们一般说 Spring 框架指的都是 Spring Framework&#xff0c;它是很多模块的集合&#xff0c;例如&#xff0c;Spring core、Spring JDBC、Spring MVC 等&#xff0c;使用这些模块可…

python 数据挖掘库orange3 介绍

orange3 是一个非常适合初学者的data mining library. 它让使用者通过拖拽内置的组件来形成工作流。让你不需要写任何代码就可以体验到数据挖掘和可视化的魅力。 它的桌面如下&#xff0c;这里我创建了 3 个节点&#xff0c;分别是数据集、小提琴图&#xff0c;散点图 其中 …

误删的文件恢复了成乱码 误删的文件恢复了成乱码怎么调整

电脑系统&#xff1a;Windows11 电脑型号&#xff1a;惠普 软件版本&#xff1a;EasyRcovery14 关于电脑&#xff0c;我们可以说是非常熟悉&#xff0c;并熟练掌握了对电脑的最基本操作&#xff0c;比如复制、粘贴、新建、删除文件。但我们真的很懂它吗&#xff1f;比如误删…

SAP SD 定价 删除不满足条件的的条件类型

项目上的需求&#xff1a;当销售订单行项目类别满足条件时&#xff0c;根据配置表&#xff0c;删除不满足条件的的条件类型。 直接上增强点&#xff0c;bapi也能跑到这个位置。

vite安装Tailwind CSS

安装 - Tailwind CSS 中文网 (nodejs.cn) 这是官网&#xff0c;平常我练习一般会用vite脚手架 我们选择这个vite模块 可选择React和Vue版本的&#xff0c;这里选择react的按照操作&#xff0c;没问题的话就要出问题了 1、在npm run dev的时候我是出现了这么个问题&#xff0c…

XML External Entity-XXE-XML实体注入

XML 实体? XML 实体允许定义标签,在解析 XML 文档时这些标签将被内容替换。一般来说,实体分为三种类型: 内部实体 外部实体 参数实体。 必须在文档类型定义(DTD)中创建实体 一旦 XML 文档被解析器处理,它将js用定义的常量“Jo Smith”替换定义的实体。正如您所看到…

React Hooks的使用

目录 1.React Hooks使用注意事项 1.useState Hook&#xff1a; 2.useEffect Hook&#xff1a; 3.其他常用Hooks&#xff1a; 2.使用React Hooks需要遵循 1.安装React&#xff1a; 2.导入所需的Hooks&#xff1a; 3.使用Hooks创建组件&#xff1a; 4.在应用中使用组件&…

Pytorch从零开始实战08

Pytorch从零开始实战——YOLOv5-C3模块实现 本系列来源于365天深度学习训练营 原作者K同学 文章目录 Pytorch从零开始实战——YOLOv5-C3模块实现环境准备数据集模型选择开始训练可视化模型预测总结 环境准备 本文基于Jupyter notebook&#xff0c;使用Python3.8&#xff0c…

在Node.js中,什么是中间件(middleware)?它们的作用是什么?

聚沙成塔每天进步一点点 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 欢迎来到前端入门之旅&#xff01;感兴趣的可以订阅本专栏哦&#xff01;这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友们量身打造的。无论你是完全的新手还是有一些基础的开发…

NodeJS 安装及环境配置

下载地址&#xff1a;https://nodejs.org/zh-cn/download/ 安装 NodeJS 根据自己电脑系统及位数选择&#xff0c;一般都选择 windows 64位 .msi 格式安装包。 所用命令&#xff1a; node -v npm -v PS&#xff1a;如果以上两条命令都能执行成功&#xff0c;表示安装完成&#…