Ant Design Statistic组件深度实践与常见问题解决方案

Newb.淑丽 组件 阅读 573
赞 11 收藏
二维码
手机扫码查看
反馈

一个数据大屏项目的意外需求

最近做的那个数据大屏项目,本来以为就是常规的数据展示,结果产品突然提了个需求:要在大屏幕上显示一些关键的统计数据,比如实时用户数、交易金额、订单量这些,而且要求要有动画效果,数字要从0滚到目标值。

Ant Design Statistic组件深度实践与常见问题解决方案

开始没想到这个看起来简单的功能会有这么多坑,之前也没专门研究过Statistic组件,就想着用现成的UI库吧,Ant Design的Statistic看起来不错,就开始用了。

基础实现倒是挺简单

最开始的代码很基础:

import { Statistic } from 'antd';
import CountUp from 'react-countup';

// 简单的计数器组件
const SimpleCounter = ({ title, value, prefix, suffix }) => {
  return (
    <div className="stat-item">
      <div className="title">{title}</div>
      <CountUp 
        start={0} 
        end={value} 
        duration={2}
        prefix={prefix || ''}
        suffix={suffix || ''}
        separator=","
      >
        {({ countUpRef }) => (
          <div className="value" ref={countUpRef} />
        )}
      </CountUp>
    </div>
  );
};

// 或者直接用Antd的Statistic
const AntdCounter = ({ title, value }) => {
  return (
    <Statistic 
      title={title} 
      value={value}
      formatter={(val) => val.toLocaleString()}
    />
  );
};

基础功能很快实现了,但项目中遇到了几个问题,特别是性能方面的问题。

最大的坑:批量更新时的性能问题

项目中有个需求是要同时更新多个统计数据,每隔几秒刷新一次,刚开始测试的时候只有3-4个指标,感觉还行,但后来加到了10多个,页面就开始卡顿了。

开始没想到问题出在哪里,折腾了半天才发现是每次更新都会触发所有的CountUp动画,而且是在同一个时间点触发,浏览器一下要处理十几个动画,CPU占用直接飙升到70%+。

后来调整了方案,加了防抖和分批更新:

import { useState, useEffect, useCallback } from 'react';
import { debounce } from 'lodash';

const BatchStats = () => {
  const [stats, setStats] = useState([
    { id: 1, title: '用户数', value: 0 },
    { id: 2, title: '订单数', value: 0 },
    { id: 3, title: '收入', value: 0 }
  ]);

  // 防抖更新函数
  const debouncedUpdate = useCallback(
    debounce((newStats) => {
      setStats(prev => prev.map((stat, index) => ({
        ...stat,
        value: newStats[index]?.value || stat.value
      })));
    }, 300),
    []
  );

  // 分批延迟更新,避免同时触发所有动画
  const updateStatsWithDelay = (newStats) => {
    newStats.forEach((stat, index) => {
      setTimeout(() => {
        setStats(prev => prev.map(p => p.id === stat.id ? { ...p, value: stat.value } : p));
      }, index * 100); // 每个间隔100ms
    });
  };

  useEffect(() => {
    const fetchStats = async () => {
      try {
        const response = await fetch('https://jztheme.com/api/stats');
        const data = await response.json();
        updateStatsWithDelay(data);
      } catch (error) {
        console.error('Failed to fetch stats:', error);
      }
    };

    fetchStats();
    const interval = setInterval(fetchStats, 5000);
    return () => clearInterval(interval);
  }, []);

  return (
    <div className="stats-container">
      {stats.map(stat => (
        <div key={stat.id} className="stat-card">
          <div className="stat-title">{stat.title}</div>
          <CountUp 
            start={0} 
            end={stat.value} 
            duration={1.5}
            separator=","
            preserveValue={true} // 保持当前值,避免重置
          />
        </div>
      ))}
    </div>
  );
};

这里要注意我踩过好几次坑的地方:preserveValue属性很重要,不然每次父组件重新渲染,动画都会从0开始。还有就是分批延迟更新,虽然用户体验稍微差一点(不是同时更新),但是性能好了很多。

另一个坑:数字格式化和精度问题

数据大屏里经常要显示很大的数字,比如交易金额可能达到百万级别,这时候需要做格式化处理。但项目中遇到了精度问题,特别是在处理浮点数的时候。

const formatNumber = (num, decimals = 2) => {
  if (typeof num !== 'number') return num;
  
  // 处理科学计数法
  const formatted = Number(num).toLocaleString('en-US', {
    minimumFractionDigits: decimals,
    maximumFractionDigits: decimals
  });

  return formatted;
};

// 自定义的Statistic组件
const CustomStat = ({ title, value, unit = '', type = 'number' }) => {
  const [displayValue, setDisplayValue] = useState(0);

  useEffect(() => {
    // 使用缓动动画,而不是线性动画
    let start = displayValue;
    const end = value;
    const duration = 1500; // 1.5秒
    const startTime = Date.now();

    const animate = () => {
      const elapsed = Date.now() - startTime;
      const progress = Math.min(elapsed / duration, 1);
      
      // 缓动函数
      const easeProgress = 1 - Math.pow(1 - progress, 3);
      const currentValue = start + (end - start) * easeProgress;

      setDisplayValue(currentValue);

      if (progress < 1) {
        requestAnimationFrame(animate);
      } else {
        setDisplayValue(end); // 最终设置精确值
      }
    };

    animate();
  }, [value]);

  return (
    <div className="custom-stat">
      <div className="stat-label">{title}</div>
      <div className="stat-value">
        {formatNumber(displayValue, type === 'money' ? 2 : 0)}
        {unit && <span className="unit">{unit}</span>}
      </div>
    </div>
  );
};

这里折腾了半天发现,如果用第三方的动画库,有时候会因为精度问题导致显示的数字不够准确,特别是在大数据场景下。所以最后还是自己实现了简单的动画逻辑,这样控制起来更灵活。

CSS样式也有些小麻烦

数据大屏对视觉效果要求比较高,光有动画还不够,样式也要够炫。但Statistic组件默认的样式比较基础,需要大量的自定义。

.stats-container {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 20px;
  padding: 20px;
}

.stat-card {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  border-radius: 10px;
  padding: 20px;
  color: white;
  box-shadow: 0 4px 15px rgba(0,0,0,0.2);
  transition: transform 0.3s ease;
}

.stat-card:hover {
  transform: translateY(-5px);
}

.stat-title {
  font-size: 14px;
  opacity: 0.8;
  margin-bottom: 10px;
}

.stat-value {
  font-size: 28px;
  font-weight: bold;
  text-align: center;
  animation: glow 2s ease-in-out infinite alternate;
}

@keyframes glow {
  from { text-shadow: 0 0 5px #fff; }
  to { text-shadow: 0 0 20px #fff, 0 0 30px #e60073; }
}

.unit {
  font-size: 16px;
  margin-left: 5px;
}

这里的CSS动画效果看起来不错,但在低端设备上可能会有性能问题,所以实际部署的时候还需要根据设备性能动态调整。

最终效果和遗留问题

经过一番折腾,最终的效果还算满意。数据显示比较流畅,动画效果也达到了预期。CPU占用基本维持在20%-30%左右,比最初的一下子70%好多了。

不过还是有些小问题没有完美解决:比如在移动设备上偶尔还是会有点卡顿,还有就是当数据变化特别大的时候(比如从几万直接跳到几十万),动画时间会显得很长。这些问题暂时影响不大,就没继续深究了。

这个组件最后在项目中复用性还挺高的,封装成了一个通用的数据展示组件,后面其他模块也有用到。

回顾与反思

这次的经验告诉我,看似简单的数字统计功能,实际落地的时候还是要考虑很多因素:性能、用户体验、兼容性等等。特别是对于数据大屏这种场景,用户的容忍度比较低,任何卡顿都会很明显。

以后再遇到类似的需求,我会提前做好性能测试,特别是在数据量比较大的情况下。另外也会考虑要不要做一些降级处理,比如移动端或者性能较差的设备上关闭复杂的动画效果。

以上是我个人对这个Statistic组件应用的完整讲解,有更优的实现方式欢迎评论区交流。这个技巧的拓展用法还有很多,比如结合WebSocket实时推送数据等,后续会继续分享这类博客。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论