ai-coding
2026-07-28 c9519c5ab314edd99d2fcee6ec8e05a55bd74267
refactor(temperature): 优化加载结束后的回调时序并修复闭包旧值问题

Co-Authored-By: 甄雪君 <zhenxj@xiaopeng.com>
2个文件已修改
81 ■■■■■ 已修改文件
src/robotaxi/pages/begin-route/components/calling-car-modules/temperature-control.tsx 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/use-minimum-spin-loading.ts 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/temperature-control.tsx
@@ -214,15 +214,13 @@
            }
        } catch {}
        stopLoading('ac');
        if (!acSuccess) {
            showToast({ title: AC_TOGGLE_FAIL_TEXT, icon: 'none' });
        } else {
            if (!tempSuccess) {
        stopLoading('ac', () => {
            if (!acSuccess) {
                showToast({ title: AC_TOGGLE_FAIL_TEXT, icon: 'none' });
            } else if (!tempSuccess) {
                showToast({ title: AC_TEMP_FAIL_TEXT, icon: 'none' });
            }
        }
        });
        saveAcStorage(targetOrderId, { acOpen: true, temperature: DEFAULT_FIRST_TEMP });
    };
@@ -412,6 +410,7 @@
        const targetOpenState = !acSwitchOn;
        setAcTogglingTarget(targetOpenState);
        startLoading('ac');
        let toastText = '';
        try {
            // 200就认为是指令下发成功了
            await airConditionerUnlock({
@@ -432,15 +431,20 @@
            userActionSeqRef.current++;
            const currentTemp = typeof tempStatus === 'number' ? tempStatus : DEFAULT_FIRST_TEMP;
            saveAcStorage(targetOrderId, { acOpen: targetOpenState, temperature: currentTemp });
            showToast({ title: targetOpenState ? '已开启空调' : '已关闭空调', icon: 'none' });
            toastText = targetOpenState ? '已开启空调' : '已关闭空调';
            if (targetOpenState && typeof tempStatus !== 'number') {
                setTempStatus(DEFAULT_FIRST_TEMP);
                lastConfirmedTempRef.current = DEFAULT_FIRST_TEMP;
            }
        } catch (error) {
            showToast({ title: AC_TOGGLE_FAIL_TEXT, icon: 'none' });
            toastText = AC_TOGGLE_FAIL_TEXT;
        } finally {
            stopLoading('ac');
            // 等 loading 动画真正结束后再弹 toast,避免异常提示已弹出但 loading 仍在转
            stopLoading('ac', () => {
                if (toastText) {
                    showToast({ title: toastText, icon: 'none' });
                }
            });
        }
    };
src/robotaxi/pages/begin-route/components/calling-car-modules/use-minimum-spin-loading.ts
@@ -9,10 +9,20 @@
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);
  }, []);
  /**
   * 清理延迟关闭定时器。
@@ -36,33 +46,34 @@
  const startLoading = useCallback((key: T) => {
    clearLoadingFinishTimer();
    loadingStartedAtRef.current = Date.now();
    setVisibleLoadingKey(key);
  }, [clearLoadingFinishTimer]);
    setKey(key);
  }, [clearLoadingFinishTimer, setKey]);
  /**
   * 结束 loading 动画;若未满一圈则延迟到一圈完成后再关闭。
   * @param onStopped loading 真正关闭(含最短时长补足)后触发,用于把 toast 等提示对齐到动画结束,避免 loading 仍在转就弹出提示。
   */
  const stopLoading = useCallback((key?: T | null) => {
    setVisibleLoadingKey((currentKey) => {
      if (!currentKey) {
        return currentKey;
      }
      if (key && currentKey !== key) {
        return currentKey;
      }
      const elapsed = Date.now() - loadingStartedAtRef.current;
      const remaining = Math.max(0, MINIMUM_SPIN_DURATION_MS - elapsed);
      clearLoadingFinishTimer();
      if (remaining === 0) {
        return null;
      }
      loadingFinishTimerRef.current = setTimeout(() => {
        setVisibleLoadingKey((latestKey) => (latestKey === currentKey ? null : latestKey));
        loadingFinishTimerRef.current = null;
      }, remaining);
      return currentKey;
    });
  }, [clearLoadingFinishTimer]);
  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,