hejianhao
2026-05-25 701fa5182a769b301bcdbced37e0236ab5070e9b
refactor(robotaxi/modify-destination): 优化修改目的地流程并适配安全区域

- 重构useModifyDestinationFlow钩子,新增流程状态枚举管理各个阶段
- 封装安全的本地存储读写工具,处理缓存异常避免影响主流程
- 统一本地状态重置逻辑,拆分复用函数优化代码可读性
- 调整release-route.less底部内边距,添加env(safe-area-inset-bottom)适配全面屏安全区域
- 移除未使用的getOrderActive API导入
2个文件已修改
295 ■■■■■ 已修改文件
src/robotaxi/pages/begin-route/components/css-folder/release-route.less 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-modify-destination-flow.ts 293 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/release-route.less
@@ -143,7 +143,7 @@
    .select-site {
        position: relative;
        padding: 32px;
        padding-bottom: 232px;
        padding-bottom: calc(232px + env(safe-area-inset-bottom));
        padding-top: 40px;
        background: #F2F3F7;
        border-top: 2px solid #fff;
src/robotaxi/pages/begin-route/hooks/use-modify-destination-flow.ts
@@ -1,7 +1,6 @@
import Taro from '@tarojs/taro';
import { useEffect, useMemo, useState } from 'react';
import { orderChange } from '../../../api/begin-route';
import { getOrderActive } from '../../../api/home';
import { TripStatus } from '../constants/trip-status';
import {
  MODIFY_DESTINATION_CANDIDATE_KEY,
@@ -28,13 +27,64 @@
  shouldRestorePreviousEndSite?: boolean;
};
/**
 * 修改目的地流程状态。
 */
const FLOW_STATES = {
  IDLE: 'idle',
  SELECTING: 'selecting',
  PREVIEWING: 'previewing',
  CONFIRMING: 'confirming'
} as const;
/**
 * 修改目的地流程状态类型。
 */
type FlowState = typeof FLOW_STATES[keyof typeof FLOW_STATES];
/** 终点缓存 key。 */
const END_SITE_STORAGE_KEY = 'endSite';
/** 订单 ID 缓存 key。 */
const ORDER_ID_STORAGE_KEY = 'orderId';
/**
 * 修改目的地会话缓存结构。
 */
type ModifyDestinationSession = {
  active?: boolean;
  previousEndSite?: any;
  previousFareEstimate?: any;
  orderId?: string;
  tripStatus?: TripStatus;
  updatedAt?: number;
};
/**
 * 修改目的地流程恢复快照。
 */
type ModifyDestinationSnapshot = {
  session: ModifyDestinationSession | null;
  candidate: any;
  cachedOverride: any;
  cachedRoutePoints: any[];
};
/**
 * 判断值是否可转为有效坐标数字。
 */
const isFiniteCoordinate = (value: any) => Number.isFinite(Number(value));
/**
 * 判断站点对象是否具备有效经纬度。
 */
const isValidSite = (site: any) => {
  if (!site) return false;
  return isFiniteCoordinate(site.latitude) && isFiniteCoordinate(site.longitude);
};
/**
 * 判断两个站点是否表示同一个地点。
 */
const isSameSite = (left: any, right: any) => {
  if (!left || !right) return false;
  if (left.id && right.id) {
@@ -45,23 +95,71 @@
    && Number(left.longitude) === Number(right.longitude);
};
const clearModifyDestinationSession = () => {
  Taro.removeStorageSync(MODIFY_DESTINATION_SESSION_KEY);
  Taro.removeStorageSync(MODIFY_DESTINATION_CANDIDATE_KEY);
  Taro.removeStorageSync(MODIFY_DESTINATION_ROUTE_POINTS_KEY);
  Taro.removeStorageSync(MODIFY_DESTINATION_OVERRIDE_KEY);
/**
 * 安全读取缓存,异常时返回兜底值。
 */
const safeGetStorageSync = <T,>(key: string, fallback: T): T => {
  try {
    const value = Taro.getStorageSync(key);
    return (value ?? fallback) as T;
  } catch (error) {
    return fallback;
  }
};
/**
 * 安全写入缓存。
 */
const safeSetStorageSync = (key: string, value: any) => {
  try {
    Taro.setStorageSync(key, value);
  } catch (error) {
    // 缓存写入失败时保持静默,避免影响主流程。
  }
};
/**
 * 安全移除缓存。
 */
const safeRemoveStorageSync = (key: string) => {
  try {
    Taro.removeStorageSync(key);
  } catch (error) {
    // 缓存移除失败时保持静默,避免影响主流程。
  }
};
/**
 * 清理修改目的地流程相关缓存。
 */
const clearModifyDestinationSession = () => {
  safeRemoveStorageSync(MODIFY_DESTINATION_SESSION_KEY);
  safeRemoveStorageSync(MODIFY_DESTINATION_CANDIDATE_KEY);
  safeRemoveStorageSync(MODIFY_DESTINATION_ROUTE_POINTS_KEY);
  safeRemoveStorageSync(MODIFY_DESTINATION_OVERRIDE_KEY);
};
/**
 * 读取修改目的地流程快照。
 */
const readModifyDestinationSnapshot = (): ModifyDestinationSnapshot => ({
  session: safeGetStorageSync<ModifyDestinationSession | null>(MODIFY_DESTINATION_SESSION_KEY, null),
  candidate: safeGetStorageSync<any>(MODIFY_DESTINATION_CANDIDATE_KEY, null),
  cachedOverride: safeGetStorageSync<any>(MODIFY_DESTINATION_OVERRIDE_KEY, null),
  cachedRoutePoints: safeGetStorageSync<any[]>(MODIFY_DESTINATION_ROUTE_POINTS_KEY, [])
});
export default function useModifyDestinationFlow(deps: Deps) {
  const [previewEndSite, setPreviewEndSite] = useState<any>(null);
  const [previousEndSite, setPreviousEndSite] = useState<any>(null);
  const [previousFareEstimate, setPreviousFareEstimate] = useState<any>(null);
  const [previousRoutePoints, setPreviousRoutePoints] = useState<any[]>([]);
  const [flowState, setFlowState] = useState<FlowState>(FLOW_STATES.IDLE); // 修改目的地流程状态
  const [isConfirmingModifyDestination, setIsConfirmingModifyDestination] = useState(false); // 确认修改请求中状态,控制按钮蒙层与防重复提交
  const [overrideEndSite, setOverrideEndSite] = useState<any>(() => {
    const cached = Taro.getStorageSync(MODIFY_DESTINATION_OVERRIDE_KEY);
    const cached = safeGetStorageSync<any>(MODIFY_DESTINATION_OVERRIDE_KEY, null);
    return isValidSite(cached) ? cached : null;
  });
  }); // 已提交但页面内尚未完全同步时使用的本地终点覆盖值
  const MODIFIABLE_TRIP_STATUSES: TripStatus[] = [
    TripStatus.VehicleApproaching,
@@ -70,75 +168,118 @@
    TripStatus.ArrivedDestination,
    TripStatus.TripFinished,
    TripStatus.PaymentSuccess
  ]; // 可修改目的地的行程状态
  ]; // 允许进入“修改目的地”流程的行程状态
  /**
   * 统一重置本地预览态数据。
   */
  const resetLocalReviewState = () => {
    setPreviewEndSite(null);
    setPreviousEndSite(null);
    setPreviousFareEstimate(null);
    setPreviousRoutePoints([]);
  };
  /**
   * 应用“待选终点”状态。
   */
  const applySelectingState = (snapshot: ModifyDestinationSnapshot) => {
    const session = snapshot.session;
    setPreviewEndSite(null);
    setPreviousEndSite(session?.previousEndSite || deps.endSite || null);
    setPreviousFareEstimate(session?.previousFareEstimate || null);
    setPreviousRoutePoints(Array.isArray(snapshot.cachedRoutePoints) ? snapshot.cachedRoutePoints : []);
  };
  /**
   * 应用“预览终点”状态。
   */
  const applyPreviewingState = (snapshot: ModifyDestinationSnapshot) => {
    const session = snapshot.session;
    setPreviousEndSite(session?.previousEndSite || deps.endSite || null);
    setPreviousFareEstimate(session?.previousFareEstimate || null);
    setPreviousRoutePoints(Array.isArray(snapshot.cachedRoutePoints) ? snapshot.cachedRoutePoints : []);
    setPreviewEndSite(snapshot.candidate);
  };
  /**
   * 根据快照推导当前流程状态。
   */
  const resolveFlowState = (snapshot: ModifyDestinationSnapshot) => {
    const session = snapshot.session;
    const isTripModifiable = MODIFIABLE_TRIP_STATUSES.includes(deps.tripStatus);
    const isSessionActive = Boolean(session?.active);
    const isSessionOrderMatched = !session?.orderId || !deps.orderId || String(session.orderId) === String(deps.orderId);
    if (!isTripModifiable) {
      return FLOW_STATES.IDLE;
    }
    if (!isSessionActive) {
      return FLOW_STATES.IDLE;
    }
    if (!isSessionOrderMatched) {
      return FLOW_STATES.IDLE;
    }
    if (isValidSite(snapshot.candidate)) {
      return FLOW_STATES.PREVIEWING;
    }
    return FLOW_STATES.SELECTING;
  };
  /**
   * 根据缓存快照恢复修改目的地流程,并同步预览态 / 待选态 / 本地覆盖态。
   */
  useEffect(() => {
    const session = Taro.getStorageSync(MODIFY_DESTINATION_SESSION_KEY);
    const candidate = Taro.getStorageSync(MODIFY_DESTINATION_CANDIDATE_KEY);
    const cachedOverride = Taro.getStorageSync(MODIFY_DESTINATION_OVERRIDE_KEY);
    const cachedRoutePoints = Taro.getStorageSync(MODIFY_DESTINATION_ROUTE_POINTS_KEY);
    const snapshot = readModifyDestinationSnapshot();
    const session = snapshot.session;
    const nextFlowState = resolveFlowState(snapshot);
    const isSessionOrderMatched = !session?.orderId || !deps.orderId || String(session.orderId) === String(deps.orderId); // 恢复早期 orderId 为空时不视为订单不匹配
    if (isValidSite(cachedOverride)) {
      setOverrideEndSite(cachedOverride);
    if (isValidSite(snapshot.cachedOverride)) {
      setOverrideEndSite(snapshot.cachedOverride);
    }
    if (session?.active && !isSessionOrderMatched) {
      clearModifyDestinationSession();
      setPreviewEndSite(null);
      setPreviousEndSite(null);
      setPreviousFareEstimate(null);
      setPreviousRoutePoints([]);
      resetLocalReviewState();
      setFlowState(FLOW_STATES.IDLE);
      return;
    }
    if (session?.active && isValidSite(candidate)) {
      if (isSameSite(session.previousEndSite, candidate)) {
    if (nextFlowState === FLOW_STATES.PREVIEWING) {
      if (isSameSite(session?.previousEndSite, snapshot.candidate)) {
        deps.onCancelReview?.({
          previousEndSite: session.previousEndSite || deps.endSite || null,
          previousFareEstimate: session.previousFareEstimate || null,
          previousRoutePoints: Array.isArray(cachedRoutePoints) ? cachedRoutePoints : []
          previousEndSite: session?.previousEndSite || deps.endSite || null,
          previousFareEstimate: session?.previousFareEstimate || null,
          previousRoutePoints: Array.isArray(snapshot.cachedRoutePoints) ? snapshot.cachedRoutePoints : []
        });
        clearModifyDestinationSession();
        setPreviewEndSite(null);
        setPreviousEndSite(null);
        setPreviousFareEstimate(null);
        setPreviousRoutePoints([]);
        resetLocalReviewState();
        setFlowState(FLOW_STATES.IDLE);
        return;
      }
      setPreviousEndSite(session.previousEndSite || deps.endSite || null);
      setPreviousFareEstimate(session.previousFareEstimate || null);
      setPreviousRoutePoints(Array.isArray(cachedRoutePoints) ? cachedRoutePoints : []);
      setPreviewEndSite(candidate);
      applyPreviewingState(snapshot);
      setFlowState(FLOW_STATES.PREVIEWING);
      return;
    }
    // 会话生效但尚未拿到候选终点时,保持“待选态”,不要在恢复阶段提前清空会话
    if (session?.active && !isValidSite(candidate)) {
      setPreviewEndSite(null);
      setPreviousEndSite(session.previousEndSite || deps.endSite || null);
      setPreviousFareEstimate(session.previousFareEstimate || null);
      setPreviousRoutePoints(Array.isArray(cachedRoutePoints) ? cachedRoutePoints : []);
    if (nextFlowState === FLOW_STATES.SELECTING) {
      // 会话生效但尚未拿到候选终点时,保持“待选态”,不要在恢复阶段提前清空会话
      applySelectingState(snapshot);
      setFlowState(FLOW_STATES.SELECTING);
      return;
    }
    resetLocalReviewState();
    setFlowState(FLOW_STATES.IDLE);
    if (!MODIFIABLE_TRIP_STATUSES.includes(deps.tripStatus)) {
      setPreviewEndSite(null);
      setPreviousEndSite(null);
      setPreviousFareEstimate(null);
      setPreviousRoutePoints([]);
      Taro.removeStorageSync(MODIFY_DESTINATION_OVERRIDE_KEY);
      safeRemoveStorageSync(MODIFY_DESTINATION_OVERRIDE_KEY);
      setOverrideEndSite(null);
      return;
    }
    // 会话已结束时,强制退出“修改目的地预览态”。
    setPreviewEndSite(null);
    setPreviousEndSite(null);
    setPreviousFareEstimate(null);
    setPreviousRoutePoints([]);
    if (isValidSite(cachedOverride)) {
      Taro.removeStorageSync(MODIFY_DESTINATION_OVERRIDE_KEY);
    if (isValidSite(snapshot.cachedOverride)) {
      safeRemoveStorageSync(MODIFY_DESTINATION_OVERRIDE_KEY);
      setOverrideEndSite(null);
    }
  }, [
@@ -150,6 +291,9 @@
    deps.endSite?.longitude,
  ]);
  /**
   * 当存在本地终点覆盖值时,优先把页面终点同步为覆盖终点。
   */
  useEffect(() => {
    if (!isValidSite(overrideEndSite)) return;
    if (isSameSite(overrideEndSite, deps.endSite)) return;
@@ -162,9 +306,12 @@
    deps.endSite?.longitude
  ]);
  /**
   * 启动修改目的地流程,写入会话快照并跳转到终点选择页。
   */
  const handleStartModifyDestination = (fareEstimate?: any, routePoints?: any[]) => {
    const currentEndSiteName = String(deps.endSite?.name || '');
    Taro.setStorageSync(MODIFY_DESTINATION_SESSION_KEY, {
    safeSetStorageSync(MODIFY_DESTINATION_SESSION_KEY, {
      active: true,
      previousEndSite: deps.endSite || null,
      previousFareEstimate: fareEstimate || null,
@@ -172,14 +319,18 @@
      tripStatus: deps.tripStatus,
      updatedAt: Date.now()
    });
    Taro.setStorageSync(MODIFY_DESTINATION_ROUTE_POINTS_KEY, Array.isArray(routePoints) ? routePoints : []);
    Taro.removeStorageSync(MODIFY_DESTINATION_CANDIDATE_KEY);
    safeSetStorageSync(MODIFY_DESTINATION_ROUTE_POINTS_KEY, Array.isArray(routePoints) ? routePoints : []);
    safeRemoveStorageSync(MODIFY_DESTINATION_CANDIDATE_KEY);
    setFlowState(FLOW_STATES.SELECTING);
    deps.toSelectSite('end', currentEndSiteName);
  };
  /**
   * 清除本地终点覆盖态。
   */
  const clearLocalDestinationOverride = () => {
    setOverrideEndSite(null);
    Taro.removeStorageSync(MODIFY_DESTINATION_OVERRIDE_KEY);
    safeRemoveStorageSync(MODIFY_DESTINATION_OVERRIDE_KEY);
  };
  /**
@@ -188,8 +339,8 @@
  const updatePreviousRoutePoints = (points: any[]) => {
    const safeRoutePoints = Array.isArray(points) ? points : [];
    setPreviousRoutePoints(safeRoutePoints);
    if (Taro.getStorageSync(MODIFY_DESTINATION_SESSION_KEY)?.active) {
      Taro.setStorageSync(MODIFY_DESTINATION_ROUTE_POINTS_KEY, safeRoutePoints);
    if (safeGetStorageSync<ModifyDestinationSession | null>(MODIFY_DESTINATION_SESSION_KEY, null)?.active) {
      safeSetStorageSync(MODIFY_DESTINATION_ROUTE_POINTS_KEY, safeRoutePoints);
    }
  };
@@ -197,7 +348,7 @@
   * 解析修改目的地流程中的“原始目的地”,优先使用会话缓存,避免内存态丢失导致回滚失败。
   */
  const resolvePreviousEndSite = () => {
    const session = Taro.getStorageSync(MODIFY_DESTINATION_SESSION_KEY);
    const session = safeGetStorageSync<ModifyDestinationSession | null>(MODIFY_DESTINATION_SESSION_KEY, null);
    const cachedPreviousEndSite = session?.previousEndSite;
    if (isValidSite(cachedPreviousEndSite)) {
      return cachedPreviousEndSite;
@@ -213,30 +364,33 @@
    const { shouldRestorePreviousEndSite = true } = options;
    const nextPreviousEndSite = resolvePreviousEndSite();
    if (shouldRestorePreviousEndSite && nextPreviousEndSite && !isSameSite(nextPreviousEndSite, deps.endSite)) {
      Taro.setStorageSync('endSite', nextPreviousEndSite);
      safeSetStorageSync(END_SITE_STORAGE_KEY, nextPreviousEndSite);
      deps.setEndSite(nextPreviousEndSite);
    }
    setPreviewEndSite(null);
    setPreviousEndSite(null);
    setPreviousFareEstimate(null);
    setPreviousRoutePoints([]);
    resetLocalReviewState();
    clearModifyDestinationSession();
    clearLocalDestinationOverride();
    setFlowState(FLOW_STATES.IDLE);
  };
  /**
   * 确认修改目的地,请求后端成功后提交新的终点并退出预览态。
   */
  const handleConfirmModifyDestination = async (selectedRoute?: any) => {
    if (!isValidSite(previewEndSite)) return;
    if (isConfirmingModifyDestination) return;
    let isModifySucceeded = false;
    try {
      setIsConfirmingModifyDestination(true);
      setFlowState(FLOW_STATES.CONFIRMING);
      Taro.showLoading({ title: '正在向车辆发起修改目的地请求', mask: true }); // 修改目的地请求进行中全局 loading
      // 如果后端需要字符串格式可以显式转String,如果后端支持number直接传值,保留原始格式防止隐式转换截断
      // 如果原本因为 Number() 导致大整数丢失精度,去掉 Number() 即可
      await orderChange({
        endPointId: previewEndSite.id || 0,
        orderId: Taro.getStorageSync('orderId') || '',
        orderId: safeGetStorageSync<string>(ORDER_ID_STORAGE_KEY, ''),
        selectedRouteId: selectedRoute?.route?.routeID || selectedRoute?.pathFingerprint || '',
        selectedRoutePlanRule: selectedRoute?.primaryRoutePlanRule || '',
        navigationSnapshotId: selectedRoute?.snapshotId || ''
@@ -253,10 +407,11 @@
        id: previewEndSite.id
      };
      Taro.setStorageSync('endSite', newEndSite);
      safeSetStorageSync(END_SITE_STORAGE_KEY, newEndSite);
      deps.setEndSite(newEndSite);
      const oldRoutePoints = [...previousRoutePoints];
      isModifySucceeded = true;
      exitModifyDestinationReview({ shouldRestorePreviousEndSite: false });
@@ -274,12 +429,21 @@
      throw e;
    } finally {
      setIsConfirmingModifyDestination(false);
      if (!isModifySucceeded) {
        setFlowState(isValidSite(previewEndSite) ? FLOW_STATES.PREVIEWING : FLOW_STATES.IDLE);
      }
    }
  };
  const isModifyDestinationSessionActive = !!Taro.getStorageSync(MODIFY_DESTINATION_SESSION_KEY)?.active; // 以缓存会话为准控制预览态展示,规避状态残留
  /**
   * 预览态展示以缓存会话为准,避免内存态恢复晚于页面渲染导致闪动。
   */
  const isModifyDestinationSessionActive = !!safeGetStorageSync<ModifyDestinationSession | null>(MODIFY_DESTINATION_SESSION_KEY, null)?.active; // 以缓存会话为准控制预览态展示,规避状态残留
  const isModifyDestinationReviewVisible = isValidSite(previewEndSite) && isModifyDestinationSessionActive;
  const hasLocalDestinationOverride = isValidSite(overrideEndSite);
  /**
   * 页面当前生效的终点,优先级:预览终点 > 本地覆盖终点 > 真实终点。
   */
  const effectiveEndSite = useMemo(() => {
    if (isModifyDestinationReviewVisible) {
      return previewEndSite;
@@ -294,6 +458,7 @@
    previousFareEstimate,
    previousRoutePoints,
    updatePreviousRoutePoints,
    flowState,
    isConfirmingModifyDestination,
    isModifyDestinationReviewVisible,
    hasLocalDestinationOverride,