zjk
2026-06-13 c4db306d02e004b2d88aba52c1ba48b4e69e1ba9
Merge branch 'feature_v1.0' into feature_v1.0_fix_zjk
3个文件已添加
23个文件已修改
10387 ■■■■■ 已修改文件
.gitignore 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
config/CIPluginOpt.js 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
config/index.js 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package-lock.json 9440 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
package.json 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/hooks/use-station-scale-focus.ts 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/begin-route-view.tsx 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/door-trunk-control.tsx 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/fare-detail-card.tsx 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/debug-autoscale-panel.tsx 334 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/evaluate-popup.tsx 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/route-selection-panel.tsx 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/speed-scale-config.ts 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-android-overlays.ts 144 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-bluetooth-access.ts 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-map-markers.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-map-viewport.ts 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-route-interaction.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-speed-based-scale.ts 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/web-socket-handlers/create-order-route-socket-handler.ts 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.tsx 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/operating-area/index.tsx 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/select-map-site/hooks/use-select-map-site.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/subscribe.ts 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.gitignore
@@ -6,4 +6,5 @@
.DS_Store
.history
key/
.swc/
# project.private.config.json
config/CIPluginOpt.js
@@ -7,6 +7,17 @@
    appid: 'wx57bb0455a168278b',
    privateKeyPath: 'key/private.wx57bb0455a168278b.key',
    robot: Number(process.env.CI_ROBOT) || 1,
        // 与本地 project.config.json 的 setting 保持一致,确保 CI 上传与开发者工具压缩档位相同
    setting: {
        es6: false, // 对应 project.config.json es6: false(es6 转 es5)
        es7: true, // 对应 enhance: true(增强编译)
        minify: true, // 对应 minified: true(压缩代码)
        minifyJS: true, // 压缩 JS(由 minified 覆盖,显式开启保持一致)
        minifyWXML: true, // 对应 minifyWXML: true
        minifyWXSS: true, // 对应 minifyWXSS: true
        autoPrefixWXSS: true, // 对应 postcss: true(样式自动补全)
        codeProtect: false, // 对应日志 codeprotect=0(代码保护关闭)
    },
}
const CIPluginOpt = {
config/index.js
@@ -182,7 +182,14 @@
module.exports = function (merge) {
  if (isCI) {
    if (API_ENV === "development") {
      return merge({}, config, require("./dev"));
      // CI 上传/预览(体验版)即使指向预发环境,也必须使用 production 版 React 打包。
      // dev.js 里 env.NODE_ENV 固定为 "development",会让 webpack 打入 development 版 React,
      // 而 development React 依赖仅存在于开发构建的内部变量(如 app.mount、
      // ReactCurrentActQueue.isBatchingLegacy),上传为体验版/预览版在真机或微信后台运行时会报
      // "null/undefined is not an object" 而崩溃(本地开发者工具用 production 构建所以正常)。
      // 这里保留 dev.js 的预发 API 域名等 defineConstants,仅把 NODE_ENV 覆盖回 production。
      return merge({}, config, require("./dev"), { env: { NODE_ENV: '"production"' } });
    }
  }
package-lock.json
Diff too large
package.json
@@ -28,13 +28,13 @@
  "dependencies": {
    "@babel/runtime": "^7.7.7",
    "@rematch/core": "^1.1.304",
    "@tarojs/components": "3.4.9",
    "@tarojs/plugin-framework-react": "3.4.9",
    "@tarojs/components": "3.6.40",
    "@tarojs/plugin-framework-react": "3.6.40",
    "@tarojs/plugin-platform-kwai": "^2.0.0",
    "@tarojs/plugin-platform-xhs": "^1.1.0",
    "@tarojs/react": "3.4.9",
    "@tarojs/runtime": "3.4.9",
    "@tarojs/taro": "3.4.9",
    "@tarojs/react": "3.6.40",
    "@tarojs/runtime": "3.6.40",
    "@tarojs/taro": "3.6.40",
    "ahooks": "^3.7.6",
    "arrify": "^1.0.1",
    "babel-plugin-setname": "^0.1.1",
@@ -65,17 +65,17 @@
  },
  "devDependencies": {
    "@babel/core": "^7.8.0",
    "@tarojs/cli": "3.4.9",
    "@tarojs/mini-runner": "3.4.9",
    "@tarojs/cli": "3.6.40",
    "@tarojs/mini-runner": "3.6.40",
    "@tarojs/plugin-mini-ci": "^4.1.2",
    "@tarojs/webpack-runner": "3.4.9",
    "@tarojs/webpack-runner": "3.6.40",
    "@types/react": "^17.0.2",
    "@types/webpack-env": "^1.13.6",
    "@typescript-eslint/eslint-plugin": "^4.15.1",
    "@typescript-eslint/parser": "^4.15.1",
    "babel-preset-taro": "3.4.9",
    "babel-preset-taro": "3.6.40",
    "eslint": "^6.8.0",
    "eslint-config-taro": "3.4.9",
    "eslint-config-taro": "3.6.40",
    "eslint-plugin-import": "^2.12.0",
    "eslint-plugin-react": "^7.8.2",
    "eslint-plugin-react-hooks": "^4.2.0",
src/robotaxi/hooks/use-station-scale-focus.ts
@@ -1,4 +1,4 @@
import { useMemo, useState, useEffect, useRef, useCallback } from 'react';
import { useMemo, useState, useRef, useCallback } from 'react';
import { GEOFENCE_STATION_FILL_COLOR, GEOFENCE_STATION_STROKE_COLOR } from '../constants/geofence';
type Deps = {
@@ -14,8 +14,8 @@
 * 场站站点地图围栏展示聚焦控制 Hook。
 * 
 * 职责:
 * 1. 当选中“场站内”的站点,并且发生地图放大操作使得缩放达到 17~18 级(即 >= 16.9)时,去掉运营区围栏蒙层,仅展示场站围栏。
 * 2. 移出场站时、选择其他站点时,或缩放比例小于 17 时,恢复展示所有围栏。
 * 1. 当地图放大操作使得缩放达到 17~18 级(即 >= 16.9)时,去掉运营区围栏蒙层,仅展示场站围栏。
 * 2. 缩放比例小于 17 时,恢复展示所有围栏。
 */
export default function useStationScaleFocus({ currentSite, originalPolygons, originalFencePolylines }: Deps) {
  const isStationSite = useMemo(() => {
@@ -26,21 +26,7 @@
  const [shouldFocusFence, setShouldFocusFence] = useState(false);
  const lastScaleRef = useRef<number | undefined>(undefined);
  useEffect(() => {
    if (!isStationSite) {
      setShouldFocusFence(false);
    } else {
      // 刚选中场站时,重置一次参照物,避免拿上一次旧的值去对比
      lastScaleRef.current = undefined;
    }
  }, [isStationSite]);
  const handleScaleChange = useCallback((scale: number) => {
    if (!isStationSite) {
      lastScaleRef.current = scale;
      return;
    }
    if (lastScaleRef.current !== undefined) {
      if (Math.abs(scale - lastScaleRef.current) > 0.001) {
        // 如果是有效放大操作(规避细微浮点误差),并且达到了 17 级及以上
@@ -58,14 +44,14 @@
      return;
    }
    lastScaleRef.current = scale;
  }, [isStationSite]);
  }, []);
  // 根据当前是否选中场站站点且满足聚焦条件时,过滤围栏数据
  // 根据缩放级别过滤围栏数据
  const displayPolygons = useMemo(() => {
    if (!shouldFocusFence) {
      return originalPolygons;
    }
    // 选中场站且满足条件时,仅保留场站的围栏
    // 放大到 17+ 级时,仅保留场站的围栏
    return originalPolygons.filter(polygon => polygon.fillColor === GEOFENCE_STATION_FILL_COLOR);
  }, [shouldFocusFence, originalPolygons]);
@@ -73,7 +59,7 @@
    if (!shouldFocusFence) {
      return originalFencePolylines;
    }
    // 选中场站且满足条件时,仅保留场站的虚线边框
    // 放大到 17+ 级时,仅保留场站的虚线边框
    return originalFencePolylines.filter(polyline => polyline.color === GEOFENCE_STATION_STROKE_COLOR);
  }, [shouldFocusFence, originalFencePolylines]);
src/robotaxi/pages/begin-route/components/begin-route-view.tsx
@@ -40,7 +40,7 @@
      onCalloutTap={handleCalloutTap}
      onRegionChange={handleRegionChange}
      onPolylineTap={onPolylineTap}
      onTap={onMapTap}
      // onTap={onMapTap}
      style={{ width: '100%', height: '87vh' }}
    >
      {renderMapOverlays()}
src/robotaxi/pages/begin-route/components/calling-car-modules/door-trunk-control.tsx
@@ -7,6 +7,9 @@
const LOCK_CLOSE_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/lock-close.png';
const LOCK_OPEN_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/lock-open.png';
const TRUNK_CLOSE_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/trunk-close.png';
const TRUNK_OPEN_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/trunk-open.png';
const SWITCH_LOADING_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/T-switch-loading-v2.png';
type Props = {
    tripStatus: TripStatus;
@@ -27,6 +30,7 @@
}: Props) {
    const [trunkStatus, setTrunkStatus] = useState(orderTrunkStatus); // 后备箱开关状态
    const [lockStatus, setLockStatus] = useState(false); // 车门开关状态
    const [loadingControlType, setLoadingControlType] = useState<'door' | 'trunk' | null>(null); // 当前处于指令执行中的控件
    const trunkPollTimerRef = useRef<any>(null); // 后备箱状态轮询定时器
    const overRoutePopupDebounceTimerRef = useRef<any>(null); // 结束行程弹窗防抖定时器
    const OVER_ROUTE_POPUP_DEBOUNCE_MS = 1000; // 结束行程弹窗点击防抖时长
@@ -106,6 +110,9 @@
    const handleUnlock = async (doorType: 'door' | 'trunk', onSuccess?: () => void) => {
        const targetOrderId = getOrderId(orderId);
        if (!targetOrderId) return;
        if (loadingControlType) {
            return;
        }
        if (doorType === 'door' && lockStatus) {
            Taro.showToast({ title: '车门已解锁,请开门上车', icon: 'none' });
            return;
@@ -116,6 +123,7 @@
            Taro.showToast({ title: '获取定位失败,请打开定位开关', icon: 'none' });
            return;
        }
        setLoadingControlType(doorType);
        const isCloseAction = doorType === 'trunk' ? !!trunkStatus : false;
        const actionLoadingText = doorType === 'trunk'
            ? (isCloseAction ? '正在关闭后备箱' : '正在开启后备箱')
@@ -130,6 +138,7 @@
            if (res?.code !== 200 || !res?.data) {
                // Taro.hideLoading();
                setLoadingControlType(null);
                Taro.showToast({ title: res?.msg || '解锁失败,请重试或联系客服', icon: 'none' });
                return;
            }
@@ -156,6 +165,7 @@
                        trunkPollTimerRef.current = setTimeout(doPoll, pollInterval);
                    } else {
                        stopPolling();
                        setLoadingControlType(null);
                        // Taro.hideLoading();
                        Taro.showToast({
                            title: failToastText,
@@ -188,6 +198,7 @@
                            stopPolling();
                            const isOpen = !isCloseAction;
                            setTrunkStatus(isOpen);
                            setLoadingControlType(null);
                            // Taro.hideLoading();
                            Taro.showToast({
                                title: successToastText,
@@ -206,6 +217,7 @@
            if (doorType === 'door') {
                // Taro.hideLoading();
                setLoadingControlType(null);
                Taro.showToast({ title: '右侧车门已解锁', icon: 'none' });
                setLockStatus(true);
                onDoorUnlockSuccess?.();
@@ -213,20 +225,38 @@
            }
        }).catch(() => {
            // Taro.hideLoading();
            setLoadingControlType(null);
            Taro.showToast({ title: '解锁失败,请重试或联系客服', icon: 'none' });
        });
    };
    /** 渲染带 loading 效果的控制按钮图标。 */
    const renderControlIcon = (controlType: 'door' | 'trunk', defaultIconSrc: string, className: string) => {
        const isLoading = loadingControlType === controlType;
        return (
            <View style={{ position: 'relative' }}>
                <Image className={className} style={isLoading ? { visibility: 'hidden' } : undefined} src={defaultIconSrc} />
                {isLoading && (
                    <Image
                        className={`${className} car-card-temp-switch-img-spinning`}
                        style={{ position: 'absolute', top: 0, left: 0 }}
                        src={SWITCH_LOADING_ICON_URL}
                    />
                )}
            </View>
        );
    };
    if (tripStatus === TripStatus.VehicleArrivedPickup) {
        return (
            <View className="car-card-arrive">
                <View className={`car-card-arrive-item ${trunkStatus && 'close-btn-item'}`} onClick={() => handleUnlock('trunk', () => setTrunkStatus(!trunkStatus))}>
                    <Image className="car-card-arrive-item-img" src={trunkStatus ? 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/trunk-open.png' : 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/trunk-close.png'} />
                    <View className="car-card-arrive-item-text">{trunkStatus ? '后备箱' : '后备箱'}</View>
                <View className={`car-card-arrive-item ${trunkStatus && 'close-btn-item'} ${loadingControlType ? 'trip-action-bar-item-disabled' : ''}`.trim()} onClick={() => handleUnlock('trunk', () => setTrunkStatus(!trunkStatus))}>
                    {renderControlIcon('trunk', trunkStatus ? TRUNK_OPEN_ICON_URL : TRUNK_CLOSE_ICON_URL, 'car-card-arrive-item-img')}
                    <View className="car-card-arrive-item-text" style={loadingControlType === 'trunk' ? { visibility: 'hidden' } : undefined}>{trunkStatus ? '后备箱' : '后备箱'}</View>
                </View>
                <View className={`car-card-arrive-item ${lockStatus && 'close-btn-item'}`} onClick={() => handleUnlock('door')}>
                    <Image className="car-card-arrive-item-img" src={lockStatus ? LOCK_OPEN_ICON_URL : LOCK_CLOSE_ICON_URL} />
                    <View className="car-card-arrive-item-text">车锁</View>
                <View className={`car-card-arrive-item ${lockStatus && 'close-btn-item'} ${loadingControlType ? 'trip-action-bar-item-disabled' : ''}`.trim()} onClick={() => handleUnlock('door')}>
                    {renderControlIcon('door', lockStatus ? LOCK_OPEN_ICON_URL : LOCK_CLOSE_ICON_URL, 'car-card-arrive-item-img')}
                    <View className="car-card-arrive-item-text" style={loadingControlType === 'door' ? { visibility: 'hidden' } : undefined}>车锁</View>
                </View>
            </View>
        );
@@ -238,9 +268,9 @@
                <View className="car-card-reach-text">点击按钮开车门</View>
                <Image className='car-card-reach-img' src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/door-anim.webp' />
                <View className="car-card-reach-btn">
                    <View className={`car-card-reach-btn-item ${trunkStatus && 'close-btn-item'}`} onClick={() => handleUnlock('trunk', () => setTrunkStatus(!trunkStatus))}>
                        <Image className="car-card-reach-btn-item-img" src={trunkStatus ? 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/trunk-open.png' : 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/trunk-close.png'} />
                        <View className="car-card-reach-btn-item-text">{trunkStatus ? '后备箱' : '后备箱'}</View>
                    <View className={`car-card-reach-btn-item ${trunkStatus && 'close-btn-item'} ${loadingControlType ? 'trip-action-bar-item-disabled' : ''}`.trim()} onClick={() => handleUnlock('trunk', () => setTrunkStatus(!trunkStatus))}>
                        {renderControlIcon('trunk', trunkStatus ? TRUNK_OPEN_ICON_URL : TRUNK_CLOSE_ICON_URL, 'car-card-reach-btn-item-img')}
                        <View className="car-card-reach-btn-item-text" style={loadingControlType === 'trunk' ? { visibility: 'hidden' } : undefined}>{trunkStatus ? '后备箱' : '后备箱'}</View>
                    </View>
                    <View className="car-card-reach-btn-item" onClick={handleOpenOverRoutePopupWithDebounce}>
                        <Image className="car-card-reach-btn-item-img" src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/over-gps-icon.png" />
src/robotaxi/pages/begin-route/components/calling-car-modules/fare-detail-card.tsx
@@ -24,6 +24,7 @@
    onOpenCouponSelect
}: Props) {
    const [showDetail, setShowDetail] = useState(false); // 费用明细展开状态
    const canOpenCouponSelect = tripStatus === TripStatus.TripFinished && typeof onOpenCouponSelect === 'function'; // 仅待支付支持切换优惠券
    // 外部重置版本变化时收起明细
    useEffect(() => {
@@ -77,8 +78,8 @@
                            :
                            <View className="car-card-end-particulars-money" onClick={handleToggleDetail}>
                                <View className="car-card-end-particulars-money-amount">{formatMoney(
                                    (fareBreakdown?.orderMoney - fareBreakdown?.redPacketMoney - fareBreakdown?.couponMoney) <= 0
                                        ? 0 : (fareBreakdown?.orderMoney - fareBreakdown?.redPacketMoney - fareBreakdown?.couponMoney))}</View>
                                    fareBreakdown?.payMoney <= 0
                                        ? 0 : fareBreakdown?.payMoney)}</View>
                                <View>元</View>
                                {
                                    fareBreakdown?.redPacketMoney + fareBreakdown?.couponMoney > 0 &&
@@ -197,10 +198,12 @@
                    (fareBreakdown?.operateFree != 1 && [TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(tripStatus)) && Number(fareBreakdown?.redPacketMoney + fareBreakdown?.couponMoney) > 0 &&
                    <View className="car-card-end-through">
                        <View className="car-card-end-through-label">优惠活动</View>
                        <View className="car-card-end-through-content" onClick={onOpenCouponSelect}>
                        <View className="car-card-end-through-content" onClick={canOpenCouponSelect ? onOpenCouponSelect : undefined}>
                            <View className="car-card-end-through-content-title">最佳优惠</View>
                            <View className="car-card-end-through-content-money">-{formatMoney(fareBreakdown?.redPacketMoney + fareBreakdown?.couponMoney)}元</View>
                            <Image src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/ccc-icon-right.png" />
                            {canOpenCouponSelect && (
                                <Image src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/ccc-icon-right.png" />
                            )}
                        </View>
                    </View>
                }
@@ -211,11 +214,11 @@
                    {
                        fareBreakdown?.operateFree != 1 ?
                            <View className="pay-box-money"><View>{formatMoney(
                                (fareBreakdown?.orderMoney - fareBreakdown?.redPacketMoney - fareBreakdown?.couponMoney) <= 0
                                    ? 0 : (fareBreakdown?.orderMoney - fareBreakdown?.redPacketMoney - fareBreakdown?.couponMoney))}</View><View className="unit">元</View></View>
                               fareBreakdown?.payMoney <= 0
                                    ? 0 : fareBreakdown?.payMoney)}</View><View className="unit">元</View></View>
                            :
                            <View className="pay-box-free">载客测试免费</View>
                    }
                    }{/*  */}
                    <View onClick={handlePay} className="pay-box-btn">立即支付</View>
                </View>
            }
src/robotaxi/pages/begin-route/components/debug-autoscale-panel.tsx
New file
@@ -0,0 +1,334 @@
/**
 * 速度自动缩放调试面板(已下线,文件保留供后续临时调参/回归测试时复用)。
 *
 * ============================================================================
 * 何时再用?
 *   - 调整 LOW_SPEED_MAX_KMH / MID_SPEED_MAX_KMH 阈值后想快速验证分档
 *   - 调整 LOW_SPEED_SCALE / MID_SPEED_SCALE / HIGH_SPEED_SCALE 缩放等级后看效果
 *   - 排查"速度变了但地图没缩放""车辆离开视区"等问题,临时模拟车速/订单状态/车辆位置
 *
 * 启用步骤:
 *   1. 把 `speed-scale-config.ts` 的 `SPEED_SCALE_DEBUG` 改为 `true`
 *      (会自动切换到 debug 阈值:低速 <10、中速 10~20、高速 >20 km/h)
 *   2. 在 `index.tsx` 的 `BeginRoute()` 返回 JSX 末尾(与 ModifyDestinationFailPopup 同级)
 *      引入并挂载本组件,传入页面已有的状态:
 *
 *      // 顶部 import:
 *      import DebugAutoscalePanel from './components/debug-autoscale-panel';
 *
 *      // 顶层 fragment 内:
 *      <DebugAutoscalePanel
 *        tripStatus={tripStatus}
 *        mapScale={mapScale}
 *        applyVehicleSpeedSample={applyVehicleSpeedSample}
 *        setTripStatus={setTripStatus}
 *        vehicleLocation={vehicleLocation}
 *        mapLatitude={mapLatitude}
 *        mapLongitude={mapLongitude}
 *        mockVehicleLocation={(lat, lng) =>
 *          vehicleState.updateVehicleLocationThrottled(lat, lng, true)
 *        }
 *      />
 *
 *   3. `applyVehicleSpeedSample` 由 `useSpeedBasedScale` 解构得到;其余 props
 *      (vehicleLocation/mapScale/...) 都是页面已有变量,按上方写法传即可。
 *   4. 测完把 SPEED_SCALE_DEBUG 改回 false 并删除上面 1 个 import + 1 段 JSX。
 *      面板文件本身保留,无需删除。
 *
 * 面板能力:
 *   - 默认收起,左侧悬浮"调速"小按钮,点开展开完整面板
 *   - Mock 行程状态(接驾中 / 行程中 / 还原)
 *   - Mock 车辆位置("把车放到地图中心" / "微移车辆")
 *   - 速度滑块 + 三档预设按钮(低速 / 中速 / 高速),喂 1 条或喂满窗口
 *   - 实时显示当前 tripStatus、mapScale、vehicleLocation、当前挡位/目标 scale
 *
 * 配套 console 工具(同样受 SPEED_SCALE_DEBUG 控制):
 *   __mockAutoscaleSpeed(kmh)       // 一次性喂 SPEED_SAMPLE_WINDOW 条样本,立即生效
 *   __mockAutoscaleSpeed(kmh, 1)    // 只喂 1 条,观察样本积累过程
 *
 * 注意:
 *   - 仅在 SPEED_SCALE_DEBUG=true 时渲染,开关为 false 时本组件返回 null
 *   - 面板覆盖层基于普通 <View>(与页面 .map-entry/.origin-box 同方案),
 *     无需 cover-view;如果被原生地图盖住,再考虑切到 cover-view
 * ============================================================================
 */
import { useMemo, useRef, useState } from 'react';
import { View, Text, Slider } from '@tarojs/components';
import { TripStatus } from '../../../constants/trip-status';
import {
  HIGH_SPEED_SCALE,
  LOW_SPEED_MAX_KMH,
  LOW_SPEED_SCALE,
  MID_SPEED_MAX_KMH,
  MID_SPEED_SCALE,
  resolveSpeedTierByKmh,
  SPEED_SAMPLE_WINDOW,
  SPEED_SCALE_DEBUG
} from '../hooks/speed-scale-config';
type Props = {
  /** 当前行程状态。 */
  tripStatus: TripStatus;
  /** 当前地图缩放等级(用于展示实际生效值)。 */
  mapScale: number;
  /** 喂入一条速度样本(km/h)。 */
  applyVehicleSpeedSample: (speedKmh: number) => void;
  /** 设置行程状态(用于本地 mock 接驾中/行程中等态,方便调试)。 */
  setTripStatus: (status: TripStatus) => void;
  /** 当前车辆位置(用于面板展示)。 */
  vehicleLocation: { latitude: number; longitude: number } | null;
  /** 当前地图受控中心纬度(用于"放到地图中心"按钮)。 */
  mapLatitude: number | null;
  /** 当前地图受控中心经度(用于"放到地图中心"按钮)。 */
  mapLongitude: number | null;
  /** Mock 车辆位置(直接更新 vehicleLocation 状态)。 */
  mockVehicleLocation: (latitude: number, longitude: number) => void;
};
/** 启用速度缩放的行程状态。 */
const ACTIVE_STATUSES = [TripStatus.VehicleApproaching, TripStatus.InService];
/** 滑块速度上限(km/h),按中速上限的 2 倍取整,保证能覆盖高速挡。 */
const SLIDER_MAX_KMH = Math.ceil(MID_SPEED_MAX_KMH * 2);
/** 挡位标签配色。 */
const TIER_COLOR: Record<string, string> = {
  low: '#36b37e',
  mid: '#ffab00',
  high: '#ff5630'
};
/** 行程状态显示名(仅展示用,不影响业务)。 */
const TRIP_STATUS_LABEL: Record<number, string> = {
  [TripStatus.PublishRoute]: '发布行程',
  [TripStatus.Publishing]: '发布中',
  [TripStatus.CallingCar]: '呼叫车中',
  [TripStatus.VehicleApproaching]: '接驾中',
  [TripStatus.VehicleArrivedPickup]: '到达上车点',
  [TripStatus.InService]: '行程中',
  [TripStatus.ArrivedDestination]: '到达目的地',
  [TripStatus.TripFinished]: '行程结束',
  [TripStatus.PaymentSuccess]: '支付成功',
  [TripStatus.CancelOnlyPickup]: '已取消'
};
/**
 * 速度自动缩放调试面板:默认收起,展开后可拖动滑块或点击预设模拟车速,验证地图自动缩放。
 * 仅在 SPEED_SCALE_DEBUG=true 时渲染,关闭开关后自动消失。
 */
export default function DebugAutoscalePanel(props: Props) {
  const [expanded, setExpanded] = useState(false);
  const [speedKmh, setSpeedKmh] = useState(Math.round((LOW_SPEED_MAX_KMH + MID_SPEED_MAX_KMH) / 2));
  const originalTripStatusRef = useRef<TripStatus | null>(null); // 首次 mock 前的原状态,用于「还原」
  const isActive = ACTIVE_STATUSES.includes(props.tripStatus);
  const tier = useMemo(() => resolveSpeedTierByKmh(speedKmh), [speedKmh]);
  if (!SPEED_SCALE_DEBUG) {
    return null;
  }
  /** 喂入 N 条当前速度样本(km/h)。 */
  const feedSamples = (kmh: number, repeat: number) => {
    for (let index = 0; index < repeat; index += 1) {
      props.applyVehicleSpeedSample(kmh);
    }
  };
  /** 点击预设:设置速度并一次性喂满窗口立即生效。 */
  const applyPreset = (kmh: number) => {
    setSpeedKmh(kmh);
    feedSamples(kmh, SPEED_SAMPLE_WINDOW);
  };
  /** Mock 行程状态:首次 mock 前先记录原状态,以便「还原」恢复。 */
  const mockTripStatus = (next: TripStatus) => {
    if (originalTripStatusRef.current === null) {
      originalTripStatusRef.current = props.tripStatus;
    }
    console.log('[autoscale] 🧪 mock 行程状态', { from: props.tripStatus, to: next });
    props.setTripStatus(next);
  };
  /** 还原到首次 mock 前的状态。 */
  const restoreTripStatus = () => {
    if (originalTripStatusRef.current === null) {
      return;
    }
    const target = originalTripStatusRef.current;
    originalTripStatusRef.current = null;
    console.log('[autoscale] 🧪 还原行程状态', { to: target });
    props.setTripStatus(target);
  };
  const lowPreset = Math.max(0, LOW_SPEED_MAX_KMH - 2);
  const midPreset = Math.round((LOW_SPEED_MAX_KMH + MID_SPEED_MAX_KMH) / 2);
  const highPreset = MID_SPEED_MAX_KMH + Math.round(MID_SPEED_MAX_KMH * 0.5);
  // 收起态:仅显示一个浮动小按钮。
  if (!expanded) {
    return (
      <View
        onClick={() => setExpanded(true)}
        style={{
          position: 'fixed',
          left: '0',
          top: '45%',
          zIndex: 9999,
          padding: '8px 10px',
          background: 'rgba(0,0,0,0.7)',
          color: '#fff',
          fontSize: '12px',
          borderTopRightRadius: '16px',
          borderBottomRightRadius: '16px'
        }}
      >
        调速
      </View>
    );
  }
  const buttonStyle = (background: string) => ({
    flex: 1,
    textAlign: 'center' as const,
    padding: '8px 0',
    margin: '0 4px',
    background,
    color: '#fff',
    fontSize: '13px',
    borderRadius: '8px'
  });
  // 展开态:完整调试面板。
  return (
    <View
      catchMove
      style={{
        position: 'fixed',
        left: '12px',
        right: '12px',
        bottom: '24px',
        zIndex: 9999,
        padding: '12px 14px',
        background: 'rgba(20,20,20,0.92)',
        color: '#fff',
        borderRadius: '12px',
        boxShadow: '0 4px 16px rgba(0,0,0,0.4)'
      }}
    >
      <View style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
        <Text style={{ fontSize: '14px', fontWeight: 'bold' }}>速度缩放调试</Text>
        <View onClick={() => setExpanded(false)} style={{ padding: '2px 10px', fontSize: '13px', background: 'rgba(255,255,255,0.15)', borderRadius: '8px' }}>
          收起
        </View>
      </View>
      <View style={{ fontSize: '12px', marginBottom: '6px', opacity: 0.85 }}>
        <Text>{`当前状态:${TRIP_STATUS_LABEL[props.tripStatus] || props.tripStatus} (${props.tripStatus})`}</Text>
        {originalTripStatusRef.current !== null && (
          <Text style={{ marginLeft: '8px', color: '#ffab00' }}>· 已 mock</Text>
        )}
      </View>
      <View style={{ display: 'flex', marginBottom: '10px' }}>
        <View
          onClick={() => mockTripStatus(TripStatus.VehicleApproaching)}
          style={buttonStyle(props.tripStatus === TripStatus.VehicleApproaching ? '#4c9aff' : 'rgba(255,255,255,0.2)')}
        >
          接驾中
        </View>
        <View
          onClick={() => mockTripStatus(TripStatus.InService)}
          style={buttonStyle(props.tripStatus === TripStatus.InService ? '#4c9aff' : 'rgba(255,255,255,0.2)')}
        >
          行程中
        </View>
        <View
          onClick={restoreTripStatus}
          style={buttonStyle(originalTripStatusRef.current === null ? 'rgba(255,255,255,0.1)' : 'rgba(255,255,255,0.25)')}
        >
          还原
        </View>
      </View>
      <View style={{ fontSize: '12px', marginBottom: '6px', opacity: 0.85 }}>
        <Text>
          {props.vehicleLocation
            ? `车辆位置:${props.vehicleLocation.latitude.toFixed(5)}, ${props.vehicleLocation.longitude.toFixed(5)}`
            : '车辆位置:null(未设置,scale 切换时无法居中)'}
        </Text>
      </View>
      <View style={{ display: 'flex', marginBottom: '10px' }}>
        <View
          onClick={() => {
            const lat = Number(props.mapLatitude);
            const lng = Number(props.mapLongitude);
            if (!Number.isFinite(lat) || !Number.isFinite(lng)) {
              console.log('[autoscale] 🧪 mock 车辆位置失败:地图中心无效', { lat, lng });
              return;
            }
            console.log('[autoscale] 🧪 mock 车辆位置 → 地图中心', { lat, lng });
            props.mockVehicleLocation(lat, lng);
          }}
          style={buttonStyle('rgba(76,154,255,0.8)')}
        >
          把车放到地图中心
        </View>
        <View
          onClick={() => {
            if (!props.vehicleLocation) {
              return;
            }
            // 在当前位置随机偏移约 30 米,验证 scale 切换后仍能跟随车辆居中
            const lat = props.vehicleLocation.latitude + (Math.random() - 0.5) * 0.0006;
            const lng = props.vehicleLocation.longitude + (Math.random() - 0.5) * 0.0006;
            console.log('[autoscale] 🧪 mock 车辆位置 → 微移', { lat, lng });
            props.mockVehicleLocation(lat, lng);
          }}
          style={buttonStyle(props.vehicleLocation ? 'rgba(255,255,255,0.2)' : 'rgba(255,255,255,0.1)')}
        >
          微移车辆
        </View>
      </View>
      <View style={{ fontSize: '12px', lineHeight: '1.7', marginBottom: '6px', opacity: 0.85 }}>
        <Text>{`阈值:低速<${LOW_SPEED_MAX_KMH} / 中速${LOW_SPEED_MAX_KMH}~${MID_SPEED_MAX_KMH} / 高速>${MID_SPEED_MAX_KMH} km/h`}</Text>
        <Text style={{ display: 'block' }}>{`scale:低速 ${LOW_SPEED_SCALE} / 中速 ${MID_SPEED_SCALE} / 高速 ${HIGH_SPEED_SCALE}`}</Text>
      </View>
      <View style={{ display: 'flex', justifyContent: 'space-between', fontSize: '13px', marginBottom: '4px' }}>
        <Text>{`当前选择:${speedKmh} km/h`}</Text>
        <Text style={{ color: TIER_COLOR[tier.key] }}>{`${tier.label} → scale ${tier.scale}`}</Text>
      </View>
      <Slider
        min={0}
        max={SLIDER_MAX_KMH}
        step={1}
        value={speedKmh}
        activeColor='#4c9aff'
        blockSize={20}
        onChanging={(event) => setSpeedKmh(Number(event.detail.value))}
        onChange={(event) => setSpeedKmh(Number(event.detail.value))}
      />
      <View style={{ display: 'flex', marginTop: '8px' }}>
        <View onClick={() => applyPreset(lowPreset)} style={buttonStyle(TIER_COLOR.low)}>{`低速 ${lowPreset}`}</View>
        <View onClick={() => applyPreset(midPreset)} style={buttonStyle(TIER_COLOR.mid)}>{`中速 ${midPreset}`}</View>
        <View onClick={() => applyPreset(highPreset)} style={buttonStyle(TIER_COLOR.high)}>{`高速 ${highPreset}`}</View>
      </View>
      <View style={{ display: 'flex', marginTop: '8px' }}>
        <View onClick={() => feedSamples(speedKmh, 1)} style={buttonStyle('rgba(255,255,255,0.2)')}>喂 1 条</View>
        <View onClick={() => feedSamples(speedKmh, SPEED_SAMPLE_WINDOW)} style={buttonStyle('#4c9aff')}>{`喂满 ${SPEED_SAMPLE_WINDOW} 条(立即生效)`}</View>
      </View>
      <View style={{ fontSize: '12px', marginTop: '8px', lineHeight: '1.6' }}>
        <Text style={{ opacity: 0.85 }}>{`实际地图 scale:${props.mapScale}`}</Text>
        {!isActive && (
          <Text style={{ display: 'block', color: '#ff5630' }}>
            当前非接驾中/行程中,喂入不会生效,请先点上方按钮 mock 状态
          </Text>
        )}
      </View>
    </View>
  );
}
src/robotaxi/pages/begin-route/components/evaluate-popup.tsx
@@ -188,7 +188,7 @@
                    <View className='evaluate-problem-pic-box'>
                        {pictures.map((p, idx) => (
                            <View key={idx} className='evaluate-problem-pic-box-item'>
                                <Image className='evaluate-problem-pic-box-img' src={p.server?.url || p.localUrl} />
                                <Image className='evaluate-problem-pic-box-img' src={p.server?.url || p.localUrl} mode='aspectFit'/>
                                {
                                    p.status === 'uploading' && (
                                        <View className='evaluate-problem-pic-box-mask'>
src/robotaxi/pages/begin-route/components/route-selection-panel.tsx
@@ -25,7 +25,7 @@
  selectedRouteIndex = 0,
  handleSelectRoute
}: Props) {
  const DEFAULT_PREFERENCE: RoutePreferenceOption = { label: '智能推荐', value: 'default', code: 'default' }; // 路线偏好默认值,避免初始渲染空指针
  const DEFAULT_PREFERENCE: RoutePreferenceOption = { label: '', value: 'default', code: 'default' }; // 路线偏好默认值,避免初始渲染空指针
  const [showPreferencePopup, setShowPreferencePopup] = useState(false);
  const [preference, setPreference] = useState<RoutePreferenceOption>(DEFAULT_PREFERENCE);
  const [preferenceOptions, setPreferenceOptions] = useState<RoutePreferenceOption[]>([]);
@@ -120,7 +120,7 @@
      <View className='route-select-type'>
        <View className='route-select-title'>路线选择:</View>
        <View className='route-select-preference' onClick={() => setShowPreferencePopup(true)}>
          <View className='route-select-preference-text'>{preference?.label || '智能推荐'}</View>
          <View className='route-select-preference-text'>{preference?.label || ''}</View>
          <Image src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/select-line.png" />
        </View>
      </View>
src/robotaxi/pages/begin-route/hooks/speed-scale-config.ts
New file
@@ -0,0 +1,62 @@
/**
 * 速度自动缩放配置(纯常量与映射,无副作用),供 hook 与调试面板共享,避免阈值散落多处。
 *
 * 速度分级与缩放等级(按 km/h,非 debug 模式):
 *   - 低速 <20:scale 19
 *   - 中速 20~80:scale 17
 *   - 高速 >80:scale 15
 *
 * 注意:后端 ORDER_ROUTE 消息 route.speed 字段单位为 km/h(不是 m/s),全链路按 km/h 处理。
 */
/**
 * 调试开关:实车难以跑到高速,开启后用更低的速度阈值方便测试,并启用页面调速面板。
 * 测试完成后改回 false 即可恢复线上正常阈值,并自动隐藏调试面板。
 *   - false(线上):低速 <20km/h,中速 20~80km/h,高速 >80km/h
 *   - true (测试):低速  <5km/h,中速  5~20km/h,高速 >20km/h
 */
export const SPEED_SCALE_DEBUG = false;
/** 速度滑动平均窗口,取最近 N 条消息求均值,避免临界值来回闪烁。 */
export const SPEED_SAMPLE_WINDOW = 1;
/** 低速挡上限(km/h)。 */
export const LOW_SPEED_MAX_KMH = SPEED_SCALE_DEBUG ? 10 : 20;
/** 中速挡上限(km/h)。 */
export const MID_SPEED_MAX_KMH = SPEED_SCALE_DEBUG ? 20 : 60;
/** 低速挡缩放等级。 */
export const LOW_SPEED_SCALE = SPEED_SCALE_DEBUG ? 20 : 20;
/** 中速挡缩放等级。 */
export const MID_SPEED_SCALE = SPEED_SCALE_DEBUG ? 16 : 18;
/** 高速挡缩放等级。 */
export const HIGH_SPEED_SCALE = SPEED_SCALE_DEBUG ? 13 : 16;
/** 速度挡位描述。 */
export type SpeedTier = {
  key: 'low' | 'mid' | 'high';
  label: string;
  scale: number;
};
/**
 * 根据速度(km/h)解析挡位与对应缩放等级。
 */
export const resolveSpeedTierByKmh = (kmh: number): SpeedTier => {
  const safeKmh = Math.max(0, Number(kmh) || 0);
  if (safeKmh < LOW_SPEED_MAX_KMH) {
    return { key: 'low', label: '低速', scale: LOW_SPEED_SCALE };
  }
  if (safeKmh < MID_SPEED_MAX_KMH) {
    return { key: 'mid', label: '中速', scale: MID_SPEED_SCALE };
  }
  return { key: 'high', label: '高速', scale: HIGH_SPEED_SCALE };
};
/**
 * 根据速度(km/h)解析目标缩放等级。
 */
export const resolveScaleByKmh = (kmh: number): number => resolveSpeedTierByKmh(kmh).scale;
src/robotaxi/pages/begin-route/hooks/use-android-overlays.ts
@@ -20,138 +20,7 @@
  latitude: number | null;
};
const CAR_CALLOUT_EDGE_GAP = 12;
const CAR_CALLOUT_SIDE_GAP = 18;
const CAR_CALLOUT_TOP_GAP = 20;
const CAR_CALLOUT_BOTTOM_GAP = 34;
const CAR_CALLOUT_DEFAULT_SIZE = { width: 166, height: 56 };
const CAR_CALLOUT_WITH_BEFORE_SIZE = { width: 188, height: 84 };
type CarCalloutPlacement = 'top' | 'bottom' | 'left' | 'right';
/**
 * 计算车辆附近路线切线方向,供气泡选边使用。
 */
const resolveRouteVector = (
  routePoints: Array<{ latitude: number; longitude: number }> = [],
  vehicleLocation: { latitude: number; longitude: number } | null
) => {
  if (!vehicleLocation || routePoints.length < 2) {
    return null;
  }
  let closestIndex = 0;
  let closestDistance = Infinity;
  routePoints.forEach((point, index) => {
    const lat = Number(point?.latitude);
    const lng = Number(point?.longitude);
    if (!Number.isFinite(lat) || !Number.isFinite(lng)) return;
    const distance = ((lat - vehicleLocation.latitude) ** 2) + ((lng - vehicleLocation.longitude) ** 2);
    if (distance < closestDistance) {
      closestDistance = distance;
      closestIndex = index;
    }
  });
  const prevIndex = closestIndex <= 0 ? 0 : closestIndex - 1;
  const nextIndex = closestIndex >= routePoints.length - 1 ? routePoints.length - 1 : closestIndex + 1;
  if (prevIndex === nextIndex) {
    return null;
  }
  const prevPoint = routePoints[prevIndex];
  const nextPoint = routePoints[nextIndex];
  const deltaX = Number(nextPoint?.longitude) - Number(prevPoint?.longitude);
  const deltaY = Number(nextPoint?.latitude) - Number(prevPoint?.latitude);
  if (!Number.isFinite(deltaX) || !Number.isFinite(deltaY)) {
    return null;
  }
  return { deltaX, deltaY };
};
/**
 * 基于路线走向与当前屏幕空间,选择车辆气泡最稳妥的停靠方向。
 */
const resolveCarCalloutPlacement = (
  pos: { x: number; y: number },
  rect: any,
  routeVector: { deltaX: number; deltaY: number } | null,
  calloutSize: { width: number; height: number },
  verticalExtraOffset: number
) => {
  const routeIsMostlyHorizontal = !!routeVector && Math.abs(routeVector.deltaX) >= Math.abs(routeVector.deltaY);
  const topPreferred = rect ? (pos.y - rect.top) >= (rect.top + rect.height - pos.y) : false;
  const leftPreferred = rect ? (pos.x - rect.left) >= (rect.left + rect.width - pos.x) : false;
  const preferredOrder: CarCalloutPlacement[] = routeIsMostlyHorizontal
    ? (topPreferred ? ['top', 'bottom', leftPreferred ? 'left' : 'right', leftPreferred ? 'right' : 'left'] : ['bottom', 'top', leftPreferred ? 'left' : 'right', leftPreferred ? 'right' : 'left'])
    : (leftPreferred ? ['left', 'right', topPreferred ? 'top' : 'bottom', topPreferred ? 'bottom' : 'top'] : ['right', 'left', topPreferred ? 'top' : 'bottom', topPreferred ? 'bottom' : 'top']);
  const computeCandidate = (placement: CarCalloutPlacement) => {
    if (placement === 'top') {
      return {
        placement,
        left: pos.x - (calloutSize.width / 2),
        top: pos.y - calloutSize.height - CAR_CALLOUT_TOP_GAP,
      };
    }
    if (placement === 'bottom') {
      return {
        placement,
        left: pos.x - (calloutSize.width / 2),
        top: pos.y + CAR_CALLOUT_BOTTOM_GAP + verticalExtraOffset,
      };
    }
    if (placement === 'left') {
      return {
        placement,
        left: pos.x - calloutSize.width - CAR_CALLOUT_SIDE_GAP,
        top: pos.y - (calloutSize.height / 2),
      };
    }
    return {
      placement,
      left: pos.x + CAR_CALLOUT_SIDE_GAP,
      top: pos.y - (calloutSize.height / 2),
    };
  };
  const scoreCandidate = (candidate: { placement: CarCalloutPlacement; left: number; top: number }) => {
    const right = candidate.left + calloutSize.width;
    const bottom = candidate.top + calloutSize.height;
    const minLeft = rect.left + CAR_CALLOUT_EDGE_GAP;
    const minTop = rect.top + CAR_CALLOUT_EDGE_GAP;
    const maxRight = rect.left + rect.width - CAR_CALLOUT_EDGE_GAP;
    const maxBottom = rect.top + rect.height - CAR_CALLOUT_EDGE_GAP;
    const overflowPenalty = (
      Math.max(0, minLeft - candidate.left)
      + Math.max(0, minTop - candidate.top)
      + Math.max(0, right - maxRight)
      + Math.max(0, bottom - maxBottom)
    ) * 100;
    const routePenalty = routeIsMostlyHorizontal
      ? (candidate.placement === 'left' || candidate.placement === 'right' ? 24 : 0)
      : (candidate.placement === 'top' || candidate.placement === 'bottom' ? 24 : 0);
    const freeSpaceScore = Math.min(
      candidate.left - minLeft,
      candidate.top - minTop,
      maxRight - right,
      maxBottom - bottom
    );
    const orderPenalty = preferredOrder.indexOf(candidate.placement) * 4;
    return freeSpaceScore - overflowPenalty - routePenalty - orderPenalty;
  };
  const bestCandidate = preferredOrder
    .map(computeCandidate)
    .sort((first, second) => scoreCandidate(second) - scoreCandidate(first))[0];
  const minLeft = rect.left + CAR_CALLOUT_EDGE_GAP;
  const minTop = rect.top + CAR_CALLOUT_EDGE_GAP;
  const maxLeft = rect.left + rect.width - calloutSize.width - CAR_CALLOUT_EDGE_GAP;
  const maxTop = rect.top + rect.height - calloutSize.height - CAR_CALLOUT_EDGE_GAP;
  return {
    left: Math.min(Math.max(bestCandidate.left, minLeft), Math.max(minLeft, maxLeft)),
    top: Math.min(Math.max(bestCandidate.top, minTop), Math.max(minTop, maxTop))
  };
};
export default function useAndroidOverlays(deps: Deps) {
  // 等待雷达样式与显示状态
@@ -244,20 +113,9 @@
    const pos = await computeScreenPosition(deps.vehicleLocation, rect, 0);
    if (!pos) return;
    const verticalExtraOffset = deps.hasBeforeUserEndSite ? 18 : 0;
    if (deps.tripStatus === TripStatus.InService) {
      const routeVector = resolveRouteVector(deps.routePoints || [], deps.vehicleLocation);
      const calloutSize = deps.hasBeforeUserEndSite ? CAR_CALLOUT_WITH_BEFORE_SIZE : CAR_CALLOUT_DEFAULT_SIZE;
      const placement = resolveCarCalloutPlacement(pos, rect, routeVector, calloutSize, verticalExtraOffset);
      setCarCalloutStyle(buildTransformOverlayStyle({
        x: placement.left,
        y: placement.top
      }, ''));
      setCarCalloutVisible(true);
      return;
    }
    setCarCalloutStyle(buildTransformOverlayStyle({
      x: pos.x,
      y: pos.y + 34 + verticalExtraOffset
      y: pos.y + CAR_CALLOUT_BOTTOM_GAP + verticalExtraOffset
    }, 'translate(-50%, 6px)'));
    setCarCalloutVisible(true);
  };
src/robotaxi/pages/begin-route/hooks/use-bluetooth-access.ts
@@ -1,6 +1,7 @@
import Taro from '@tarojs/taro';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { TripStatus } from '../../../constants/trip-status';
import { waitForOrderSuccessSubscribePopup } from '../../../../utils/subscribe';
type Deps = {
  tripStatus: TripStatus;
@@ -26,6 +27,7 @@
const BLUETOOTH_ACCESS_LOG_PREFIX = '[蓝牙权限]';
const BLUETOOTH_AUTH_SYNC_DELAY_MS = 300; // 授权完成后首轮刷新延迟,给小程序权限态一点同步时间
const BLUETOOTH_AUTH_PROTECTION_WINDOW_MS = 5000; // 授权成功后的保护窗口,避免瞬时 permission_deny 覆盖 UI
const SUBSCRIBE_POPUP_WAIT_TIMEOUT_MS = 8000; // 进入接驾态时等待订阅弹窗结束的最长兜底时间,超时后仍放行蓝牙初始化
// 蓝牙图标资源:点亮(已授权)与熄灭(未开/未授权)两态
const BLUETOOTH_ICON_ON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/bluetooth-icon.png';
const BLUETOOTH_ICON_OFF = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/close-bluetooth.png';
@@ -352,9 +354,22 @@
        tripStatus
      });
      setStatusReady(false);
      refreshBluetoothState();
      // 方案 B:先等待“下单成功”订阅弹窗结束,再初始化蓝牙
      // (refreshBluetoothState 内部 openBluetoothAdapter 会触发系统蓝牙授权弹窗),
      // 保证订阅弹窗与蓝牙授权弹窗按先后顺序出现,避免叠加;
      // Promise.race 设兜底超时,订阅异常未 complete 时不阻塞蓝牙初始化。
      let cancelled = false;
      const subscribeTimeout = new Promise<void>((resolve) => {
        setTimeout(resolve, SUBSCRIBE_POPUP_WAIT_TIMEOUT_MS);
      });
      Promise.race([waitForOrderSuccessSubscribePopup(), subscribeTimeout]).then(() => {
        if (cancelled) return;
        logBluetoothAccess('订阅弹窗已结束,开始初始化蓝牙');
        refreshBluetoothState();
      });
      startBluetoothStateWatchRef.current();
      return () => {
        cancelled = true;
        stopBluetoothStateWatchRef.current();
      };
    }
src/robotaxi/pages/begin-route/hooks/use-map-markers.ts
@@ -203,9 +203,9 @@
      anchor: { x: 0.5, y: 0.5 },
      zIndex: 2,
      name: '车辆位置',
      // ...(!shouldUsePureTranslateCarMarker ? {
      //   rotate: typeof deps.vehicleDirection === 'number' ? deps.vehicleDirection : 0,
      // } : {}),
      ...(deps.isAndroid ? {
        rotate: typeof deps.vehicleDirection === 'number' ? deps.vehicleDirection : 0,
      } : {}),
      customCallout: { anchorY, anchorX, display: 'ALWAYS' }
    };
  };
src/robotaxi/pages/begin-route/hooks/use-map-viewport.ts
@@ -549,8 +549,11 @@
    /**
     * 行程动态缩放生效时,将地图中心跟随到车辆位置,确保车辆留在当前视野内。
     * @param options.force 跳过节流/冷却/syncing-in-progress/死区四道闸,强制按安全区目标重定位。
     *   仅供速度驱动缩放后强制居中使用,其他调用点不传该参数即沿用原有行为。
     */
    const syncActiveTripViewportToVehicle = useCallback(() => {
    const syncActiveTripViewportToVehicle = useCallback((options?: { force?: boolean }) => {
        const force = options?.force === true;
        if (![TripStatus.VehicleApproaching, TripStatus.InService].includes(deps.tripStatus)) {
            return;
        }
@@ -560,14 +563,16 @@
            return;
        }
        const now = Date.now();
        if (activeTripViewportSyncingRef.current) {
            return;
        }
        if (now - lastActiveTripViewportSyncAtRef.current < ACTIVE_TRIP_VIEWPORT_SYNC_INTERVAL_MS) {
            return;
        }
        if (getDynamicScaleHoldUntil() > now) {
            return;
        if (!force) {
            if (activeTripViewportSyncingRef.current) {
                return;
            }
            if (now - lastActiveTripViewportSyncAtRef.current < ACTIVE_TRIP_VIEWPORT_SYNC_INTERVAL_MS) {
                return;
            }
            if (getDynamicScaleHoldUntil() > now) {
                return;
            }
        }
        activeTripViewportSyncingRef.current = true;
        Promise.all([ensureMapRect(), getMapRegion(), getMapCenterLocation()])
@@ -598,7 +603,7 @@
                // 纵向死区检查
                const isYInDeadZone = vehicleScreenPosition.y >= safeAreaMetrics.deadZoneTopY
                    && vehicleScreenPosition.y <= safeAreaMetrics.deadZoneBottomY;
                if (isYInDeadZone && !isXOutOfBounds) {
                if (!force && isYInDeadZone && !isXOutOfBounds) {
                    return;
                }
                const nativeCenterLatitude = Number(mapCenterLocation?.latitude);
@@ -611,10 +616,12 @@
                const currentLongitude = Number.isFinite(nativeCenterLongitude)
                    ? nativeCenterLongitude
                    : fallbackCenterLongitude;
                const desiredCenterLatitude = isYInDeadZone
                    ? currentLatitude
                    : vehicleLatitude - (0.5 - safeAreaMetrics.targetMapRatio) * vehicleScreenPosition.latitudeSpan;
                const desiredCenterLongitude = isXOutOfBounds ? vehicleLongitude : currentLongitude;
                // force=true 时纵横向都按安全区目标重定位;
                // 其余情况(force=false)保持原有死区半纠偏行为不变。
                const desiredCenterLatitude = (force || !isYInDeadZone)
                    ? vehicleLatitude - (0.5 - safeAreaMetrics.targetMapRatio) * vehicleScreenPosition.latitudeSpan
                    : currentLatitude;
                const desiredCenterLongitude = (force || isXOutOfBounds) ? vehicleLongitude : currentLongitude;
                const hasLatitudeDelta = !Number.isFinite(currentLatitude) || Math.abs(currentLatitude - desiredCenterLatitude) >= ACTIVE_TRIP_VIEWPORT_SYNC_MIN_DELTA;
                const hasLongitudeDelta = !Number.isFinite(currentLongitude) || Math.abs(currentLongitude - desiredCenterLongitude) >= ACTIVE_TRIP_VIEWPORT_SYNC_MIN_DELTA;
                if (!hasLatitudeDelta && !hasLongitudeDelta) {
src/robotaxi/pages/begin-route/hooks/use-route-interaction.ts
@@ -332,7 +332,7 @@
   * 处理 polyline 点击事件。
   */
  const handlePolylineTap = (event: any) => {
    console.log('[点击折现事件:',event);
    console.log('[点击折线事件:',event);
    const targetIndex = resolveRouteIndexByTap(event);
    if (targetIndex >= 0 && targetIndex < allRoutePoints.length) {
      handleSelectRoute(targetIndex, { syncSites: false });
src/robotaxi/pages/begin-route/hooks/use-speed-based-scale.ts
New file
@@ -0,0 +1,105 @@
import { useCallback, useEffect, useRef } from 'react';
import { TripStatus } from '../../../constants/trip-status';
import {
  resolveScaleByKmh,
  SPEED_SAMPLE_WINDOW
} from './speed-scale-config';
type Deps = {
  /** 当前行程状态。 */
  tripStatus: TripStatus;
  /** 设置地图缩放等级。 */
  setMapScale: (scale: number) => void;
  /** 获取速度缩放暂停截止时间,用于和一次性视口调整/用户手势冷却互斥。 */
  getDynamicScaleHoldUntil: () => number;
  /** 车辆当前位置,缩放时以车辆为中心。 */
  vehicleLocation: { latitude: number; longitude: number } | null;
  /** 当前地图实际缩放等级,用于判断是否需要重新应用。 */
  mapScale: number;
  /** 设置受控地图中心纬度,缩放时同步到车辆位置避免 React 重渲染按陈旧中心重定位。 */
  setLatitude: (latitude: number) => void;
  /** 设置受控地图中心经度,同上。 */
  setLongitude: (longitude: number) => void;
  /** scale 应用后,调 use-map-viewport 的安全区同步逻辑(force 模式)将车辆精确居中。 */
  syncMapToVehicleSafeArea: () => void;
};
/** 启用速度缩放的行程状态:仅接驾中、行程中。 */
const ACTIVE_STATUSES = [TripStatus.VehicleApproaching, TripStatus.InService];
/**
 * 接驾中/行程中根据车辆行驶速度自动缩放地图。
 *
 * - 速度来自后端 ORDER_ROUTE 消息的 route.speed(单位 km/h)。
 * - 维护最近 SPEED_SAMPLE_WINDOW 条速度的滑动平均,用均值判定速度挡位,避免临界值抖动。
 * - 仅在接驾中/行程中生效,且尊重 holdDynamicScale 冷却期(用户手势、一次性视口调整后短暂让位)。
 */
export default function useSpeedBasedScale(deps: Deps) {
  const speedSamplesRef = useRef<number[]>([]); // 最近的速度样本(km/h)
  const applyVehicleSpeedSample = useCallback((rawSpeedKmh: number) => {
    console.log('[autoscale] 收到速度样本', { rawSpeedKmh, tripStatus: deps.tripStatus });
    // 仅接驾中/行程中启用速度缩放。
    if (!ACTIVE_STATUSES.includes(deps.tripStatus)) {
      console.log('[autoscale] 跳过:当前非接驾中/行程中状态', { tripStatus: deps.tripStatus });
      return;
    }
    const speedKmh = Number(rawSpeedKmh);
    // 非法或负速度直接忽略;speed 为 0(停车)仍是合法低速样本。
    if (!Number.isFinite(speedKmh) || speedKmh < 0) {
      return;
    }
    const samples = speedSamplesRef.current;
    samples.push(speedKmh);
    if (samples.length > SPEED_SAMPLE_WINDOW) {
      samples.shift();
    }
    // 样本不足窗口大小时不调整缩放,避免行程初期数据不稳导致抖动。
    if (samples.length < SPEED_SAMPLE_WINDOW) {
      return;
    }
    const averageSpeedKmh = samples.reduce((sum, value) => sum + value, 0) / samples.length;
    const targetScale = resolveScaleByKmh(averageSpeedKmh);
    // 目标缩放与当前地图实际缩放一致则无需重复设置(无论是速度驱动还是用户手动调到同值)。
    if (targetScale === deps.mapScale) {
      return;
    }
    // 处于用户手势/一次性视口调整冷却期内时让位,不抢回缩放;不更新已应用挡位以便冷却结束后重试。
    const holdUntil = deps.getDynamicScaleHoldUntil();
    if (holdUntil > Date.now()) {
      return;
    }
    console.log('[autoscale] ✅ 应用缩放', {
      目标缩放等级: targetScale,
      均值速度_kmh: Number(averageSpeedKmh.toFixed(2))
    });
    // 先把受控中心 lat/lng 更新到车辆位置,再切 scale;
    // 否则 setMapScale 触发的 React 重渲染会把 <Map> 按陈旧的中心+新 scale 重新定位,导致车辆离开视区。
    const vehicleLat = deps.vehicleLocation?.latitude;
    const vehicleLng = deps.vehicleLocation?.longitude;
    const hasVehicle = Number.isFinite(vehicleLat) && Number.isFinite(vehicleLng);
    if (hasVehicle) {
      deps.setLatitude(vehicleLat as number);
      deps.setLongitude(vehicleLng as number);
    }
    deps.setMapScale(targetScale);
    // scale 应用后,让 use-map-viewport 的安全区同步逻辑(force 模式)按 targetMapRatio 把车辆精确居中。
    // 必须等 React 重渲染 + 原生地图应用新 scale 后再触发,否则 syncActive 内部 getRegion() 拿到的 latSpan
    // 还是旧 scale 下的值,算出的偏移会失真。
    setTimeout(() => {
      deps.syncMapToVehicleSafeArea();
    }, 500);
  }, [deps.tripStatus, deps.getDynamicScaleHoldUntil, deps.setMapScale, deps.vehicleLocation, deps.mapScale, deps.setLatitude, deps.setLongitude, deps.syncMapToVehicleSafeArea]);
  // 退出接驾/行程态时清空采样,避免跨行程污染。
  useEffect(() => {
    if (!ACTIVE_STATUSES.includes(deps.tripStatus)) {
      speedSamplesRef.current = [];
    }
  }, [deps.tripStatus]);
  return {
    applyVehicleSpeedSample
  };
}
src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx
@@ -211,6 +211,12 @@
export default function useViewDelegates(deps: Deps) {
  /** 时长文案 */
  const durationDisplay = useMemo(() => formatRemainingDurationDisplay(deps.routeDuration), [deps.routeDuration]);
  /** 车辆公里/分钟气泡专用时长文案:不足 1 分钟时统一展示 1 分钟。 */
  const vehicleMetricDurationDisplay = useMemo(() => (
    durationDisplay.unit === '秒'
      ? { value: 1, unit: '分钟' as const }
      : durationDisplay
  ), [durationDisplay]);
  /** 修改目的地预览态下隐藏车辆公里/分钟气泡。 */
  const shouldShowVehicleMetricCallout = !deps.isModifyDestinationReviewVisible;
  const routeSummaryText = useMemo(() => (
@@ -223,9 +229,9 @@
    <AndroidCarCalloutContent
      hasBeforeUserEndSite={deps.hasBeforeUserEndSite}
      routeDistance={deps.routeDistance}
      durationDisplay={durationDisplay}
      durationDisplay={vehicleMetricDurationDisplay}
    />
  ), [deps.hasBeforeUserEndSite, deps.routeDistance, durationDisplay]);
  ), [deps.hasBeforeUserEndSite, deps.routeDistance, vehicleMetricDurationDisplay]);
  /**
   * 渲染等待态气泡。
@@ -282,8 +288,8 @@
              <CoverView className='car-time'>
                <CoverView className='car-time-num'>{deps.routeDistance?.value ?? 0}</CoverView>
                <CoverView>{deps.routeDistance?.unit === 'm' ? '米' : '公里'}</CoverView>
                <CoverView style={{ marginLeft: '2.5px' }} className='car-time-num'>{durationDisplay.value}</CoverView>
                <CoverView>{durationDisplay.unit}</CoverView>
                <CoverView style={{ marginLeft: '2.5px' }} className='car-time-num'>{vehicleMetricDurationDisplay.value}</CoverView>
                <CoverView>{vehicleMetricDurationDisplay.unit}</CoverView>
              </CoverView>
            </CoverView>
          )}
@@ -305,8 +311,8 @@
    deps.routeSummaryBgSrc,
    deps.showTrip1StyleEndpoint,
    deps.tripStatus,
    durationDisplay,
    routeSummaryText,
    vehicleMetricDurationDisplay,
    shouldShowVehicleMetricCallout
  ]);
src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts
@@ -74,6 +74,8 @@
    onOrderUpdateLineRendered?: (points: any[]) => void;
    applySmoothRouteSnapshot: (snapshot: any) => void;
    resetSmoothRouteMotion: () => void;
    /** 上报车辆实时速度样本(km/h),驱动接驾中/行程中按速度自动缩放地图。 */
    applyVehicleSpeedSample?: (speed: number) => void;
};
type BusinessSocketEventName =
src/robotaxi/pages/begin-route/hooks/web-socket-handlers/create-order-route-socket-handler.ts
@@ -79,6 +79,11 @@
    distanceMeters: Number.isFinite(distanceMeters) ? distanceMeters : null,
    durationSeconds: Number.isFinite(durationSecondsRaw) ? durationSecondsRaw : null
  });
  // 车辆实时速度(km/h),用于接驾中/行程中根据速度自动缩放地图。
  const speedKmh = Number(route?.speed);
  if (Number.isFinite(speedKmh)) {
    deps.applyVehicleSpeedSample?.(speedKmh);
  }
};
/**
src/robotaxi/pages/begin-route/index.tsx
@@ -25,6 +25,8 @@
// 地图视口状态管理
import useMapViewport from './hooks/use-map-viewport';
import useActiveTripScale from './hooks/use-active-trip-scale';
// 速度驱动的地图自动缩放
import useSpeedBasedScale from './hooks/use-speed-based-scale';
// 页面恢复与初始化编排
import usePageRestore from './hooks/use-page-restore';
// 页面动作编排
@@ -608,6 +610,7 @@
  const {
    mapBottomPadding,
    mapScale,
    setMapScale,
    originBoxHidden,
    originOpacity,
    setOriginOpacity,
@@ -628,6 +631,16 @@
    resolveSafeTopInset,
    holdDynamicScale
  }); // 行程中 ORDER_UPDATE_LINE 视口编排
  const { applyVehicleSpeedSample } = useSpeedBasedScale({
    tripStatus,
    setMapScale,
    getDynamicScaleHoldUntil,
    vehicleLocation,
    mapScale,
    setLatitude,
    setLongitude,
    syncMapToVehicleSafeArea: () => syncActiveTripViewportToVehicle({ force: true })
  }); // 接驾中/行程中按车辆速度自动缩放
  const arrivedMarkerReady = tripStatus !== TripStatus.ArrivedDestination || typeof vehicleDirection === 'number';
  const arrivedTooltipReady = tripStatus !== TripStatus.ArrivedDestination || (arrivedMarkerReady && mapScale >= 20);
  const markerState = useMapMarkers({
@@ -944,6 +957,7 @@
    onOrderUpdateLineRendered: activeTripScale.handleOrderUpdateLineRendered,
    applySmoothRouteSnapshot,
    resetSmoothRouteMotion,
    applyVehicleSpeedSample,
    modifyDestinationPickupStatusGuardMs: MODIFY_DESTINATION_PICKUP_STATUS_GUARD_MS
  });
@@ -1197,6 +1211,24 @@
  const handleRegionChange = (e) => {
    activeTripScale.handleRegionChange(e);
    handleAndroidOverlayRegionChange(e);
    // 用户手势导致的缩放/拖拽结束后,把原生地图的实际 scale 同步回 React 状态,
    // 避免受控 <Map scale={mapScale}> 与原生实际缩放漂移,导致后续速度驱动误判"挡位未变化"。
    const causedBy = String(e?.causedBy || e?.detail?.causedBy || '').toLowerCase();
    const eventType = String(e?.type || e?.detail?.type || '').toLowerCase();
    if (eventType === 'end' && (causedBy === 'scale' || causedBy === 'drag' || causedBy === 'gesture')) {
      Taro.createMapContext('map').getScale({
        success: (res: any) => {
          const actualScale = Number(res?.scale);
          if (!Number.isFinite(actualScale)) {
            return;
          }
          const rounded = Math.round(actualScale);
          if (rounded !== mapScale) {
            setMapScale(rounded);
          }
        }
      });
    }
  };
  const handleToggleDetailFade = (expanded: boolean, hasFare: boolean) => {
@@ -1207,14 +1239,6 @@
    }
    updateMapViewportPadding();
  };
  useEffect(() => {
    if (isRouteSelectExpanded) {
      setOriginOpacity(0);
    } else {
      setOriginOpacity(1);
    }
  }, [isRouteSelectExpanded]);
  const handleModifyDestination = () => {
    isCheckedEnter.current = true;
    handleStartModifyDestination(fareEstimate, routePoints);
@@ -1323,7 +1347,7 @@
  };
  // 打开优惠券选择弹窗
  const handleOpenCouponSelect = () => {
    if (![TripStatus.PublishRoute, TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(tripStatus)) {
    if (![TripStatus.PublishRoute, TripStatus.TripFinished].includes(tripStatus)) {
      return;
    }
    setShowCouponSelectPopup(true);
@@ -1332,6 +1356,12 @@
  const handleCloseCouponSelect = () => {
    setShowCouponSelectPopup(false);
  };
  // 支付成功后关闭优惠券选择弹窗
  useEffect(() => {
    if (tripStatus === TripStatus.PaymentSuccess) {
      setShowCouponSelectPopup(false);
    }
  }, [tripStatus]);
  // 选择优惠券后仅刷新价格,不刷新地图路线
  const handleSelectCoupon = (item: any) => {
    const selectedCouponId = String(item?.operationCouponId || item?.couponId || '');
src/robotaxi/pages/operating-area/index.tsx
@@ -73,11 +73,12 @@
      return [];
    }
    return geofenceList.map(item => {
      const isSelected = item.mapPolygonId === selectedId;
      return {
        points: item.polygonPoints || [],
        strokeWidth: 0,
        strokeColor: 'rgba(0, 0, 0, 0)',
        fillColor: '#6d7fd933',
        fillColor: isSelected ? '#6d7fd933' : '#6d7fd91a',
        zIndex: 1,
        id: item.mapPolygonId
      }
@@ -92,11 +93,11 @@
      const isSelected = item.mapPolygonId === selectedId;
      return {
        points: item.polylinePoints || [],
        color: isSelected ? '#6D7FD9' : '#A1B1FF',
        color: '#A1B1FF',
        width: 2,
        dottedLine: !isSelected,
        arrowLine: false,
        borderColor: isSelected ? '#6D7FD9' : '#A1B1FF',
        borderColor: '#A1B1FF',
        borderWidth: 0,
        zIndex: isSelected ? 3 : 2,
        id: Number(item.mapPolygonId ?? index + 1)
src/robotaxi/pages/select-map-site/hooks/use-select-map-site.ts
@@ -45,8 +45,8 @@
const AUTO_SCALE_MAX = 17; // 自动缩放最大等级
const AUTO_SCALE_MIN = 11; // 自动缩放最小等级
const INCLUDE_POINTS_PADDING = [100, 100, 100, 100] as [number, number, number, number]; // 地图回视图内边距
const CROSS_OPERATING_AREA_FILL_COLOR = 'rgba(0, 0, 0, 0.25)'; // 非可选运营区围栏填充色
const CROSS_OPERATING_AREA_STROKE_COLOR = 'rgba(0, 0, 0, 0.4)'; // 非可选运营区围栏描边色
const CROSS_OPERATING_AREA_FILL_COLOR = '#00000040'; // 非可选运营区围栏填充色
const CROSS_OPERATING_AREA_STROKE_COLOR = '#00000066'; // 非可选运营区围栏描边色
const SELECT_MAP_POINT_ON_OFF = {
  start: 1,
  end: 2
src/utils/subscribe.ts
@@ -23,6 +23,8 @@
];
const ORDER_SUCCESS_CACHE_EXPIRE_MS = 10 * 60 * 1000; // 下单成功订阅缓存有效期(毫秒)
let isOrderSuccessSubscribeRequesting = false; // 下单成功订阅请求中标记,避免重复触发
let orderSuccessSubscribePromise: Promise<void> | null = null; // 下单成功订阅弹窗进行中的 Promise,结束后 resolve
let orderSuccessSubscribeResolve: (() => void) | null = null; // 上述 Promise 的 resolve 引用
let pendingOrderSuccessResult: Record<string, string> | null = null; // 点击阶段缓存的订阅返回结果
let pendingOrderSuccessAcceptedTemplateIds: string[] = []; // 点击阶段缓存的已同意模板
let pendingOrderSuccessCapturedAt = 0; // 点击阶段缓存写入时间戳
@@ -144,6 +146,9 @@
export function requestOrderSuccessSubscribeOnTap() {
  if (isOrderSuccessSubscribeRequesting) return;
  isOrderSuccessSubscribeRequesting = true;
  orderSuccessSubscribePromise = new Promise<void>((resolve) => {
    orderSuccessSubscribeResolve = resolve;
  });
  Taro.requestSubscribeMessage({
    tmplIds: ORDER_SUCCESS_SUBSCRIBE_CONFIG.tmplIds,
    success: (result) => {
@@ -157,11 +162,24 @@
    },
    complete: () => {
      isOrderSuccessSubscribeRequesting = false;
      // 订阅弹窗已结束(用户已处理或未弹出),放行等待中的后续原生弹窗
      orderSuccessSubscribeResolve?.();
      orderSuccessSubscribeResolve = null;
      orderSuccessSubscribePromise = null;
    }
  });
}
/**
 * 等待“下单成功”订阅弹窗结束。
 * 若当前没有进行中的订阅弹窗,立即 resolve;否则在订阅弹窗 complete 后 resolve。
 * 用于让蓝牙系统授权等其它原生弹窗排在订阅弹窗之后,避免多个原生弹窗叠加。
 */
export function waitForOrderSuccessSubscribePopup(): Promise<void> {
  return orderSuccessSubscribePromise || Promise.resolve();
}
/**
 * 消费点击阶段缓存结果,并在拿到订单 ID 后补齐频控与后端上报。
 */
async function consumeOrderSuccessSubscribeCache(orderId: string) {