| | |
| | | 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, |
| | |
| | | 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) { |
| | |
| | | && 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, |
| | |
| | | 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); |
| | | } |
| | | }, [ |
| | |
| | | deps.endSite?.longitude, |
| | | ]); |
| | | |
| | | /** |
| | | * 当存在本地终点覆盖值时,优先把页面终点同步为覆盖终点。 |
| | | */ |
| | | useEffect(() => { |
| | | if (!isValidSite(overrideEndSite)) return; |
| | | if (isSameSite(overrideEndSite, deps.endSite)) return; |
| | |
| | | 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, |
| | |
| | | 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); |
| | | }; |
| | | |
| | | /** |
| | |
| | | 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); |
| | | } |
| | | }; |
| | | |
| | |
| | | * 解析修改目的地流程中的“原始目的地”,优先使用会话缓存,避免内存态丢失导致回滚失败。 |
| | | */ |
| | | 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; |
| | |
| | | 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 || '' |
| | |
| | | id: previewEndSite.id |
| | | }; |
| | | |
| | | Taro.setStorageSync('endSite', newEndSite); |
| | | safeSetStorageSync(END_SITE_STORAGE_KEY, newEndSite); |
| | | deps.setEndSite(newEndSite); |
| | | |
| | | const oldRoutePoints = [...previousRoutePoints]; |
| | | isModifySucceeded = true; |
| | | |
| | | exitModifyDestinationReview({ shouldRestorePreviousEndSite: false }); |
| | | |
| | |
| | | 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; |
| | |
| | | previousFareEstimate, |
| | | previousRoutePoints, |
| | | updatePreviousRoutePoints, |
| | | flowState, |
| | | isConfirmingModifyDestination, |
| | | isModifyDestinationReviewVisible, |
| | | hasLocalDestinationOverride, |