zjk
2026-07-29 429208957f30ab9eedc5533a81aaa284048d09f5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import { useCallback, useEffect, useRef, useState } from 'react';
 
/** loading 动画至少完成一圈的展示时长(毫秒)。 */
const MINIMUM_SPIN_DURATION_MS = 1000;
 
/**
 * 控制 loading 动画最短展示时长,避免图标未完成一圈旋转就切回普通态。
 */
export default function useMinimumSpinLoading<T extends string>() {
  /** 当前处于 loading 展示中的控件标识。 */
  const [visibleLoadingKey, setVisibleLoadingKey] = useState<T | null>(null);
  /** visibleLoadingKey 的同步镜像,供 stopLoading 内即时读取,避免闭包拿到旧值。 */
  const visibleLoadingKeyRef = useRef<T | null>(null);
  /** 当前 loading 开始时间戳。 */
  const loadingStartedAtRef = useRef(0);
  /** 延迟关闭 loading 的定时器。 */
  const loadingFinishTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
 
  /**
   * 同步更新 state 与 ref,保证两者一致。
   */
  const setKey = useCallback((key: T | null) => {
    visibleLoadingKeyRef.current = key;
    setVisibleLoadingKey(key);
  }, []);
 
  /**
   * 清理延迟关闭定时器。
   */
  const clearLoadingFinishTimer = useCallback(() => {
    if (loadingFinishTimerRef.current) {
      clearTimeout(loadingFinishTimerRef.current);
      loadingFinishTimerRef.current = null;
    }
  }, []);
 
  useEffect(() => {
    return () => {
      clearLoadingFinishTimer();
    };
  }, [clearLoadingFinishTimer]);
 
  /**
   * 开始展示 loading 动画。
   */
  const startLoading = useCallback((key: T) => {
    clearLoadingFinishTimer();
    loadingStartedAtRef.current = Date.now();
    setKey(key);
  }, [clearLoadingFinishTimer, setKey]);
 
  /**
   * 结束 loading 动画;若未满一圈则延迟到一圈完成后再关闭。
   * @param onStopped loading 真正关闭(含最短时长补足)后触发,用于把 toast 等提示对齐到动画结束,避免 loading 仍在转就弹出提示。
   */
  const stopLoading = useCallback((key?: T | null, onStopped?: () => void) => {
    const currentKey = visibleLoadingKeyRef.current;
    // 无 loading 或指定 key 不匹配:不关闭,但仍执行回调作为兜底,保证提示不丢失。
    if (!currentKey || (key && currentKey !== key)) {
      onStopped?.();
      return;
    }
    const elapsed = Date.now() - loadingStartedAtRef.current;
    const remaining = Math.max(0, MINIMUM_SPIN_DURATION_MS - elapsed);
    clearLoadingFinishTimer();
    if (remaining === 0) {
      setKey(null);
      onStopped?.();
      return;
    }
    loadingFinishTimerRef.current = setTimeout(() => {
      setKey(null);
      loadingFinishTimerRef.current = null;
      onStopped?.();
    }, remaining);
  }, [clearLoadingFinishTimer, setKey]);
 
  return {
    visibleLoadingKey,
    startLoading,
    stopLoading
  };
}