index.tsx 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. import { isFunction } from 'lodash-es';
  2. import React, { useCallback, useEffect, useRef, useState } from 'react';
  3. interface CountdownProps {
  4. duration: number;
  5. onTimeout?: (() => void) | null;
  6. loading: boolean;
  7. title?: string | React.ReactNode | ((countdown: number) => React.ReactNode);
  8. color?: string;
  9. }
  10. const Countdown: React.FC<CountdownProps> = React.memo(
  11. ({ duration, onTimeout, loading, color = '#faad14', title = '下次刷新:' }) => {
  12. const timerRef = useRef<NodeJS.Timeout | null>(null);
  13. const [countdown, setCountdown] = useState(duration);
  14. const onTimeoutRef = useRef(onTimeout);
  15. const loadingRef = useRef(loading);
  16. useEffect(() => {
  17. onTimeoutRef.current = onTimeout;
  18. }, [onTimeout]);
  19. useEffect(() => {
  20. loadingRef.current = loading;
  21. }, [loading]);
  22. // 启动定时器的函数
  23. const startTimer = useCallback(() => {
  24. timerRef.current = setInterval(() => {
  25. setCountdown((prev) => {
  26. const newCountdown = prev > 1 ? prev - 1 : duration;
  27. if (prev > 1) return newCountdown;
  28. setTimeout(() => {
  29. onTimeoutRef.current?.();
  30. }, 50);
  31. return newCountdown;
  32. });
  33. }, 1000);
  34. }, [duration]);
  35. useEffect(() => {
  36. // duration, loading, startTimer 任意一项产生变化时,都需要重置倒计时,并清除之前的定时器
  37. // 重置倒计时
  38. setCountdown(duration);
  39. // 清除之前的定时器
  40. if (timerRef.current) {
  41. clearInterval(timerRef.current);
  42. timerRef.current = null;
  43. }
  44. // 如果 loading 为 true,不启动定时器
  45. if (loading) return;
  46. startTimer();
  47. return () => {
  48. if (timerRef.current) {
  49. clearInterval(timerRef.current);
  50. timerRef.current = null;
  51. }
  52. };
  53. }, [duration, loading, startTimer]);
  54. // 监听页面可见性变化,当页面隐藏时暂停计时器,显示时恢复
  55. useEffect(() => {
  56. const handleVisibilityChange = () => {
  57. if (document.hidden) {
  58. // 页面隐藏:暂停计时器
  59. if (timerRef.current) {
  60. clearInterval(timerRef.current);
  61. timerRef.current = null;
  62. }
  63. } else {
  64. // 页面显示:恢复计时器
  65. if (!loadingRef.current && !timerRef.current) {
  66. // 只有在 loading 为 false 且定时器未运行时才恢复
  67. startTimer();
  68. }
  69. }
  70. };
  71. document.addEventListener('visibilitychange', handleVisibilityChange);
  72. return () => {
  73. document.removeEventListener('visibilitychange', handleVisibilityChange);
  74. };
  75. }, [startTimer]);
  76. return (
  77. <span style={{ color, marginRight: 16 }}>
  78. {isFunction(title) ? (
  79. title(countdown)
  80. ) : (
  81. <>
  82. {title}
  83. {countdown}s
  84. </>
  85. )}
  86. </span>
  87. );
  88. },
  89. (prevProps, nextProps) => {
  90. // 自定义比较函数:只比较影响 UI 和行为的 props,忽略 onTimeout 的变化(函数引用变化不影响 UI 渲染,通过 ref 存储,并在 useEffect 中更新,不重新渲染不会影响功能)
  91. return (
  92. prevProps.duration === nextProps.duration &&
  93. prevProps.loading === nextProps.loading &&
  94. prevProps.title === nextProps.title &&
  95. prevProps.color === nextProps.color
  96. );
  97. },
  98. );
  99. export default Countdown;