编辑
2024-12-31
Vue
00

目录

使用React Hooks实现动态压差仪表盘 - 深入理解角度计算
1. 角度设计原理
1.1 基本角度概念
1.2 角度计算详解
1.3 为什么是这些角度?
2. 核心实现
2.1 Hook实现
2.2 角度转换实现
3. 视觉实现
4. 组件使用示例
5. 性能优化
6. 注意事项
结论

看效果
仪表盘

使用React Hooks实现动态压差仪表盘 - 深入理解角度计算

在本文中,我们将详细探讨如何实现一个专业的压差仪表盘组件,特别关注角度计算的原理和实现。

1. 角度设计原理

1.1 基本角度概念

仪表盘使用两个重要的角度系统:

  1. 指针角度范围:-150° 到 +150°(总计300度)
  2. 刻度角度范围:240° 到 -54°(总计294度)

1.2 角度计算详解

typescript
// 指针角度计算 const calculatePointerAngle = (value: number): number => { // 将0-1000的值映射到-150到150度的范围 return (value - 0) * (150 - (-150)) / (1000 - 0) + (-150); }; // 刻度角度计算 const calculateScaleAngle = (index: number): number => { // 从240度开始,每个刻度递减6度 return 240 - index * 6; };

1.3 为什么是这些角度?

  1. 指针角度范围(-150° 到 +150°)

    • 总范围300度,提供更大的显示空间
    • 对称设计,便于读数
    • 适合显示0-1000的值范围
  2. 刻度角度(240°)

    • 起始于240度位置
    • 50个刻度,每个间隔6度
    • 确保刻度分布均匀

2. 核心实现

2.1 Hook实现

typescript
import { useEffect, useRef, useState } from 'react'; interface PressureGaugeProps { pressure: number; differentialPressure?: { high: number; low: number; }; } const usePressureGauge = ({ pressure, differentialPressure }: PressureGaugeProps) => { const [angle, setAngle] = useState(-150); // 起始角度 const [statValueCurrent, setStatValueCurrent] = useState(0); const [statValueInterval, setStatValueInterval] = useState(0); // 角度计算工具函数 const calculateAngle = (value: number) => { const clampedValue = Math.min(Math.max(value, 0), 1000); return (clampedValue - 0) * (150 - (-150)) / (1000 - 0) + (-150); }; // 刻度位置计算 const calculateScalePosition = (index: number) => { const angle = 240 - index * 6; const theta = (angle * Math.PI) / 180; const radius = OUTER_RING_RADIUS + (index % 6 ? 0 : 4); return { x: (Math.cos(theta) * radius) * 0.432, y: (Math.sin(theta) * -radius) * 0.432, angle }; }; // 其余实现代码... };

2.2 角度转换实现

typescript
const getScalePositions = () => { return Array.from({ length: 50 }).map((_, index) => { const { x, y, angle } = calculateScalePosition(index); return { transform: `translate(${x}px, ${y}px) rotate(${-angle}deg)`, isHighlight: index % 5 === 0 }; }); }; const getDigitPositions = () => { return Array.from({ length: 11 }).map((_, index) => { const angle = 240 - index * 30; // 数字间隔30度 const theta = (angle * Math.PI) / 180; const x = (Math.cos(theta) * DIGIT_RING_RADIUS) * 0.432; const y = (Math.sin(theta) * -DIGIT_RING_RADIUS) * 0.432; return { value: index * 100, transform: `translate(${x}px, ${y}px)` }; }); };

3. 视觉实现

看图

4. 组件使用示例

tsx
const PressureGauge: React.FC<PressureGaugeProps> = ({ pressure, differentialPressure }) => { const { angle, scalePositions, digitPositions } = usePressureGauge({ pressure, differentialPressure }); return ( <div className="speedometer-container"> <div className="outer-ring"> {scalePositions.map((position, index) => ( <span key={index} className="tick" style={position} /> ))} </div> <div className="digit-ring"> {digitPositions.map((position, index) => ( <span key={index} className="digit" style={position.transform} > {position.value} </span> ))} </div> <div className="pointer" style={{ transform: `rotate(${angle}deg)` }} /> </div> ); };

5. 性能优化

  1. 角度计算优化
typescript
// 使用记忆化避免重复计算 const memoizedAngle = useMemo(() => { return calculateAngle(pressure); }, [pressure]);
  1. 动画性能优化
typescript
// 使用RAF代替setTimeout const animate = useCallback(() => { if (animationRef.current) { cancelAnimationFrame(animationRef.current); } const step = () => { setAngle(currentAngle => { const nextAngle = currentAngle + (targetAngle - currentAngle) * 0.1; if (Math.abs(targetAngle - nextAngle) < 0.1) { return targetAngle; } animationRef.current = requestAnimationFrame(step); return nextAngle; }); }; animationRef.current = requestAnimationFrame(step); }, [targetAngle]);

6. 注意事项

  1. 角度计算精度

    • 注意处理边界情况(0和1000)
    • 确保动画过程中角度计算的准确性
  2. 性能考虑

    • 避免不必要的角度重计算
    • 使用RAF优化动画性能
    • 合理使用useMemo和useCallback
  3. 兼容性

    • 考虑不同设备的旋转支持
    • 确保CSS transform的浏览器兼容性

结论

通过深入理解仪表盘的角度计算原理,我们实现了一个专业的压差仪表盘组件。300度的总角度范围提供了良好的可读性和精确度,而精心设计的刻度分布确保了视觉效果的专业性。结合React Hooks的响应式特性,我们得到了一个高性能、可复用的组件。

以上内容由claude.ai生成,结合解决思路与相关API,实现代码使用AI润色生成, 由vue2.0代码使用AI生成


以下为原先vue2.0代码

vue
<template> <div class="speedometer speedometer-container"> <div class="inner-ring"></div> <div class="outer-ring" ref="refScale"> <span class="tick" style="transform: translate(-145.492px, 84px) rotate(-210deg);"></span> <span class="tick" style="transform: translate(-148.634px, 69.3094px) rotate(-205deg);"></span> <span class="tick" style="transform: translate(-154.11px, 56.0913px) rotate(-200deg);"></span> <span class="tick" style="transform: translate(-158.412px, 42.4463px) rotate(-195deg);"></span> <span class="tick" style="transform: translate(-161.508px, 28.4783px) rotate(-190deg);"></span> <span class="tick" style="transform: translate(-163.376px, 14.2935px) rotate(-185deg);"></span> <span class="tick" style="transform: translate(-168px, -2.05741e-14px) rotate(-180deg);"></span> <span class="tick" style="transform: translate(-163.376px, -14.2935px) rotate(-175deg);"></span> <span class="tick" style="transform: translate(-161.508px, -28.4783px) rotate(-170deg);"></span> <span class="tick" style="transform: translate(-158.412px, -42.4463px) rotate(-165deg);"></span> <span class="tick" style="transform: translate(-154.11px, -56.0913px) rotate(-160deg);"></span> <span class="tick" style="transform: translate(-148.634px, -69.3094px) rotate(-155deg);"></span> <span class="tick" style="transform: translate(-145.492px, -84px) rotate(-150deg);"></span> <span class="tick" style="transform: translate(-134.341px, -94.0665px) rotate(-145deg);"></span> <span class="tick" style="transform: translate(-125.631px, -105.417px) rotate(-140deg);"></span> <span class="tick" style="transform: translate(-115.966px, -115.966px) rotate(-135deg);"></span> <span class="tick" style="transform: translate(-105.417px, -125.631px) rotate(-130deg);"></span> <span class="tick" style="transform: translate(-94.0665px, -134.341px) rotate(-125deg);"></span> <span class="tick" style="transform: translate(-84px, -145.492px) rotate(-120deg);"></span> <span class="tick" style="transform: translate(-69.3094px, -148.634px) rotate(-115deg);"></span> <span class="tick" style="transform: translate(-56.0913px, -154.11px) rotate(-110deg);"></span> <span class="tick" style="transform: translate(-42.4463px, -158.412px) rotate(-105deg);"></span> <span class="tick" style="transform: translate(-28.4783px, -161.508px) rotate(-100deg);"></span> <span class="tick" style="transform: translate(-14.2935px, -163.376px) rotate(-95deg);"></span> <span class="tick" style="transform: translate(1.0287e-14px, -168px) rotate(-90deg);"></span> <span class="tick" style="transform: translate(14.2935px, -163.376px) rotate(-85deg);"></span> <span class="tick" style="transform: translate(28.4783px, -161.508px) rotate(-80deg);"></span> <span class="tick" style="transform: translate(42.4463px, -158.412px) rotate(-75deg);"></span> <span class="tick" style="transform: translate(56.0913px, -154.11px) rotate(-70deg);"></span> <span class="tick" style="transform: translate(69.3094px, -148.634px) rotate(-65deg);"></span> <span class="tick" style="transform: translate(84px, -145.492px) rotate(-60deg);"></span> <span class="tick" style="transform: translate(94.0665px, -134.341px) rotate(-55deg);"></span> <span class="tick" style="transform: translate(105.417px, -125.631px) rotate(-50deg);"></span> <span class="tick" style="transform: translate(115.966px, -115.966px) rotate(-45deg);"></span> <span class="tick" style="transform: translate(125.631px, -105.417px) rotate(-40deg);"></span> <span class="tick" style="transform: translate(134.341px, -94.0665px) rotate(-35deg);"></span> <span class="tick" style="transform: translate(145.492px, -84px) rotate(-30deg);"></span> <span class="tick" style="transform: translate(94.0665px, -134.341px) rotate(-55deg);"></span> <span class="tick" style="transform: translate(105.417px, -125.631px) rotate(-50deg);"></span> <span class="tick" style="transform: translate(115.966px, -115.966px) rotate(-45deg);"></span> <span class="tick" style="transform: translate(125.631px, -105.417px) rotate(-40deg);"></span> <span class="tick" style="transform: translate(134.341px, -94.0665px) rotate(-35deg);"></span> <span class="tick" style="transform: translate(145.492px, -84px) rotate(-30deg);"></span> <span class="tick" style="transform: translate(94.0665px, -134.341px) rotate(-55deg);"></span> <span class="tick" style="transform: translate(105.417px, -125.631px) rotate(-50deg);"></span> <span class="tick" style="transform: translate(115.966px, -115.966px) rotate(-45deg);"></span> <span class="tick" style="transform: translate(125.631px, -105.417px) rotate(-40deg);"></span> <span class="tick" style="transform: translate(134.341px, -94.0665px) rotate(-35deg);"></span> <span class="tick" style="transform: translate(145.492px, -84px) rotate(-30deg);"></span> <span class="tick" style="transform: translate(94.0665px, -134.341px) rotate(-55deg);"></span> <span class="tick" style="transform: translate(105.417px, -125.631px) rotate(-50deg);"></span> </div> <div class="digit-ring" ref="refDigit"> <span class="digit" style="transform: translate(-125.574px, 72.5px);">0</span> <span class="digit" style="transform: translate(-145px, -1.77574e-14px);">100</span> <span class="digit" style="transform: translate(-125.574px, -72.5px);">200</span> <span class="digit" style="transform: translate(-72.5px, -125.574px);">300</span> <span class="digit" style="transform: translate(8.87869e-15px, -145px);">400</span> <span class="digit" style="transform: translate(72.5px, -125.574px);">500</span> <span class="digit" style="transform: translate(125.574px, -0px);">600</span> <span class="digit" style="transform: translate(125.574px, -0px);">700</span> <span class="digit" style="transform: translate(125.574px, -0px);">800</span> <span class="digit" style="transform: translate(125.574px, -0px);">900</span> <span class="digit" style="transform: translate(125.574px, -0px);">1000</span> </div> <div class="progress" ref="refProgress" :style="{ transform: `rotate(${angle}deg)` }"> <img src="../assets/pointer.png" alt="" style="margin-top:-15px"> </div> </div> </template> <script> const outerRingRadius = 164; const digitRingRadius = 145; const frameCount = 100; const frameInterval = 0.3; // const digitValueMax = 1000; let number = 0; export default { name : 'SheerDialPressure', props : { pressure : { default : 0 }, differentialPressure : { default : null } }, data(){ return { angle: -150, statValueCurrent : 0, statValueInterval : 0, statValueMax : 0, isGreater : true } }, watch : { pressure : function(newVal,oldVal){ if(this.statValueMax == newVal)return; if(Number(newVal) >= Number(oldVal) != this.isGreater)number = 0; this.isGreater = Number(newVal) >= Number(oldVal); this.statValueMax = newVal; if(this.isGreater && number == 0)this.statValueCurrent = 0; ++number this.statValueIntervalOne = 0; this.update(); }, differentialPressure : function(newVal,oldVal){ let {high,low} = newVal; if(high == oldVal.high && low == oldVal.low)return; this._refScale(newVal); } }, created(){ }, mounted(){ this.initSpeedometer(); }, methods : { initSpeedometer : function(){ this.refDigit(); // this._refScale({high : null,low:null}); this._refScale({high : 700,low:200}); // this.update(); }, update : function(){ this.statValueInterval = this.statValueMax / frameCount; this.updateDetails(); }, updateDetails : function(){ let _flag = this.isGreater ? this.statValueCurrent.toFixed(2) > Number(this.statValueMax) : this.statValueCurrent.toFixed(2) < Number(this.statValueMax) if (_flag) { return; } this.setStatValue(); if(this.isGreater){this.statValueCurrent += this.statValueInterval;}else{this.statValueCurrent -= this.statValueInterval;} const _timer = setTimeout(() => { this.updateDetails(); clearTimeout(_timer); }, frameInterval); }, refDigit : function(){ let _childrenDigit = this.$refs.refDigit.children; const _num = {0 : {x:5,y:65},1:{x:0,y:65},2:{x:-2,y:60},3:{x:-2,y:55},4:{x:5,y:50},5:{x:10,y:50},6:{x:15,y:50},7:{x:20,y:50},8:{x:25,y:60},9:{x:25,y:60},10:{x:25,y:65}} for (let index = 0; index < _childrenDigit.length; index++) { let angle = 240 - index * 30; let theta = this.deg2rad(angle); let x = (Math.cos(theta) * digitRingRadius) * 0.432 - _num[index].x; let y = (Math.sin(theta) * -digitRingRadius) * 0.432 - _num[index].y; this.$refs.refDigit.children[index].style.transform = `translate(${x}px, ${y}px)`; } }, _refScale : function(value){ let {high,low} = value; let _children = this.$refs.refScale.children; for (let index = 0; index < _children.length; index++) { let angle = 240 - index * 6; let theta = this.deg2rad(angle); let radius = outerRingRadius + (index % 6 ? 0 : 4); let x = (Math.cos(theta) * radius) * 0.432; let y = (Math.sin(theta) * -radius) * 0.432; let transform = [`translate(${x}px, ${y }px)`, `rotate(${-angle}deg)`].join(' '); this.$refs.refScale.children[index].style.transform = transform; let _flag = index % 5 === 0; if(high != null || low != null){ let _high = `${(index * 20 < high) && (index * 20 > low)? 'rgb(15 194 158 / 6%) 0px 0px 1rem 0.8rem' : 'rgb(220 37 47 / 6%) 0px 0px 1rem 0.8rem'}`; let _color = `${(index * 20 < high) && (index * 20 > low)? `2px solid rgb(15 194 158 / ${_flag ? '60' : '20'}%)` : `2px solid rgb(220 37 47 / ${_flag ? '60' : '20'}%)`}`; this.$refs.refScale.children[index].style.boxShadow = _high; this.$refs.refScale.children[index].style.borderTop = _color; } } }, setStatValue : function(){ const clampedValue = Math.min(Math.max(this.statValueMax, 0), 1000); this.angle = (clampedValue - 0) * (150 - (-150)) / (1000 - 0) + (-150) }, deg2rad : function(angle){ return angle * (Math.PI / 180); }, rad2deg : function(angle){ return angle * (180 / Math.PI); } } } </script> <style scoped> .speedometer { font-size: 10px; display: flex; justify-content: center; align-items: center; font-family: "Roboto", sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; line-height: 1; color: #666; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; } /** Speedometer Styles **/ .speedometer-container { width: 159.83px; height: 149.53px; position: relative; } /* .speedometer-container::before { content: ""; position: absolute; top: 0; left: 0; right: 0; bottom: 0; opacity: 0.12; } */ .overlay { pointer-events: none; } .overlay::before, .overlay::after { content: ""; position: absolute; top: 0; left: 0; right: 0; bottom: 0; } .overlay::before { background-image: radial-gradient(circle at top left, #25fabb, transparent); opacity: 0.04; } .overlay::after { background-image: radial-gradient(circle at bottom right, #874bd7, transparent); opacity: 0.2; } .speedometer .inner-ring { width: 5rem; height: 5rem; transform: rotate(-60deg); position: absolute; /* top: calc(50% - 12.5rem); */ /* left: calc(50% - 10.5rem); */ } .speedometer .inner-ring::before, .speedometer .inner-ring::after { content: ""; position: absolute; top: 0; left: 0; right: 0; bottom: 0; border: 2px solid transparent; border-top: 2px solid #3b3d45; border-right: 2px solid #3b3d45; border-bottom: 2px solid #3b3d45; border-radius: 50%; } .speedometer .inner-ring::before { -webkit-transform: rotate(317deg); transform: rotate(317deg); } .speedometer .inner-ring::after { -webkit-transform: rotate(-15deg); transform: rotate(-15deg); } .speedometer .outer-ring { /* width: 15.9rem; height: 15.9rem; */ position: absolute; /* top: calc(50% - 18rem); */ /* left: calc(50% - 16rem); */ border-radius: 50%; } .speedometer .digit-ring { position: absolute; top: calc(75%); left: 50%; } .speedometer .tick { width: 0.8rem; border-top: 2px solid #3b3d45; position: absolute; top: calc(50% - 0.1rem); left: calc(50% - 0.4rem); } .speedometer .tick:nth-child(5n+1) { width: 1rem; left: calc(50% - 0.6rem); border-color: #787a81; } .speedometer .digit { /* width: 2rem; height: 2rem; */ position: absolute; /* top: calc(50% - 1rem); */ /* left: calc(50% - 1rem); */ font-weight: bold; text-align: center; line-height: 2rem; } .speedometer .details { display: flex; flex-direction: column; justify-content: center; align-items: center; width: 9.6rem; height: 9.6rem; position: absolute; top: calc(50% - 12.5rem); left: calc(50% - 10.5rem); } .speedometer .label { font-size: 1.2rem; font-weight: bold; text-transform: uppercase; } .speedometer .speed { font-size: 6rem; color: #fff; } .speedometer .unit { font-size: 1.6rem; } .speedometer .progress { width: 5rem; height: 5rem; position: absolute; text-align: center; transition: transform 0.3s linear; /* 添加以下属性来避免重置 */ transform-origin: center; will-change: transform; /* background-image: url('../assets/pointer.png'); background-repeat: no-repeat; background-attachment: fixed; */ /* top: calc(50% - 12.5rem); */ /* left: calc(50% - 10.5rem); */ /* border-radius: 50%; */ } .speedometer .progress-one { width: 9.6rem; height: 9.6rem; position: absolute; /* top: calc(50% - 12.5rem); */ /* left: calc(50% - 10.5rem); */ border-radius: 50%; } .speedometer .progress-one::before { content: ""; position: absolute; top: -0.2rem; left: calc(50% - 0.3rem); width: 0.6rem; height: 0.6rem; border-radius: 50%; background-color: #ffbe00; box-shadow: 0 0 6rem 2rem rgba(255, 174, 0, 0.35); } /* .speedometer .progress::before { content: ""; position: absolute; top: -0.2rem; left: calc(50% - 0.3rem); width: 0.6rem; height: 0.6rem; border-radius: 50%; background-color: #0de882; box-shadow: 0 0 6rem 2rem rgba(15, 194, 182, 0.35); } */ .speedometer .retry-button { width: 10rem; border: 2px solid #3b3d45; -webkit-appearance: none; -moz-appearance: none; appearance: none; position: absolute; left: calc(50% - 5rem); bottom: 13.5rem; font-size: 1.2rem; font-weight: bold; text-align: center; text-transform: uppercase; line-height: 3rem; color: #666; border-radius: 3rem; background-color: transparent; cursor: pointer; outline: none; transition: background-color 250ms ease-out; } .speedometer .retry-button:hover, .speedometer .retry-button:focus { background-color: rgba(59,61,69,0.15); } .speedometer footer { display: flex; justify-content: center; padding: 3.5rem 0; position: absolute; top: auto; left: 0; right: 0; bottom: 0; } .speedometer .stat { flex-grow: 1; width: 0; padding: 1rem 0; text-align: center; } .speedometer .stat:not(:last-child) { border-right: 2px solid rgba(255,255,255,0.05); } .speedometer .stat label { display: block; margin-bottom: 0.75rem; font-size: 1.2rem; font-weight: bold; text-transform: uppercase; } .speedometer .stat p { font-size: 1.4rem; color: #fff; } </style>
如果对你有用的话,可以打赏哦
打赏
ali pay
wechat pay

本文作者:还是夸张一点

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

还是夸张一点技术专栏 © 2019 - 2023 | 滇ICP备2022001556号
世间情动不过盛夏白瓷梅子汤,碎冰碰壁当啷响。