hejianhao
2026-04-17 d83b7e4ec8457f6dbec4760088d8d8d90e3f39b5
fix: 修复行程结束态地图跳动与搜索页兜底逻辑

- 行程状态8/9时停止消费路线点数据,避免视口重算导致跳动
- 优化搜索页兜底展示条件,避免搜索中误显“暂无结果”
- 统一车辆到达态的气泡与标记渲染就绪条件,防止闪烁
- 补充多个组件的注释说明,提升可读性
23个文件已修改
360 ■■■■■ 已修改文件
src/robotaxi/pages/begin-route/components/announcements-popup.tsx 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/begin-route-view.tsx 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/door-trunk-control.tsx 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/evaluation-card.tsx 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/fare-detail-card.tsx 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/temperature-control.tsx 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/trip-status-header.tsx 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/vehicle-info-card.tsx 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car.tsx 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/cancel-popup.tsx 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/evaluate-popup.tsx 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/over-route.tsx 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/passenger-notice.tsx 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/release-be-being.tsx 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/release-route.tsx 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-android-overlays.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-map-markers.ts 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-map-viewport.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.tsx 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/site-search/hooks/use-site-search.ts 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/site-search/index.tsx 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/announcements-popup.tsx
@@ -9,14 +9,15 @@
}
export default function AnnouncementsPopup({ show = false, onClose }: Props) {
    const [visible, setVisible] = useState(false);
    const [animationData, setAnimationData] = useState<any>();
    const [doorImgError, setDoorImgError] = useState(false);
    const [seatbeltImgError, setSeatbeltImgError] = useState(false);
    const [visible, setVisible] = useState(false); // 弹窗显示状态
    const [animationData, setAnimationData] = useState<any>(); // 弹窗开合动画
    const [doorImgError, setDoorImgError] = useState(false); // 开门动画图加载失败状态
    const [seatbeltImgError, setSeatbeltImgError] = useState(false); // 系安全带动画图加载失败状态
    const doorImage = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/door-anim.webp'
    const seatbeltImage = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/seatbelt-anim.webp'
    // 控制弹窗开合动画并重置图片兜底状态
    useEffect(() => {
        if (show && !visible) {
            setVisible(true);
src/robotaxi/pages/begin-route/components/begin-route-view.tsx
@@ -115,6 +115,7 @@
    handleConfirmEvaluate
  } = props;
  // 统一渲染地图与业务弹窗容器
  return (
    <View className='home-container'>
      <Map
src/robotaxi/pages/begin-route/components/calling-car-modules/door-trunk-control.tsx
@@ -18,15 +18,17 @@
    handleOpenOverRoutePopup,
    handleOpenAnnouncementsPopup
}: Props) {
    const [trunkStatus, setTrunkStatus] = useState(orderTrunkStatus);
    const [lockStatus, setLockStatus] = useState(false);
    const trunkPollTimerRef = useRef<any>(null);
    const hasShownAnnouncementsRef = useRef(false);
    const [trunkStatus, setTrunkStatus] = useState(orderTrunkStatus); // 后备箱开关状态
    const [lockStatus, setLockStatus] = useState(false); // 车门解锁状态
    const trunkPollTimerRef = useRef<any>(null); // 后备箱状态轮询定时器
    const hasShownAnnouncementsRef = useRef(false); // 安全须知是否已展示
    // 外部后备箱状态变化时同步本地状态
    useEffect(() => {
        setTrunkStatus(orderTrunkStatus);
    }, [orderTrunkStatus]);
    // 组件卸载时清理轮询定时器
    useEffect(() => {
        return () => {
            if (trunkPollTimerRef.current) {
@@ -35,6 +37,7 @@
        };
    }, []);
    // 获取解锁请求所需定位参数,失败时回退到缓存上车点
    const getUnlockLocationPayload = async () => {
        try {
            Taro.showLoading({ title: '正在获取位置...', mask: true });
@@ -57,11 +60,17 @@
        return {};
    };
    // 解锁车门/后备箱,后备箱场景会轮询确认结果
    const handleUnlock = async (doorType: 'door' | 'trunk', onSuccess?: () => void) => {
        const targetOrderId = orderId || Taro.getStorageSync('orderId');
        if (!targetOrderId) return;
        
        const locationPayload = await getUnlockLocationPayload();
        const isCloseAction = doorType === 'trunk' ? !!trunkStatus : false;
        const actionLoadingText = doorType === 'trunk'
            ? (isCloseAction ? '正在关闭后备箱' : '正在开启后备箱')
            : '正在解锁车门';
        Taro.showLoading({ title: actionLoadingText, mask: true });
        unlockVehicle({
            orderId: targetOrderId,
            doorType,
@@ -69,16 +78,13 @@
        }).then((res: any) => {
            console.log('解锁结果:', res);
            
            if (!res) {
                Taro.showToast({ title: '解锁失败,请重试或联系客服', icon: 'none' });
            if (res?.code !== 200 || !res?.data) {
                Taro.hideLoading();
                Taro.showToast({ title: res?.msg || '解锁失败,请重试或联系客服', icon: 'none' });
                return;
            }
            if (doorType === 'trunk') {
                const isCloseAction = !!trunkStatus;
                const loadingText = isCloseAction ? '正在关闭后备箱' : '正在开启后备箱';
                Taro.showLoading({ title: loadingText, mask: true });
                let pollCount = 0;
                const maxPolls = 5;
                const pollInterval = 2000;
@@ -149,6 +155,7 @@
            }
            
            if (doorType === 'door') {
                Taro.hideLoading();
                Taro.showToast({ title: '右侧车门已解锁', icon: 'none' });
                if (!hasShownAnnouncementsRef.current) {
                    handleOpenAnnouncementsPopup?.();
@@ -156,6 +163,7 @@
                }
            }
        }).catch(() => {
            Taro.hideLoading();
            Taro.showToast({ title: '解锁失败,请重试或联系客服', icon: 'none' });
        });
    };
src/robotaxi/pages/begin-route/components/calling-car-modules/evaluation-card.tsx
@@ -24,10 +24,11 @@
    evaluateSubmitVersion,
    evaluateCloseVersion
}: Props) {
    const [activeIndex, setActiveIndex] = useState(0);
    const [isEvaluated, setIsEvaluated] = useState(false);
    const skipEvaluateResetRef = useRef(false);
    const [activeIndex, setActiveIndex] = useState(0); // 当前选中的满意度
    const [isEvaluated, setIsEvaluated] = useState(false); // 是否已完成评价
    const skipEvaluateResetRef = useRef(false); // 跳过关闭重置的一次性标记
    // 外部提交成功后同步评价状态
    useEffect(() => {
        if (typeof evaluateSubmitVersion === 'number' && evaluateSubmitVersion > 0) {
            skipEvaluateResetRef.current = true;
@@ -36,6 +37,7 @@
        }
    }, [evaluateSubmitVersion]);
    // 关闭评价弹窗时重置高亮状态
    useEffect(() => {
        if (typeof evaluateCloseVersion === 'number' && evaluateCloseVersion > 0) {
            if (skipEvaluateResetRef.current) {
@@ -46,6 +48,7 @@
        }
    }, [evaluateCloseVersion]);
    // 点击评价项:不满意走弹窗,其余直接提交
    const changeEvaluate = (index: number) => {
        if (isEvaluated) return;
        setActiveIndex(index);
@@ -95,4 +98,4 @@
            </View>
        </View>
    );
}
}
src/robotaxi/pages/begin-route/components/calling-car-modules/fare-detail-card.tsx
@@ -20,20 +20,23 @@
    onToggleDetailFade,
    handlePay
}: Props) {
    const [showDetail, setShowDetail] = useState(false);
    const [showDetail, setShowDetail] = useState(false); // 费用明细展开状态
    // 外部重置版本变化时收起明细
    useEffect(() => {
        if (typeof resetDetailVersion === 'number') {
            setShowDetail(false);
        }
    }, [resetDetailVersion]);
    // 跳转费用说明页
    const toMoneyDetail = () => {
        Taro.navigateTo({
            url: `/robotaxi/pages/expense-description/index?orderId=${orderId}`
        });
    };
    // 切换明细面板并同步地图淡化状态
    const handleToggleDetail = () => {
        const next = !showDetail;
        setShowDetail(next);
@@ -170,4 +173,4 @@
            }
        </View>
    );
}
}
src/robotaxi/pages/begin-route/components/calling-car-modules/temperature-control.tsx
@@ -9,13 +9,14 @@
};
export default function TemperatureControl({ tripStatus, orderId }: Props) {
    const [tempStatus, setTempStatus] = useState<number | null>();
    const tempTimerRef = useRef<any>(null);
    const lastConfirmedTempRef = useRef<number>();
    const [tempStatus, setTempStatus] = useState<number | null>(); // 当前显示温度
    const tempTimerRef = useRef<any>(null); // 温度设置防抖定时器
    const lastConfirmedTempRef = useRef<number>(); // 最近一次设置成功温度
    const TEMP_MIN = 22;
    const TEMP_MAX = 28;
    // 状态 4 时拉取车辆当前温度
    useEffect(() => {
        if (tripStatus === 4) {
            const targetOrderId = orderId || Taro.getStorageSync('orderId');
@@ -31,6 +32,7 @@
        }
    }, [tripStatus, orderId]);
    // 初始化确认温度并在卸载时清理定时器
    useEffect(() => {
        lastConfirmedTempRef.current = tempStatus as number;
        return () => {
@@ -40,6 +42,7 @@
        };
    }, []);
    // 防抖提交目标温度到后端
    const scheduleTemperatureUpdate = (targetTemperature: number) => {
        if (tempTimerRef.current) {
            clearTimeout(tempTimerRef.current);
@@ -76,6 +79,7 @@
        }, 5000);
    };
    // 处理温度增减并做边界校验
    const handleTempChange = (delta: number) => {
        setTempStatus((prev) => {
            if (typeof prev !== 'number') {
@@ -120,4 +124,4 @@
            </View>
        </View>
    );
}
}
src/robotaxi/pages/begin-route/components/calling-car-modules/trip-status-header.tsx
@@ -27,6 +27,7 @@
    limitMinutes,
    handleOpenCancelPopup
}: Props) {
    // 标题行文案映射
    const titleMap = {
        3: <>预计<View className="time">{waitMinutes}</View>分钟内有车应答</>,
        4: <>车辆还有<View className="time">{durationDisplay.value}</View>{durationDisplay.unit}到达</>,
@@ -39,6 +40,7 @@
        9: '支付成功'
    }
    // 说明文案映射
    const descMap = {
        3: { text: '正全力为您叫车中,请耐心等待...', className: 'calling-car-content-desc' },
        4: limitMinutes ? { text: `此处限停${limitMinutes}分钟,请尽快前往上车点`, className: isArriveSoon ? 'calling-car-content-desc-red' : 'calling-car-content-warning' } : null,
@@ -66,4 +68,4 @@
            }
        </View>
    );
}
}
src/robotaxi/pages/begin-route/components/calling-car-modules/vehicle-info-card.tsx
@@ -12,6 +12,7 @@
};
export default function VehicleInfoCard({ vehicleInfo }: Props) {
    // 展示车辆基础信息与车辆图片
    return (
        <View className="car-card-detail">
            <View>
@@ -31,4 +32,4 @@
            }
        </View>
    );
}
}
src/robotaxi/pages/begin-route/components/calling-car.tsx
@@ -76,6 +76,7 @@
    orderTrunkStatus,
}: Props) {
    // 汇总行程中的倒计时与费用数据
    const {
        waitMinutes,
        limitMinutes,
@@ -99,6 +100,7 @@
    /** 上车倒计时即将结束(<60秒) */
    const isPickupCountdownSoon = tripStatus === 5 && (typeof pickupCountdownSeconds === 'number' && pickupCountdownSeconds < 60);
    // 按状态组合渲染行程卡片模块
    return (
        <View className='calling-car'>
            <Image className="calling-car-bg" src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/site-top-bg.png' />
src/robotaxi/pages/begin-route/components/cancel-popup.tsx
@@ -22,8 +22,8 @@
}
export default function CancelPopup({ show = false, cancelType = 1, startSite, endSite, waitMinutes, tripStatus, routeDurationSeconds, discountAmount, onClose, onConfirm }: Props) {
    const [visible, setVisible] = useState(false);
    const [animationData, setAnimationData] = useState<any>();
    const [visible, setVisible] = useState(false); // 弹窗显示状态
    const [animationData, setAnimationData] = useState<any>(); // 弹窗开合动画
    const cachedStartSite = startSite || Taro.getStorageSync('startSite') || {};
    const cachedEndSite = endSite || Taro.getStorageSync('endSite') || {};
    const startSiteName = cachedStartSite?.name || cachedStartSite?.address || '-';
@@ -32,6 +32,7 @@
    const durationDisplay = formatDurationDisplay(routeDurationSeconds);
    const discountAmountValue = typeof discountAmount === 'number' && !Number.isNaN(discountAmount) ? discountAmount : 0;
    // 不同取消阶段对应的文案配置
    const STATUS_MAP = {
        1: {
            title: `预计${waitMinutesValue}分钟内有车,确定取消吗?`,
@@ -61,6 +62,7 @@
        },
    }
    // 控制弹窗开合动画
    useEffect(() => {
        if (show && !visible) {
            setVisible(true);
@@ -84,6 +86,7 @@
        }
    }, [show]);
    // 关闭弹窗时按类型执行对应流程
    const closePopup = () => {
        if (cancelType == 5) {
            onConfirm?.();
src/robotaxi/pages/begin-route/components/evaluate-popup.tsx
@@ -15,18 +15,19 @@
}
export default function EvaluatePopup({ show = false, onClose, onConfirm, orderId, orderRating, submitMode = 'comment' }: Props) {
    const [visible, setVisible] = useState(false);
    const [animationData, setAnimationData] = useState<any>();
    const [selectedTypeId, setSelectedTypeId] = useState<number | null>(null);
    const [feedbackContent, setFeedbackContent] = useState('');
    const [typeOptions, setTypeOptions] = useState<any[]>([]);
    const [isSubmitting, setIsSubmitting] = useState(false);
    const [visible, setVisible] = useState(false); // 弹窗显示状态
    const [animationData, setAnimationData] = useState<any>(); // 弹窗进出场动画
    const [selectedTypeId, setSelectedTypeId] = useState<number | null>(null); // 当前选择的问题类型
    const [feedbackContent, setFeedbackContent] = useState(''); // 反馈文本内容
    const [typeOptions, setTypeOptions] = useState<any[]>([]); // 问题类型选项
    const [isSubmitting, setIsSubmitting] = useState(false); // 提交中状态
    const [pictures, setPictures] = useState<Array<{
        localUrl: string;
        server?: { id: string; name: string; url: string } | null;
        status: 'uploading' | 'success' | 'failed';
    }>>([]);
    }>>([]); // 图片上传队列
    // 控制弹窗开合动画
    useEffect(() => {
        if (show && !visible) {
            setVisible(true);
@@ -50,6 +51,7 @@
        }
    }, [show]);
    // 提交反馈(评价/投诉)
    const submit = () => {
        if (isSubmitting) {
            return;
@@ -114,6 +116,7 @@
        });
    }
    // 压缩图片到可上传大小
    const ensureSizeUnderLimit = async (path: string): Promise<string> => {
        try {
            const fs = Taro.getFileSystemManager();
@@ -149,6 +152,7 @@
        }
    };
    // 上传单张图片并写回状态
    const uploadSingle = async (index: number, filePath: string) => {
        setPictures(prev => {
            const next = [...prev];
@@ -192,6 +196,7 @@
        }
    };
    // 选择图片并触发上传
    const handleChooseImages = async () => {
        const remain = Math.max(0, 3 - pictures.length);
        if (remain <= 0) {
@@ -218,12 +223,14 @@
        }
    };
    // 重试上传失败图片
    const handleRetry = async (index: number) => {
        const item = pictures[index];
        if (!item) return;
        uploadSingle(index, item.localUrl);
    };
    // 删除已选图片
    const handleDelete = (index: number) => {
        setPictures(prev => {
            const next = [...prev];
@@ -232,6 +239,7 @@
        });
    };
    // 拉取问题类型并重置本次填写内容
    useEffect(() => {
        if (!show) {
            return;
src/robotaxi/pages/begin-route/components/over-route.tsx
@@ -14,11 +14,12 @@
}
export default function OverRoute({ show = false, onClose, onConfirm }: Props) {
    const [visible, setVisible] = useState(false);
    const [animationData, setAnimationData] = useState<any>();
    const [finishMinutes, setFinishMinutes] = useState<number | null>(null);
    const [isSubmitting, setIsSubmitting] = useState(false);
    const [visible, setVisible] = useState(false); // 弹窗显示状态
    const [animationData, setAnimationData] = useState<any>(); // 弹窗动画数据
    const [finishMinutes, setFinishMinutes] = useState<number | null>(null); // 自动驶离倒计时(分钟)
    const [isSubmitting, setIsSubmitting] = useState(false); // 结束行程提交状态
    // 确认结束行程并调用结束接口
    const handleConfirm = () => {
        if (isSubmitting) {
            return;
@@ -51,6 +52,7 @@
        });
    };
    // 控制弹窗关闭动画
    useEffect(() => {
        if (!show && visible) {
            const animation = Taro.createAnimation({
@@ -66,6 +68,7 @@
        }
    }, [show]);
    // 打开弹窗时先拉取结束后离车时间
    useEffect(() => {
        if (!show) {
            return;
src/robotaxi/pages/begin-route/components/passenger-notice.tsx
@@ -11,15 +11,16 @@
}
export default function PassengerNotice({ show = false, onClose, onCheckedChange }: Props) {
    const [visible, setVisible] = useState(false);
    const [animationData, setAnimationData] = useState<any>();
    const [checked, setChecked] = useState(true);
    const [noticeContent, setNoticeContent] = useState('');
    const [thumbHeight, setThumbHeight] = useState(0);
    const [thumbTop, setThumbTop] = useState(0);
    const [containerHeight, setContainerHeight] = useState(0);
    const [showScrollbar, setShowScrollbar] = useState(false);
    const [visible, setVisible] = useState(false); // 弹窗显示状态
    const [animationData, setAnimationData] = useState<any>(); // 弹窗动画数据
    const [checked, setChecked] = useState(true); // 近三个月不再显示勾选状态
    const [noticeContent, setNoticeContent] = useState(''); // 须知富文本内容
    const [thumbHeight, setThumbHeight] = useState(0); // 自定义滚动条高度
    const [thumbTop, setThumbTop] = useState(0); // 自定义滚动条位置
    const [containerHeight, setContainerHeight] = useState(0); // 文本容器高度
    const [showScrollbar, setShowScrollbar] = useState(false); // 是否展示自定义滚动条
    // 内容滚动时同步自定义滚动条位置
    const onScroll = (e) => {
        const { scrollHeight, scrollTop } = e.detail;
        if (containerHeight > 0 && scrollHeight > containerHeight) {
@@ -28,6 +29,7 @@
        }
    };
    // 文案渲染完成后计算滚动条尺寸
    useEffect(() => {
        if (noticeContent && visible) {
            // Give it a little time to render
@@ -54,6 +56,7 @@
        }
    }, [noticeContent, visible]);
    // 控制弹窗开合动画
    useEffect(() => {
        if (show && !visible) {
            setVisible(true);
@@ -77,6 +80,7 @@
        }
    }, [show]);
    // 每次打开时拉取最新须知内容
    useEffect(() => {
        if (!show) {
            return;
src/robotaxi/pages/begin-route/components/release-be-being.tsx
@@ -9,13 +9,14 @@
}
export default function ReleaseBeBeing({ handleOpenCancelPopup, onFinish }: Props) {
    const [progress, setProgress] = useState(0);
    const [completedCount, setCompletedCount] = useState(0);
    const [progress, setProgress] = useState(0); // 发布进度条
    const [completedCount, setCompletedCount] = useState(0); // 已完成步骤数
    const startSite = Taro.getStorageSync('startSite') || {};
    const endSite = Taro.getStorageSync('endSite') || {};
    const startSiteName = startSite?.name || startSite?.address || '-';
    const endSiteName = endSite?.name || endSite?.address || '-';
    // 模拟发布流程进度和状态推进
    useEffect(() => {
        // 使用 setTimeout 触发 CSS transition 动画
        const timer = setTimeout(() => {
src/robotaxi/pages/begin-route/components/release-route.tsx
@@ -17,16 +17,19 @@
export default function ReleaseRoute({ startSite, endSite, callImmediately, fareEstimate = {}, handleOpenPassengerNotice, walkingTime = 0, isCallDisabled = false }: Props) {
    // 跳转选点页(起点/终点)
    const toSeleteSitePage = (type: string) => {
        Taro.navigateTo({
            url: `/robotaxi/pages/select-map-site/index?pageType=${type}&skipSource=1`,
        })
    }
    // 跳转费用说明页
    const toPriceDescription = () => {
        Taro.navigateTo({
            url: `/robotaxi/pages/expense-detail/index`,
        })
    }
    // 格式化步行时长文案
    const formatWalkingTime = (minutes: number) => {
        const safeMinutes = Math.max(0, Math.floor(Number(minutes) || 0));
        if (safeMinutes >= 60) {
src/robotaxi/pages/begin-route/hooks/use-android-overlays.ts
@@ -5,6 +5,7 @@
  shouldShowWaitingRadar: boolean;
  isAndroid: boolean;
  tripStatus: number;
  mapScale: number;
  startSite: any;
  endSite: any;
  vehicleLocation: { latitude: number; longitude: number } | null;
@@ -151,6 +152,15 @@
    updateArriveCalloutPosition();
  }, [deps.isAndroid, deps.tripStatus, deps.endSite?.latitude, deps.endSite?.longitude]);
  useEffect(() => {
    if (!deps.isAndroid || deps.tripStatus !== 7) return;
    if (deps.mapScale < 20) return;
    const timer = setTimeout(() => {
      updateArriveCalloutPosition();
    }, 80);
    return () => clearTimeout(timer);
  }, [deps.isAndroid, deps.tripStatus, deps.mapScale, deps.endSite?.latitude, deps.endSite?.longitude]);
  return {
    waitingRadarStyle,
    waitingRadarVisible,
src/robotaxi/pages/begin-route/hooks/use-map-markers.ts
@@ -10,6 +10,8 @@
  vehicleLocation: { latitude: number; longitude: number } | null;
  vehicleDirection: number | null;
  vehicleMarkerId: number;
  arrivedMarkerReady: boolean;
  arrivedTooltipReady: boolean;
};
const defaultDeps: Deps = {
@@ -20,7 +22,9 @@
  endSite: null,
  vehicleLocation: null,
  vehicleDirection: null,
  vehicleMarkerId: 3
  vehicleMarkerId: 3,
  arrivedMarkerReady: false,
  arrivedTooltipReady: false
};
export default function useMapMarkers(rawDeps?: Partial<Deps>) {
@@ -137,10 +141,12 @@
    const useAndroidNativeTrip1Callout = deps.isAndroid && [1, 5, 8, 9, 10].includes(deps.tripStatus);
    const pickupMarker = createPickupMarker(pickupSite, useAndroidNativeTrip1Callout);
    const dropoffMarker = createDropoffMarker(dropoffSite, useAndroidNativeTrip1Callout);
    const shouldWaitArrivedDirection = deps.tripStatus === 7 && !deps.arrivedMarkerReady;
    const effectiveShowCar = deps.statusRule.showCar && !shouldWaitArrivedDirection;
    const carMarker = createCarMarker();
    const nextMarkers: any[] = [];
    if (deps.statusRule.showPickup && pickupMarker) nextMarkers.push(pickupMarker);
    if (deps.statusRule.showCar && carMarker) nextMarkers.push(carMarker);
    if (effectiveShowCar && carMarker) nextMarkers.push(carMarker);
    if (deps.statusRule.showDropoff && dropoffMarker) nextMarkers.push(dropoffMarker);
    const nextMarkersSignature = nextMarkers
      .map((m: any) => [
@@ -153,8 +159,25 @@
      ].join('|'))
      .join(';');
    const hasDropoffMarker = markersRef.current.some((m: any) => m.id === 2);
    const hasCarMarker = markersRef.current.some((m: any) => m.id === deps.vehicleMarkerId);
    const currentCarMarker = markersRef.current.find((m: any) => m.id === deps.vehicleMarkerId);
    const currentRotate = Number(currentCarMarker?.rotate);
    const targetRotate = Number(deps.vehicleDirection);
    const angleDiff = (a: number, b: number) => {
      const diff = Math.abs(((a - b + 540) % 360) - 180);
      return diff;
    };
    const isArrivedAngleSynced = Number.isFinite(currentRotate)
      && Number.isFinite(targetRotate)
      && angleDiff(currentRotate, targetRotate) < 1;
    const shouldHoldMarkerByTranslate = [4, 6].includes(deps.tripStatus) && carMarkerInitialized.current && deps.statusRule.showCar && (!deps.statusRule.showDropoff || hasDropoffMarker);
    const shouldHoldMarkerWhenArrived = deps.tripStatus === 7 && carMarkerInitialized.current && deps.statusRule.showCar && (!deps.statusRule.showDropoff || hasDropoffMarker);
    const shouldHoldMarkerWhenArrived = deps.tripStatus === 7
      && deps.arrivedTooltipReady
      && carMarkerInitialized.current
      && effectiveShowCar
      && hasCarMarker
      && isArrivedAngleSynced
      && (!deps.statusRule.showDropoff || hasDropoffMarker);
    const shouldSkipUpdate = (shouldHoldMarkerByTranslate && (deps.isAndroid || tripStatus6MarkerRefreshedRef.current)) || shouldHoldMarkerWhenArrived;
    applyMarkerState(nextMarkers, nextMarkersSignature, shouldSkipUpdate, carMarker);
  }, [deps.tripStatus, deps.startSite?.name, deps.startSite?.latitude, deps.startSite?.longitude, deps.endSite?.name, deps.endSite?.latitude, deps.endSite?.longitude, deps.vehicleLocation, deps.vehicleDirection, deps.vehicleMarkerId, deps.isAndroid, deps.statusRule.showPickup, deps.statusRule.showCar, deps.statusRule.showDropoff]);
src/robotaxi/pages/begin-route/hooks/use-map-viewport.ts
@@ -263,10 +263,15 @@
        const { points, keyParts } = buildViewportPayload(viewportType, pickupSite, dropoffSite);
        if (points.length === 0) return;
        const nextKey = keyParts.join('|');
        if (nextKey === viewportKeyRef.current) return;
        if (nextKey === viewportKeyRef.current) {
            if (mapScale < DROPOFF_MAX_SCALE) {
                setMapScale(DROPOFF_MAX_SCALE);
            }
            return;
        }
        viewportKeyRef.current = nextKey;
        updateViewportByPayload(viewportType, points);
    }, [deps.tripStatus, deps.statusRule?.viewport, deps.endSite?.latitude, deps.endSite?.longitude]);
    }, [deps.tripStatus, deps.statusRule?.viewport, deps.endSite?.latitude, deps.endSite?.longitude, mapScale]);
    useEffect(() => {
        const { pickupSite, dropoffSite } = resolveViewportSites();
src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx
@@ -30,6 +30,8 @@
  arriveCalloutVisible: boolean;
  /** 到达气泡样式 */
  arriveCalloutStyle: any;
  /** 到达态气泡渲染就绪(角度与缩放均就绪) */
  arrivedTooltipReady: boolean;
  /** 路线距离 */
  routeDistance: { value: number; unit: 'km' | 'm' } | null;
  /** 路线时长 */
@@ -130,7 +132,7 @@
          </CoverView>
        </CoverView>
      )}
      {(item.id === 2 && [7].includes(deps.tripStatus) && !deps.isAndroid && deps.mapScale >= 20) && (
      {(item.id === 2 && [7].includes(deps.tripStatus) && !deps.isAndroid && deps.arrivedTooltipReady) && (
        <CoverView className='custom-callout-content'>
          已到达
        </CoverView>
@@ -170,7 +172,7 @@
          </View>
        </View>
      )}
      {deps.isAndroid && deps.arriveCalloutVisible && (
      {deps.isAndroid && deps.arriveCalloutVisible && deps.arrivedTooltipReady && (
        <View className="car-callout-android" style={deps.arriveCalloutStyle}>
          <View className="custom-callout-content">
            已到达
src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts
@@ -268,6 +268,10 @@
    const forceFullRedraw = payload?.forceFullRedraw === true;
    const currentTripStatus = Number(deps.tripStatusRef.current);
    updateRouteSummary(payload, deps);
    // 行程结束态仅更新概要信息,不再消费路线点,避免视口反复重算造成跳动
    if ([8, 9].includes(currentTripStatus)) {
        return;
    }
    if ([4, 6].includes(currentTripStatus)) {
        deps.setVehicleMoveAlongPoints(servedPoints);
    }
src/robotaxi/pages/begin-route/index.tsx
@@ -144,38 +144,6 @@
    lastVehiclePointRef,
    lastVehicleUpdateAtRef
  } = vehicleState; // 车辆状态解构
  const markerState = useMapMarkers({
    tripStatus,
    statusRule,
    isAndroid,
    startSite,
    endSite,
    vehicleLocation,
    vehicleDirection,
    vehicleMarkerId
  }); // 地图标记状态容器
  const {
    markers,
    tripStatus6MarkerRefreshedRef
  } = markerState; // 标记状态解构
  const {
    waitingRadarStyle,
    waitingRadarVisible,
    carCalloutStyle,
    carCalloutVisible,
    arriveCalloutStyle,
    arriveCalloutVisible,
    handleRegionChange: handleAndroidOverlayRegionChange
  } = useAndroidOverlays({
    shouldShowWaitingRadar,
    isAndroid,
    tripStatus,
    startSite,
    endSite,
    vehicleLocation,
    longitude,
    latitude
  }); // Android 覆盖物状态编排
  const routeState = useRouteManager({
    isAndroid,
    tripStatus,
@@ -239,6 +207,43 @@
    toOrigin,
    getLocationInfo
  } = viewportState; // 视口状态解构
  const arrivedMarkerReady = tripStatus !== 7 || typeof vehicleDirection === 'number';
  const arrivedTooltipReady = tripStatus !== 7 || (arrivedMarkerReady && mapScale >= 20);
  const markerState = useMapMarkers({
    tripStatus,
    statusRule,
    isAndroid,
    startSite,
    endSite,
    vehicleLocation,
    vehicleDirection,
    vehicleMarkerId,
    arrivedMarkerReady,
    arrivedTooltipReady
  }); // 地图标记状态容器
  const {
    markers,
    tripStatus6MarkerRefreshedRef
  } = markerState; // 标记状态解构
  const {
    waitingRadarStyle,
    waitingRadarVisible,
    carCalloutStyle,
    carCalloutVisible,
    arriveCalloutStyle,
    arriveCalloutVisible,
    handleRegionChange: handleAndroidOverlayRegionChange
  } = useAndroidOverlays({
    shouldShowWaitingRadar,
    isAndroid,
    tripStatus,
    mapScale,
    startSite,
    endSite,
    vehicleLocation,
    longitude,
    latitude
  }); // Android 覆盖物状态编排
  const isCheckedEnter = useRef(false); // 是否已检查页面进入状态
  const { checkDistanceAndCityReady } = useOrderDistanceConfig({
    endSite,
@@ -458,6 +463,7 @@
    carCalloutStyle,
    arriveCalloutVisible,
    arriveCalloutStyle,
    arrivedTooltipReady,
    routeDistance,
    routeDuration,
    customCalloutIconSrc,
src/robotaxi/pages/site-search/hooks/use-site-search.ts
@@ -30,6 +30,11 @@
  const [nearbySiteList, setNearbySiteList] = useState<any[]>([]);
  // 页面类型(start/end)
  const [pageType, setPageType] = useState('');
  // 当前关键词是否仍在搜索中(含高德补充结果)
  const [isSearching, setIsSearching] = useState(false);
  // 最近一次完成搜索的关键词
  const [lastCompletedKeyword, setLastCompletedKeyword] = useState('');
  const searchSeqRef = useRef(0);
  // 拉取附近站点
  const fetchNearbySites = async (lat: number, lon: number, electronicAreaId?: string) => {
@@ -50,9 +55,17 @@
  // 站点搜索请求与结果处理
  const searchSite = async (val?: string) => {
    setNonOperationalSite([]);
    const queryVal = val !== undefined ? val : deps.searchValue;
    if (!queryVal) return;
    const normalizedQuery = String(queryVal || '').trim();
    searchSeqRef.current += 1;
    const requestSeq = searchSeqRef.current;
    setNonOperationalSite([]);
    if (!normalizedQuery) {
      setIsSearching(false);
      setLastCompletedKeyword('');
      return;
    }
    setIsSearching(true);
    const { latitude, longitude, electronicAreaId } = deps.router.params;
    const selectedCity = Taro.getStorageSync('selectedCity');
    const payload = {
@@ -60,42 +73,63 @@
      electronicAreaId: electronicAreaId ? Number(electronicAreaId) : 0,
      lat: Number(latitude),
      lon: Number(longitude),
      pointPoi: queryVal
      pointPoi: normalizedQuery
    };
    try {
      const res = await queryPointByName(payload);
      if (requestSeq !== searchSeqRef.current) return;
      const { availablePoints } = res;
      if (Array.isArray(availablePoints)) {
        const list = availablePoints.map(normalizeSiteItem);
        const sorted = sortSitesByNameMatchAndDistance(list, queryVal);
        const sorted = sortSitesByNameMatchAndDistance(list, normalizedQuery);
        setSiteList(sorted.slice(0, deps.maxExpandedCount));
      } else {
        setSiteList([]);
      }
      const myAmapFun = new amapFile.AMapWX({ key: _AMAP_KEY });
      myAmapFun.getPoiAround({
        querykeywords: queryVal,
        location: `${longitude},${latitude}`,
        success: (data: any) => {
          if (data && data.poisData) {
            const list = data.poisData.map((item: any) => {
              const locationArr = item.location.split(',');
              return {
                ...item,
                longitude: Number(locationArr[0]),
                latitude: Number(locationArr[1]),
                ...normalizeSiteItem(item)
              };
            });
            const sorted = sortSitesByNameMatchAndDistance(list, queryVal);
            setNonOperationalSite(sorted.slice(0, deps.maxShowCount));
      await new Promise<void>((resolve) => {
        myAmapFun.getPoiAround({
          querykeywords: normalizedQuery,
          location: `${longitude},${latitude}`,
          success: (data: any) => {
            if (requestSeq !== searchSeqRef.current) {
              resolve();
              return;
            }
            if (data && data.poisData) {
              const list = data.poisData.map((item: any) => {
                const locationArr = item.location.split(',');
                return {
                  ...item,
                  longitude: Number(locationArr[0]),
                  latitude: Number(locationArr[1]),
                  ...normalizeSiteItem(item)
                };
              });
              const sorted = sortSitesByNameMatchAndDistance(list, normalizedQuery);
              setNonOperationalSite(sorted.slice(0, deps.maxShowCount));
            } else {
              setNonOperationalSite([]);
            }
            resolve();
          },
          fail: () => {
            if (requestSeq === searchSeqRef.current) {
              setNonOperationalSite([]);
            }
            resolve();
          }
        },
        fail: () => { }
        });
      });
    } catch {
      if (requestSeq !== searchSeqRef.current) return;
      setSiteList([]);
      setNonOperationalSite([]);
    } finally {
      if (requestSeq === searchSeqRef.current) {
        setIsSearching(false);
        setLastCompletedKeyword(normalizedQuery);
      }
    }
  };
@@ -111,6 +145,8 @@
  const clearSearchResults = () => {
    setSiteList([]);
    setNonOperationalSite([]);
    setIsSearching(false);
    setLastCompletedKeyword('');
  };
  // 初始化:拉取附近站点、历史记录、页面类型
@@ -145,6 +181,8 @@
    historySite,
    nonOperationalSite,
    nearbySiteList,
    isSearching,
    lastCompletedKeyword,
    searchSite,
    debouncedSearch,
    clearSearchResults
src/robotaxi/pages/site-search/index.tsx
@@ -37,6 +37,8 @@
    historySite,
    nonOperationalSite,
    nearbySiteList,
    isSearching,
    lastCompletedKeyword,
    searchSite,
    debouncedSearch,
    clearSearchResults
@@ -224,6 +226,7 @@
  const recommendList = searchValue ? sortSitesByNameMatchAndDistance(siteList, searchValue) : filteredNearbyList;
  const visibleHistory = showAllHistory ? historySite.slice(0, MAX_EXPANDED_COUNT) : historySite.slice(0, MAX_SHOW_COUNT);
  const visibleRecommend = showAllRecommend ? recommendList.slice(0, MAX_EXPANDED_COUNT) : recommendList.slice(0, MAX_SHOW_COUNT);
  const canShowSearchFallback = !searchValue || (!isSearching && lastCompletedKeyword === searchValue.trim());
  // 使用数据截断展示,不再通过高度裁剪内容
@@ -297,7 +300,7 @@
        </View>
      }
      {
        (!siteList.length && (!searchValue ? !nearbySiteList.length : true) && !nonOperationalSite.length && (!searchValue ? !historySite.length : true)) &&
        canShowSearchFallback && (!siteList.length && (!searchValue ? !nearbySiteList.length : true) && !nonOperationalSite.length && (!searchValue ? !historySite.length : true)) &&
        <View className='history-card'>
          <View className='card-header'>暂无搜索结果,为您推荐附近站点</View>
          <View className='card-content list-content'>