zjk
2026-04-20 7ae86c6b0379fdd04ba5a74b930e34de7d6e5f9a
Merge branch 'feature_v1.0' of http://gitlab.xiaopeng.local:18080/robotaxi1/xp-robotaxi-miniprogram into feature_v1.0
10个文件已添加
30个文件已修改
6745 ■■■■■ 已修改文件
config/BuildDonePlugin.js 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
config/dev.js 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
config/index.js 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
config/prod.js 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/api/begin-route.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/announcements-popup.tsx 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/begin-route-view.tsx 6 ●●●●● 补丁 | 查看 | 原始文档 | 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 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/use-trip-countdown.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | 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/css-folder/modify-destination-review.less 165 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/route-selection-panel.less 116 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/evaluate-popup.tsx 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/modify-destination-review.tsx 178 ●●●●● 补丁 | 查看 | 原始文档 | 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 154 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/route-selection-panel.tsx 163 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/constants/modify-destination.ts 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-android-overlays.ts 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-map-markers.ts 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-map-viewport.ts 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-modify-destination-flow.ts 209 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-route-manager.ts 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-trip-runtime.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.tsx 264 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/site-search/hooks/use-site-search.ts 82 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/site-search/index.tsx 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/utils/car-control/RtAuthBluetoothManager.js 432 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/utils/car-control/bleMethod.js 4557 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/utils/car-control/bleMethod.wasm.br 补丁 | 查看 | 原始文档 | blame | 历史
config/BuildDonePlugin.js
New file
@@ -0,0 +1,45 @@
const fs = require('fs-extra');
const path = require('path');
// 需要在构建完成后拷贝到产物目录的文件列表
// src: 相对于项目根目录的源文件路径
// dest: 相对于产物目录(outputPath)的目标文件路径
const filesToCopy = [
  // {
  //   src: 'src/otherPage/pages/carControl/bleMethod.wasm.br',
  //   dest: 'otherPage/pages/carControl/bleMethod.wasm.br',
  // },
  {
    src: 'src/robotaxi/utils/car-control/bleMethod.wasm.br',
    dest: 'robotaxi/utils/car-control/bleMethod.wasm.br',
  },
];
class BuildDonePlugin {
  apply(compiler) {
    compiler.hooks.done.tap('BuildDonePlugin', (stats) => {
      if (process.env.TARO_ENV !== 'weapp') return;
      const outputPath = stats.compilation.outputOptions.path;
      filesToCopy.forEach(({ src, dest }) => {
        const srcFile = path.resolve(src);
        const destFile = path.resolve(outputPath, dest);
        const destDir = path.dirname(destFile);
        if (!fs.existsSync(destDir)) {
          fs.mkdirSync(destDir, { recursive: true });
        }
        try {
          fs.copyFileSync(srcFile, destFile);
          console.log(`✅ copy: ${src} -> ${dest}`);
        } catch (error) {
          console.error(`❌ copy: ${src} -> ${dest}: ${error.message}`);
        }
      });
    });
  }
}
module.exports = BuildDonePlugin;
config/dev.js
@@ -116,6 +116,9 @@
              },
            ],
          },
          buildDone: {
            plugin: require('./BuildDonePlugin'),
          },
        },
      })
    },
config/index.js
@@ -48,16 +48,6 @@
  ]
}
class BuildDonePlugin {
  apply(compiler) {
    compiler.hooks.done.tap('Build Done Plugin', () => {
      // if (process.env.TARO_ENV === 'alipay') {
      //   fs.copyFileSync('./mini.project.json', './dist/alipay/mini.project.json')
      // }
    });
  }
}
const config = {
  projectName: "xp-robotaxi-miniprogram",
  date: "2020-2-11",
@@ -138,9 +128,9 @@
    webpackChain(chain) {
      chain.merge({
        plugin: {
          install: {
            plugin: BuildDonePlugin
          }
          buildDone: {
            plugin: require('./BuildDonePlugin'),
          },
        }
      });
    }
config/prod.js
@@ -17,7 +17,7 @@
const ADMIN_OSS_HOST = '"https://xps01.xiaopeng.com"'; // 运营后台上传的额文件的 oss 域名
const ROBOTAXI_HOST = '"https://rt-api.xiaopeng.com"';
const ROBOTAXI_WS_HOST = '"wss://rt-api.xiaopeng.com/websocket"';
const ROBOTAXI_WS_HOST = '"wss://rt-ws.xiaopeng.com/websocket"';
const AMAP_KEY = '"8157fdfa13878d3f7bf69c560c8a846a"';
const XP_APPID_MAP = {
@@ -33,7 +33,7 @@
const GIO_CONFIG = {
  accountId: '"84eafaa69d23412f"', // 埋点平台id
  dataSourceId: {
    weapp: '"b9aa24dc0aab9edb"', // 埋点数据源id
    weapp: '"b63706df57a52ecf"', // 埋点数据源id
  },
  host: '"https://collect.xiaopeng.com"',
}
src/robotaxi/api/begin-route.ts
@@ -112,6 +112,15 @@
    }, true);
}
// 修改目的地
export const orderChange = async (payload) => {
    return request({
        url: _ROBOTAXI_HOST + '/itinerary/api/v1/mini-program/order/orderChange',
        method: 'POST',
        payload
    }, true);
}
// 查询订单费用明细
export const queryFareBreakdown = async (payload) => {
    return request({
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
@@ -40,6 +40,7 @@
  packUpIcon: string;
  onSelectRouteClick: () => void;
  isRouteSelectExpanded: boolean;
  canToggleRouteSelect: boolean;
  walkIcon: string;
  renderTripCard: () => JSX.Element | null;
  showCustomerService: boolean;
@@ -101,6 +102,7 @@
    packUpIcon,
    onSelectRouteClick,
    isRouteSelectExpanded,
    canToggleRouteSelect,
    walkIcon,
    renderTripCard,
    showCustomerService,
@@ -130,6 +132,8 @@
    handleCloseEvaluatePopup,
    handleConfirmEvaluate
  } = props;
  // 统一渲染地图与业务弹窗容器
  return (
    <View className='home-container'>
      <Map
@@ -167,7 +171,7 @@
              )}
              <Image onClick={toOrigin} className='origin-icon' src={originIcon} />
              {
                tripStatus == 1 &&
                canToggleRouteSelect &&
                <View>
                  {
                    isRouteSelectExpanded ?
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
@@ -87,6 +87,7 @@
        };
    }, [tripStatus, orderId]);
    // 标题行文案映射
    const titleMap = {
        3: <>预计<View className="time">{waitMinutes}</View>分钟内有车应答</>,
        4: <>车辆还有<View className="time">{durationDisplay.value}</View>{durationDisplay.unit}到达</>,
@@ -99,6 +100,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,
src/robotaxi/pages/begin-route/components/calling-car-modules/use-trip-countdown.ts
@@ -36,6 +36,7 @@
    const pickupTimerRef = useRef<any>(null);
    const finishTimerRef = useRef<any>(null);
    const countdownFinishedRef = useRef(false);
    const pickupTripStatusRef = useRef(tripStatus);
    const pickupEndTimeRef = useRef<number | null>(null);
    const finishEndTimeRef = useRef<number | null>(null);
    const lastFareOrderIdRef = useRef<string | null>(null);
@@ -108,12 +109,21 @@
    }, [tripStatus]);
    useEffect(() => {
        pickupTripStatusRef.current = tripStatus;
    }, [tripStatus]);
    useEffect(() => {
        if (tripStatus !== 5) {
            clearPickupTimer();
            return;
        }
        const runPickupTick = () => {
            // 防止状态已切换后,旧定时回调仍触发取消逻辑
            if (pickupTripStatusRef.current !== 5) {
                clearPickupTimer();
                return;
            }
            const remaining = getRemainingSeconds(pickupEndTimeRef.current);
            if (remaining !== null && remaining <= 0) {
                if (!countdownFinishedRef.current) {
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
@@ -86,6 +86,7 @@
    orderTrunkStatus,
}: Props) {
    // 汇总行程中的倒计时与费用数据
    const {
        waitMinutes,
        limitMinutes,
@@ -109,6 +110,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/css-folder/modify-destination-review.less
New file
@@ -0,0 +1,165 @@
.modify-destination-review-container {
  position: relative;
  width: 100vw;
}
.modify-destination-review {
  position: relative;
  width: 100%;
  border-radius: 40px 40px 0 0;
  background: transparent;
  z-index: 10;
  overflow: hidden;
  transition: height 0.3s ease;
  &-inner {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
  }
  // &-route-panel {
  //   margin: 0 32px 16px 32px;
  // }
  &-content-wrapper {
    position: relative;
    padding-bottom: 180px;
    background: #f2f3f7 url('https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/site-top-bg.png') no-repeat top center;
    background-size: 100% auto;
    border-top: 2px solid #ffffff;
    border-radius: 40px 40px 0 0;
  }
  &-content {
    position: relative;
    padding: 40px 32px 40px;
    z-index: 10;
  }
  &-title {
    position: relative;
    z-index: 1;
    font-size: 28px;
    font-weight: 500;
    margin-bottom: 32px;
  }
  &-sites {
    position: relative;
    z-index: 1;
    padding: 24px;
    border-radius: 24px;
    background: #ffffff;
    box-sizing: border-box;
    border: 2px solid #ffffff;
    backdrop-filter: blur(20px);
    box-shadow: 0 40px 60px 0 rgba(87, 99, 147, 0.1);
  }
  &-site-item {
    display: flex;
    align-items: center;
    min-height: 92px;
    padding: 0 24px;
    border-radius: 20px;
    & + & {
      margin-top: 12px;
    }
  }
  &-site-item-active {
    background: #F5F6FA;
  }
  &-site-dot {
    width: 12px;
    height: 12px;
    margin-right: 20px;
    border-radius: 50%;
    flex-shrink: 0;
    &-old {
      background: #768aff;
    }
    &-new {
      background: #9cc14e;
    }
  }
  &-site-text {
    flex: 1;
    font-size: 30px;
    line-height: 42px;
    color: rgba(0, 0, 0, 0.9);
    word-break: break-all;
  }
  &-footer {
    position: fixed;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 11;
    height: 180px;
    box-sizing: border-box;
    padding: 24px 38px 0 32px;
    background: #fefeff;
    display: flex;
    // align-items: center;
    justify-content: space-between;
    gap: 24px;
  }
  &-fare {
    flex: 1;
    min-width: 0;
  }
  &-fare-main {
    display: flex;
    align-items: flex-end;
    font-size: 24px;
    line-height: 36px;
    color: rgba(0, 0, 0, 0.9);
  }
  &-fare-num {
    margin: 0 6px;
    font-size: 56px;
    line-height: 56px;
    color: rgba(0, 0, 0, 0.9);
  }
  &-fare-discount {
    margin-top: 8px;
    font-size: 24px;
    line-height: 34px;
    color: #f58900;
  }
  &-fare-original {
    margin-top: 6px;
    font-size: 24px;
    line-height: 34px;
    color: rgba(0, 0, 0, 0.4);
    text-decoration: line-through;
  }
  &-confirm {
    flex-shrink: 0;
    width: 296px;
    height: 94px;
    border-radius: 20px;
    background: linear-gradient(102deg, #8475ff 0%, #7dd6ff 102%);
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 28px;
    line-height: 40px;
    color: #ffffff;
  }
}
src/robotaxi/pages/begin-route/components/css-folder/route-selection-panel.less
New file
@@ -0,0 +1,116 @@
.route-select {
  border-radius: 24px;
  background: #FFFFFF;
  box-sizing: border-box;
  border: 1px solid #FFFFFF;
  margin: 0 32px 16px 32px;
  padding: 16px 24px 24px 24px;
  .route-select-type {
    display: flex;
    align-items: center;
    .route-select-title {
      font-size: 24px;
      font-weight: 500;
      line-height: 44px;
      color: rgba(0, 0, 0, 0.9);
    }
    .route-select-preference {
      display: flex;
      align-items: center;
      .route-select-preference-text {
        font-size: 24px;
        font-weight: 500;
        line-height: 40px;
        color: #6D7FD9;
      }
      image {
        flex-shrink: 0;
        margin-left: 9.25px;
        height: 11.97px;
        width: 17.5px;
      }
    }
  }
  .route-select-content {
    display: flex;
    justify-content: space-between;
    gap: 10px;
    margin-top: 16px;
    .route-select-content-item {
      flex: 1;
      padding: 16px 0 24px 24px;
      border-radius: 16px;
      background: #F5F6FA;
      border: 1px solid #F2F3F7;
      .route-select-content-item-title {
        font-size: 20px;
        line-height: 20px;
        color: rgba(0, 0, 0, 0.4);
      }
      .route-select-content-item-time {
        margin-top: 10px;
        font-size: 28px;
        font-weight: 500;
        line-height: 40px;
        color: rgba(0, 0, 0, 0.9);
      }
      .route-select-content-item-distance {
        display: flex;
        align-items: center;
        gap: 12px;
        margin-top: 12px;
        font-size: 20px;
        line-height: 20px;
        color: rgba(0, 0, 0, 0.4);
        .traffic-signal-item {
          display: flex;
          align-items: center;
          gap: 4px;
          .traffic-signal-icon {
            flex-shrink: 0;
            width: 20px;
            height: 20px;
          }
        }
      }
    }
    .active-item {
      background: linear-gradient(97deg, rgba(132, 117, 255, 0.2) 0%, rgba(125, 214, 255, 0.2) 103%) !important;
      box-sizing: border-box;
      position: relative;
      .route-select-content-item-title {
        color: #6D7FD9;
      }
      &::after {
        content: "";
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        border-radius: 16px;
        padding: 1px;
        background: linear-gradient(180deg, #AAA8FF -10%, rgba(41, 159, 255, 0.0469) 108%, rgba(0, 0, 0, 0) 108%);
        -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
        -webkit-mask-composite: xor;
        mask-composite: exclude;
        pointer-events: none;
      }
    }
  }
}
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/modify-destination-review.tsx
New file
@@ -0,0 +1,178 @@
import { View, Text } from '@tarojs/components';
import Taro from '@tarojs/taro';
import { useEffect, useState, useRef } from 'react';
import './css-folder/modify-destination-review.less';
import RouteSelectionPanel from './route-selection-panel';
type Props = {
  previousEndSite: any;
  nextEndSite: any;
  previousFareEstimate?: any;
  fareEstimate: any;
  routeOptions?: any[];
  selectedRouteIndex?: number;
  isRouteSelectExpanded?: boolean;
  onToggleRouteSelect?: (expanded: boolean) => void;
  handleSelectRoute?: (index: number) => void;
  onEditDestination?: () => void;
  onConfirm: () => void;
};
export default function ModifyDestinationReview({
  previousEndSite,
  nextEndSite,
  previousFareEstimate,
  fareEstimate,
  routeOptions = [],
  selectedRouteIndex = 0,
  isRouteSelectExpanded = false,
  onToggleRouteSelect,
  handleSelectRoute,
  onEditDestination,
  onConfirm
}: Props) {
  // 高度统一使用 rpx,'auto' 表示由内容撑开
  const [boxHeightRpx, setBoxHeightRpx] = useState<number | 'auto'>('auto');
  const isDragging = useRef(false);
  const startY = useRef(0);
  const startHeight = useRef(0);
  const windowWidth = useRef(375);
  useEffect(() => {
    const sysInfo = Taro.getSystemInfoSync();
    windowWidth.current = sysInfo.windowWidth;
  }, []);
  useEffect(() => {
    if (!isDragging.current) {
      if (isRouteSelectExpanded) {
        // 提取数字,兼容之前可能带有 'px' 的情况
        setBoxHeightRpx(378); // 展开时高度
      } else {
        setBoxHeightRpx('auto'); // 正常高度自适应
      }
    }
  }, [isRouteSelectExpanded]);
  const pxToRpx = (px: number) => {
    return (px * 750) / windowWidth.current;
  };
  const handleTouchStart = (e: any) => {
    if (!isRouteSelectExpanded) return;
    isDragging.current = true;
    startY.current = e.touches[0].clientY;
    // 如果是 auto,我们在拖拽开始时需要获取当前真实高度并转换为 rpx
    // 但为了流畅,通常展开时已经是具体的 rpx 数字了
    startHeight.current = typeof boxHeightRpx === 'number' ? boxHeightRpx : 378;
  };
  const handleTouchMove = (e: any) => {
    if (!isRouteSelectExpanded || !isDragging.current) return;
    const deltaYPx = e.touches[0].clientY - startY.current;
    const deltaYRpx = pxToRpx(deltaYPx);
    // 往下拉 deltaY > 0,高度变小;往上拉 deltaY < 0,高度变大
    let newHeightRpx = startHeight.current - deltaYRpx;
    // 限制拖拽范围
    if (newHeightRpx < 150) newHeightRpx = 150;
    if (newHeightRpx > 800) newHeightRpx = 800;
    setBoxHeightRpx(newHeightRpx);
  };
  const handleTouchEnd = () => {
    if (!isRouteSelectExpanded || !isDragging.current) return;
    isDragging.current = false;
    // 不自动回弹:让高度停留在滑动结束的位置
    // 但如果滑得太低,当做收起
    if (typeof boxHeightRpx === 'number' && boxHeightRpx <= 200) {
       onToggleRouteSelect && onToggleRouteSelect(false);
    }
  };
  return (
    <View className="modify-destination-review-container">
      {isRouteSelectExpanded && routeOptions.length > 0 && (
        <View className='modify-destination-review-route-panel'>
          <RouteSelectionPanel
            routeOptions={routeOptions}
            selectedRouteIndex={selectedRouteIndex}
            handleSelectRoute={handleSelectRoute}
          />
        </View>
      )}
      <View
        className='modify-destination-review'
        style={{
          height: boxHeightRpx === 'auto' ? 'auto' : Taro.pxTransform(boxHeightRpx as number),
          transition: isDragging.current ? 'none' : 'height 0.3s ease'
        }}
        catchMove={isRouteSelectExpanded}
        onTouchStart={(e) => {
          if (!isRouteSelectExpanded) return;
          e.stopPropagation();
          handleTouchStart(e);
        }}
        onTouchMove={(e) => {
          if (!isRouteSelectExpanded) return;
          e.stopPropagation();
          handleTouchMove(e);
        }}
        onTouchEnd={(e) => {
          if (!isRouteSelectExpanded) return;
          e.stopPropagation();
          handleTouchEnd();
        }}
      >
        <View className='modify-destination-review-content-wrapper'>
          <View className='modify-destination-review-content'>
            <View className='modify-destination-review-title'>
              由于目的地变更,本次行程费用已更新
            </View>
            <View className='modify-destination-review-sites'>
              <View className='modify-destination-review-site-item'>
                <View className='modify-destination-review-site-dot modify-destination-review-site-dot-old' />
                <View className='modify-destination-review-site-text'>{previousEndSite?.name || '-'}</View>
              </View>
              <View
                className='modify-destination-review-site-item modify-destination-review-site-item-active'
                onClick={() => onEditDestination?.()}
              >
                <View className='modify-destination-review-site-dot modify-destination-review-site-dot-new' />
                <View className='modify-destination-review-site-text'>{nextEndSite?.name || '-'}</View>
              </View>
            </View>
          </View>
          <View className='modify-destination-review-footer'>
            <View className='modify-destination-review-fare'>
              <View className='modify-destination-review-fare-main'>
                预估
                <Text className='modify-destination-review-fare-num'>{fareEstimate?.finalFareStr || '-'}</Text>
                元
              </View>
              {Number(fareEstimate?.discountAmount) > 0 && (
                <View className='modify-destination-review-fare-discount'>
                  已优惠{fareEstimate.discountAmount}元
                </View>
              )}
              {previousFareEstimate?.finalFareStr && previousFareEstimate?.finalFareStr !== fareEstimate?.finalFareStr && (
                <View className='modify-destination-review-fare-original'>
                  原预估{previousFareEstimate.finalFareStr}元
                </View>
              )}
            </View>
            <View className='modify-destination-review-confirm' onClick={onConfirm}>
              确认修改
            </View>
          </View>
        </View>
      </View>
    </View>
  );
}
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
@@ -5,11 +5,7 @@
import './css-folder/release-route.less'
import tipBlueIcon from '../../../assets/tip-blue-icon.png'
import tipDefaultIcon from '../../../assets/tip-default-icon.png'
import selectLineIcon from '../../../assets/select-line.png'
import trafficSignalIcon from '../../../assets/traffic-signal.png'
import RoutePreferencePopup, { RoutePreferenceOption } from './route-preference-popup';
import { saveRoutePreference, queryRoutePreferences } from '../../../api/begin-route';
import { formatDurationDisplay, formatTripDistance } from '../../../../utils/Robotaxi-public-method';
import RouteSelectionPanel from './route-selection-panel';
type Props = {
    startSite: any;
@@ -36,67 +32,8 @@
    const dragFrame = useRef<any>(null);
    const pendingHeight = useRef<number | null>(null);
    const [showPreferencePopup, setShowPreferencePopup] = useState(false);
    const [preference, setPreference] = useState<RoutePreferenceOption>({ label: '智能推荐', value: '智能推荐', code: '智能推荐' });
    const [preferenceOptions, setPreferenceOptions] = useState<RoutePreferenceOption[]>([]);
    const openCityId = Number(Taro.getStorageSync('selectedCity')?.id || 0);
    const selectedRoute = routeOptions?.[selectedRouteIndex];
    const isOperateFree = Number(selectedRoute?.operateFree) === 1;
    useEffect(() => {
        let cancelled = false;
        queryRoutePreferences({ openCityId }).then((res: any) => {
            if (cancelled) return;
            const list = Array.isArray(res) ? res : (Array.isArray(res?.list) ? res.list : (Array.isArray(res?.data) ? res.data : []));
            const normalized = list.map((item: any) => {
                const label = item?.label || item?.name || item?.displayName || item?.preferenceName || item?.value;
                const value = item?.value ?? item?.code ?? item?.preferenceCode ?? label;
                const code = item?.code ?? item?.preferenceCode ?? item?.value ?? label;
                if (!label || value === undefined || value === null) return null;
                return { label: String(label), value: String(value), code: String(code) };
            }).filter(Boolean) as RoutePreferenceOption[];
            if (normalized.length > 0) {
                setPreferenceOptions(normalized);
            }
        }).catch(() => {});
        return () => { cancelled = true; };
    }, [openCityId]);
    // Sync preference label when route selection changes externally (e.g. clicking map or route card)
    useEffect(() => {
        if (routeOptions && routeOptions[selectedRouteIndex]) {
            const currentRoute = routeOptions[selectedRouteIndex];
            // Match label based on preference from API response
            let mappedLabel = '智能推荐';
            if (preferenceOptions.length > 0 && currentRoute.preference !== undefined && currentRoute.preference !== null) {
                const matchedOption = preferenceOptions.find(opt => String(opt.code) === String(currentRoute.preference));
                if (matchedOption) {
                    mappedLabel = matchedOption.label;
                } else if (currentRoute.routePlanName) {
                    // Fallback to routePlanName if preference matching fails
                    const name = currentRoute.routePlanName;
                    mappedLabel = name.includes('-') ? name.split('-')[1] : name;
                }
            } else if (currentRoute.routePlanName) {
                // Fallback to routePlanName if preference matching fails
                const name = currentRoute.routePlanName;
                mappedLabel = name.includes('-') ? name.split('-')[1] : name;
            }
            setPreference(prev => {
                if (prev.label !== mappedLabel) {
                    const newCode = currentRoute.preference !== undefined && currentRoute.preference !== null ? String(currentRoute.preference) : mappedLabel;
                    return {
                        label: mappedLabel,
                        value: newCode,
                        code: newCode
                    };
                }
                return prev;
            });
        }
    }, [selectedRouteIndex, routeOptions, preferenceOptions]);
    useEffect(() => {
        if (!isRouteSelectExpanded && boxHeight !== null && minHeight.current > 0) {
@@ -194,16 +131,19 @@
        }
    };
    // 跳转选点页(起点/终点)
    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) {
@@ -212,41 +152,6 @@
            return remainMinutes > 0 ? `${hours}小时${remainMinutes}分钟` : `${hours}小时`;
        }
        return `${safeMinutes}分钟`;
    }
    const handleSelectPreference = async (item: RoutePreferenceOption) => {
        setPreference(item);
        if (routeOptions && routeOptions.length > 0 && handleSelectRoute) {
            const targetIndex = routeOptions.findIndex(route => {
                // Exact match by preference code
                if (route.preference !== undefined && route.preference !== null && String(route.preference) === String(item.code)) {
                    return true;
                }
                // Fallback match by name
                if (route.routePlanName) {
                    const name = route.routePlanName;
                    const mappedLabel = name.includes('-') ? name.split('-')[1] : name;
                    if (mappedLabel === item.label) {
                        return true;
                    }
                }
                return false;
            });
            if (targetIndex !== -1) {
                handleSelectRoute(targetIndex);
            }
        }
        try {
            await saveRoutePreference({
                code: item.code
            });
        } catch (error) {
            // keep local selection state to avoid interrupting the popup interaction
        }
    }
    return (
@@ -274,45 +179,11 @@
            >
                <View className='release-route-content'>
                    {isRouteSelectExpanded && (
                        <View className="route-select">
                            <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>
                                    <Image src={selectLineIcon} />
                                </View>
                            </View>
                            <View className="route-select-content">
                                {
                                    routeOptions.map((item, index) => {
                                        const routeData = item.route || {};
                                        const time = formatDurationDisplay(routeData.duration || 0);
                                        const dist = formatTripDistance(routeData.distance || 0);
                                        const isSelected = index === selectedRouteIndex;
                                        return (
                                            <View
                                                className={`route-select-content-item ${isSelected ? 'active-item' : ''}`}
                                                key={index}
                                                onClick={() => handleSelectRoute && handleSelectRoute(index)}
                                            >
                                                <View className="route-select-content-item-title">{item.label || (index === 0 ? '智能推荐' : `路线${index + 1}`)}</View>
                                                <View className="route-select-content-item-time">{time.value}{time.unit}</View>
                                                <View className="route-select-content-item-distance">
                                                    <View>{dist}</View>
                                                    {typeof routeData.trafficLights !== 'undefined' && (
                                                        <View className="traffic-signal-item">
                                                            <Image className="traffic-signal-icon" src={trafficSignalIcon} />
                                                            <Text>{routeData.trafficLights}个</Text>
                                                        </View>
                                                    )}
                                                </View>
                                            </View>
                                        );
                                    })
                                }
                            </View>
                        </View>
                        <RouteSelectionPanel
                            routeOptions={routeOptions}
                            selectedRouteIndex={selectedRouteIndex}
                            handleSelectRoute={handleSelectRoute}
                        />
                    )}
                    <View className="select-site">
                        <Image className="site-top-bg" src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/site-top-bg.png' />
@@ -381,13 +252,6 @@
                    </View>
                </View>
            </View>
            <RoutePreferencePopup
                show={showPreferencePopup}
                value={preference.value}
                openCityId={openCityId}
                onClose={() => setShowPreferencePopup(false)}
                onSelect={handleSelectPreference}
            />
        </View>
    )
}
src/robotaxi/pages/begin-route/components/route-selection-panel.tsx
New file
@@ -0,0 +1,163 @@
import Taro from '@tarojs/taro';
import { useEffect, useState } from 'react';
import { View, Text, Image } from '@tarojs/components';
import selectLineIcon from '../../../assets/select-line.png';
import trafficSignalIcon from '../../../assets/traffic-signal.png';
import RoutePreferencePopup, { RoutePreferenceOption } from './route-preference-popup';
import { queryRoutePreferences, saveRoutePreference } from '../../../api/begin-route';
import { formatDurationDisplay, formatTripDistance } from '../../../../utils/Robotaxi-public-method';
import './css-folder/route-selection-panel.less';
type Props = {
  routeOptions?: any[];
  selectedRouteIndex?: number;
  handleSelectRoute?: (index: number) => void;
};
export default function RouteSelectionPanel({
  routeOptions = [],
  selectedRouteIndex = 0,
  handleSelectRoute
}: Props) {
  const [showPreferencePopup, setShowPreferencePopup] = useState(false);
  const [preference, setPreference] = useState<RoutePreferenceOption>({
    label: '智能推荐',
    value: '智能推荐',
    code: '智能推荐'
  });
  const [preferenceOptions, setPreferenceOptions] = useState<RoutePreferenceOption[]>([]);
  const openCityId = Number(Taro.getStorageSync('selectedCity')?.id || 0);
  useEffect(() => {
    let cancelled = false;
    queryRoutePreferences({ openCityId }).then((res: any) => {
      if (cancelled) return;
      const list = Array.isArray(res) ? res : (Array.isArray(res?.list) ? res.list : (Array.isArray(res?.data) ? res.data : []));
      const normalized = list.map((item: any) => {
        const label = item?.label || item?.name || item?.displayName || item?.preferenceName || item?.value;
        const value = item?.value ?? item?.code ?? item?.preferenceCode ?? label;
        const code = item?.code ?? item?.preferenceCode ?? item?.value ?? label;
        if (!label || value === undefined || value === null) return null;
        return { label: String(label), value: String(value), code: String(code) };
      }).filter(Boolean) as RoutePreferenceOption[];
      if (normalized.length > 0) {
        setPreferenceOptions(normalized);
      }
    }).catch(() => {});
    return () => {
      cancelled = true;
    };
  }, [openCityId]);
  useEffect(() => {
    if (!routeOptions || !routeOptions[selectedRouteIndex]) return;
    const currentRoute = routeOptions[selectedRouteIndex];
    let mappedLabel = '智能推荐';
    if (preferenceOptions.length > 0 && currentRoute.preference !== undefined && currentRoute.preference !== null) {
      const matchedOption = preferenceOptions.find(opt => String(opt.code) === String(currentRoute.preference));
      if (matchedOption) {
        mappedLabel = matchedOption.label;
      } else if (currentRoute.routePlanName) {
        const name = currentRoute.routePlanName;
        mappedLabel = name.includes('-') ? name.split('-')[1] : name;
      }
    } else if (currentRoute.routePlanName) {
      const name = currentRoute.routePlanName;
      mappedLabel = name.includes('-') ? name.split('-')[1] : name;
    }
    setPreference(prev => {
      if (prev.label !== mappedLabel) {
        const newCode = currentRoute.preference !== undefined && currentRoute.preference !== null ? String(currentRoute.preference) : mappedLabel;
        return {
          label: mappedLabel,
          value: newCode,
          code: newCode
        };
      }
      return prev;
    });
  }, [selectedRouteIndex, routeOptions, preferenceOptions]);
  const handleSelectPreference = async (item: RoutePreferenceOption) => {
    setPreference(item);
    if (routeOptions.length > 0 && handleSelectRoute) {
      const targetIndex = routeOptions.findIndex(route => {
        if (route.preference !== undefined && route.preference !== null && String(route.preference) === String(item.code)) {
          return true;
        }
        if (route.routePlanName) {
          const name = route.routePlanName;
          const mappedLabel = name.includes('-') ? name.split('-')[1] : name;
          if (mappedLabel === item.label) {
            return true;
          }
        }
        return false;
      });
      if (targetIndex !== -1) {
        handleSelectRoute(targetIndex);
      }
    }
    try {
      await saveRoutePreference({ code: item.code });
    } catch (error) {
      // Keep local state even if persistence fails.
    }
  };
  return (
    <View className='route-select'>
      <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>
          <Image src={selectLineIcon} />
        </View>
      </View>
      <View className='route-select-content'>
        {routeOptions.map((item, index) => {
          const routeData = item.route || {};
          const time = formatDurationDisplay(routeData.duration || 0);
          const dist = formatTripDistance(routeData.distance || 0);
          const isSelected = index === selectedRouteIndex;
          return (
            <View
              className={`route-select-content-item ${isSelected ? 'active-item' : ''}`}
              key={index}
              onClick={() => handleSelectRoute && handleSelectRoute(index)}
            >
              <View className='route-select-content-item-title'>{item.label || (index === 0 ? '智能推荐' : `路线${index + 1}`)}</View>
              <View className='route-select-content-item-time'>{time.value}{time.unit}</View>
              <View className='route-select-content-item-distance'>
                <View>{dist}</View>
                {typeof routeData.trafficLights !== 'undefined' && (
                  <View className='traffic-signal-item'>
                    <Image className='traffic-signal-icon' src={trafficSignalIcon} />
                    <Text>{routeData.trafficLights}个</Text>
                  </View>
                )}
              </View>
            </View>
          );
        })}
      </View>
      <RoutePreferencePopup
        show={showPreferencePopup}
        value={preference.value}
        openCityId={openCityId}
        onClose={() => setShowPreferencePopup(false)}
        onSelect={handleSelectPreference}
      />
    </View>
  );
}
src/robotaxi/pages/begin-route/constants/modify-destination.ts
New file
@@ -0,0 +1,4 @@
export const MODIFY_DESTINATION_SESSION_KEY = 'modifyDestinationSession';
export const MODIFY_DESTINATION_CANDIDATE_KEY = 'modifyDestinationCandidate';
export const MODIFY_DESTINATION_OVERRIDE_KEY = 'modifyDestinationOverride';
export const MODIFY_DESTINATION_ROUTE_POINTS_KEY = 'modifyDestinationRoutePoints';
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
@@ -5,22 +5,28 @@
  tripStatus: number;
  statusRule: any;
  isAndroid: boolean;
  showTrip1StyleEndpoint: boolean;
  startSite: any;
  endSite: any;
  vehicleLocation: { latitude: number; longitude: number } | null;
  vehicleDirection: number | null;
  vehicleMarkerId: number;
  arrivedMarkerReady: boolean;
  arrivedTooltipReady: boolean;
};
const defaultDeps: Deps = {
  tripStatus: 1,
  statusRule: { showPickup: true, showDropoff: true, showCar: false },
  isAndroid: false,
  showTrip1StyleEndpoint: false,
  startSite: null,
  endSite: null,
  vehicleLocation: null,
  vehicleDirection: null,
  vehicleMarkerId: 3
  vehicleMarkerId: 3,
  arrivedMarkerReady: false,
  arrivedTooltipReady: false
};
export default function useMapMarkers(rawDeps?: Partial<Deps>) {
@@ -48,7 +54,7 @@
      ...(useAndroidNativeTrip1Callout
        ? {
          callout: {
            content: deps.tripStatus === 1 ? `${pickupSite?.name || '上车点'} >` : (pickupSite?.name || '上车点'),
            content: (deps.tripStatus === 1 || deps.showTrip1StyleEndpoint) ? `${pickupSite?.name || '上车点'} >` : (pickupSite?.name || '上车点'),
            color: '#000000',
            fontSize: 12,
            bgColor: '#FFFFFF',
@@ -81,7 +87,7 @@
      ...(useAndroidNativeTrip1Callout || deps.tripStatus == 6
        ? {
          callout: {
            content: deps.tripStatus === 1 ? `${dropoffName} >` : dropoffName,
            content: (deps.tripStatus === 1 || deps.showTrip1StyleEndpoint) ? `${dropoffName} >` : dropoffName,
            color: '#000000',
            fontSize: 12,
            bgColor: '#FFFFFF',
@@ -134,13 +140,15 @@
  useEffect(() => {
    const pickupSite = deps.startSite?.latitude && deps.startSite?.longitude ? deps.startSite : Taro.getStorageSync('startSite');
    const dropoffSite = deps.endSite?.latitude && deps.endSite?.longitude ? deps.endSite : Taro.getStorageSync('endSite');
    const useAndroidNativeTrip1Callout = deps.isAndroid && [1, 5, 8, 9, 10].includes(deps.tripStatus);
    const useAndroidNativeTrip1Callout = deps.isAndroid && ([1, 5, 8, 9, 10].includes(deps.tripStatus) || deps.showTrip1StyleEndpoint);
    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) => [
@@ -154,8 +162,7 @@
      .join(';');
    const hasDropoffMarker = markersRef.current.some((m: any) => m.id === 2);
    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 shouldSkipUpdate = (shouldHoldMarkerByTranslate && (deps.isAndroid || tripStatus6MarkerRefreshedRef.current)) || shouldHoldMarkerWhenArrived;
    const shouldSkipUpdate = shouldHoldMarkerByTranslate && (deps.isAndroid || tripStatus6MarkerRefreshedRef.current);
    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
@@ -49,7 +49,10 @@
    const mapRectRef = useRef<any>(null);
    const viewportKeyRef = useRef<string>('');
    const orderDetailViewportLockedRef = useRef(false);
    const tripStatus6ViewportLockedRef = useRef(false);
    const orderDetailEnterSignatureRef = useRef('');
    const orderDetailRouteUpdatedAfterEnterRef = useRef(false);
    const isAndroid = useMemo(() => {
        try {
@@ -183,6 +186,22 @@
        return { points, keyParts };
    };
    const buildOrderDetailRouteSignature = (pickupSite: any, dropoffSite: any) => {
        const firstPoint = deps.routePoints[0];
        const lastPoint = deps.routePoints[deps.routePoints.length - 1];
        return [
            deps.routePoints.length,
            firstPoint?.latitude ?? '',
            firstPoint?.longitude ?? '',
            lastPoint?.latitude ?? '',
            lastPoint?.longitude ?? '',
            pickupSite?.latitude ?? '',
            pickupSite?.longitude ?? '',
            dropoffSite?.latitude ?? '',
            dropoffSite?.longitude ?? '',
        ].join('|');
    };
    const updateViewportByPayload = (viewportType: string, points: any[]) => {
        const mapCtx = Taro.createMapContext('map');
        const bottomPadding = computeBottomPadding(points, viewportType);
@@ -263,10 +282,44 @@
        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(() => {
        if (![8, 9].includes(deps.tripStatus)) {
            orderDetailViewportLockedRef.current = false;
            orderDetailRouteUpdatedAfterEnterRef.current = false;
            orderDetailEnterSignatureRef.current = '';
            return;
        }
        const { pickupSite, dropoffSite } = resolveViewportSites();
        orderDetailEnterSignatureRef.current = buildOrderDetailRouteSignature(pickupSite, dropoffSite);
        orderDetailRouteUpdatedAfterEnterRef.current = false;
        orderDetailViewportLockedRef.current = false;
        viewportKeyRef.current = '';
        setMapScale(14);
    }, [deps.tripStatus]);
    useEffect(() => {
        if (![8, 9].includes(deps.tripStatus)) return;
        const { pickupSite, dropoffSite } = resolveViewportSites();
        const currentSignature = buildOrderDetailRouteSignature(pickupSite, dropoffSite);
        if (!orderDetailEnterSignatureRef.current) {
            orderDetailEnterSignatureRef.current = currentSignature;
            return;
        }
        if (currentSignature === orderDetailEnterSignatureRef.current) return;
        orderDetailRouteUpdatedAfterEnterRef.current = true;
        orderDetailViewportLockedRef.current = false;
        viewportKeyRef.current = '';
    }, [deps.tripStatus, deps.routePoints, deps.startSite?.latitude, deps.startSite?.longitude, deps.endSite?.latitude, deps.endSite?.longitude]);
    useEffect(() => {
        const { pickupSite, dropoffSite } = resolveViewportSites();
@@ -274,6 +327,14 @@
        if (viewportType === 'dropoff') return;
        const { points, keyParts } = buildViewportPayload(viewportType, pickupSite, dropoffSite);
        if (points.length === 0) return;
        if (
            [8, 9].includes(deps.tripStatus) &&
            viewportType === 'order_detail' &&
            orderDetailViewportLockedRef.current &&
            orderDetailRouteUpdatedAfterEnterRef.current
        ) {
            return;
        }
        if (deps.tripStatus === 6 && viewportType === 'route' && tripStatus6ViewportLockedRef.current) return;
        const nextKey = keyParts.join('|');
        if (nextKey === viewportKeyRef.current) return;
@@ -281,6 +342,10 @@
        updateViewportByPayload(viewportType, points);
        if (deps.tripStatus === 6 && viewportType === 'route') {
            tripStatus6ViewportLockedRef.current = true;
        }
        if ([8, 9].includes(deps.tripStatus) && viewportType === 'order_detail') {
            const hasOrderDetailRouteAfterEnter = orderDetailRouteUpdatedAfterEnterRef.current && deps.routePoints.length > 0;
            orderDetailViewportLockedRef.current = hasOrderDetailRouteAfterEnter;
        }
    }, [deps.tripStatus, deps.statusRule, deps.routePoints, deps.walkingPoints, deps.startSite?.latitude, deps.startSite?.longitude, deps.endSite?.latitude, deps.endSite?.longitude, deps.vehicleLocation, deps.longitude, deps.latitude, mapBottomPadding]);
@@ -309,6 +374,7 @@
        setShowLocationFlag,
        mapRectRef,
        viewportKeyRef,
        orderDetailViewportLockedRef,
        tripStatus6ViewportLockedRef,
        isAndroid,
        resolveSafeBottomInset,
src/robotaxi/pages/begin-route/hooks/use-modify-destination-flow.ts
New file
@@ -0,0 +1,209 @@
import Taro from '@tarojs/taro';
import { useEffect, useMemo, useState } from 'react';
import { orderChange } from '../../../api/begin-route';
import { getOrderActive } from '../../../api/home';
import {
  MODIFY_DESTINATION_CANDIDATE_KEY,
  MODIFY_DESTINATION_OVERRIDE_KEY,
  MODIFY_DESTINATION_ROUTE_POINTS_KEY,
  MODIFY_DESTINATION_SESSION_KEY
} from '../constants/modify-destination';
type Deps = {
  tripStatus: number;
  orderId: string;
  endSite: any;
  setEndSite: (site: any) => void;
  toSelectSite: (pageType: 'start' | 'end') => void;
  onModifySuccess?: () => void;
};
const isFiniteCoordinate = (value: any) => Number.isFinite(Number(value));
const isValidSite = (site: any) => {
  if (!site) return false;
  return isFiniteCoordinate(site.latitude) && isFiniteCoordinate(site.longitude);
};
const isSameSite = (left: any, right: any) => {
  if (!left || !right) return false;
  if (left.id && right.id) {
    return String(left.id) === String(right.id);
  }
  return String(left.name || '') === String(right.name || '')
    && Number(left.latitude) === Number(right.latitude)
    && Number(left.longitude) === Number(right.longitude);
};
const clearModifyDestinationSession = () => {
  Taro.removeStorageSync(MODIFY_DESTINATION_SESSION_KEY);
  Taro.removeStorageSync(MODIFY_DESTINATION_CANDIDATE_KEY);
  Taro.removeStorageSync(MODIFY_DESTINATION_ROUTE_POINTS_KEY);
};
export default function useModifyDestinationFlow(deps: Deps) {
  const [previewEndSite, setPreviewEndSite] = useState<any>(null);
  const [previousEndSite, setPreviousEndSite] = useState<any>(null);
  const [previousFareEstimate, setPreviousFareEstimate] = useState<any>(null);
  const [previousRoutePoints, setPreviousRoutePoints] = useState<any[]>([]);
  const [overrideEndSite, setOverrideEndSite] = useState<any>(() => {
    const cached = Taro.getStorageSync(MODIFY_DESTINATION_OVERRIDE_KEY);
    return isValidSite(cached) ? cached : null;
  });
  useEffect(() => {
    if (!deps.orderId || ![4, 5, 6, 7, 8, 9].includes(deps.tripStatus)) {
      setPreviewEndSite(null);
      setPreviousEndSite(null);
      setPreviousFareEstimate(null);
      setPreviousRoutePoints([]);
      if (![4, 5, 6, 7, 8, 9].includes(deps.tripStatus)) {
        clearModifyDestinationSession();
        Taro.removeStorageSync(MODIFY_DESTINATION_OVERRIDE_KEY);
        setOverrideEndSite(null);
      }
      return;
    }
    const session = Taro.getStorageSync(MODIFY_DESTINATION_SESSION_KEY);
    const candidate = Taro.getStorageSync(MODIFY_DESTINATION_CANDIDATE_KEY);
    const cachedOverride = Taro.getStorageSync(MODIFY_DESTINATION_OVERRIDE_KEY);
    const cachedRoutePoints = Taro.getStorageSync(MODIFY_DESTINATION_ROUTE_POINTS_KEY);
    if (isValidSite(cachedOverride)) {
      setOverrideEndSite(cachedOverride);
    }
    if (session?.active && isValidSite(candidate)) {
      if (isSameSite(session.previousEndSite, candidate)) {
        clearModifyDestinationSession();
        setPreviewEndSite(null);
        return;
      }
      setPreviousEndSite(session.previousEndSite || deps.endSite || null);
      setPreviousFareEstimate(session.previousFareEstimate || null);
      setPreviousRoutePoints(Array.isArray(cachedRoutePoints) ? cachedRoutePoints : []);
      setPreviewEndSite(candidate);
      return;
    }
    if (session?.active && !isValidSite(candidate)) {
      clearModifyDestinationSession();
    }
  }, [
    deps.tripStatus,
    deps.orderId,
    deps.endSite?.id,
    deps.endSite?.name,
    deps.endSite?.latitude,
    deps.endSite?.longitude,
  ]);
  useEffect(() => {
    if (!isValidSite(overrideEndSite)) return;
    if (isSameSite(overrideEndSite, deps.endSite)) return;
    deps.setEndSite(overrideEndSite);
  }, [
    overrideEndSite,
    deps.endSite?.id,
    deps.endSite?.name,
    deps.endSite?.latitude,
    deps.endSite?.longitude
  ]);
  const handleStartModifyDestination = (fareEstimate?: any, routePoints?: any[]) => {
    Taro.setStorageSync(MODIFY_DESTINATION_SESSION_KEY, {
      active: true,
      previousEndSite: deps.endSite || null,
      previousFareEstimate: fareEstimate || null,
      orderId: deps.orderId || '',
      tripStatus: deps.tripStatus,
      updatedAt: Date.now()
    });
    Taro.setStorageSync(MODIFY_DESTINATION_ROUTE_POINTS_KEY, Array.isArray(routePoints) ? routePoints : []);
    Taro.removeStorageSync(MODIFY_DESTINATION_CANDIDATE_KEY);
    deps.toSelectSite('end');
  };
  const clearLocalDestinationOverride = () => {
    setOverrideEndSite(null);
    Taro.removeStorageSync(MODIFY_DESTINATION_OVERRIDE_KEY);
  };
  const handleConfirmModifyDestination = async (selectedRoute?: any) => {
    if (!isValidSite(previewEndSite)) return;
    try {
      Taro.showLoading({ title: '正在修改...', mask: true });
      let targetOrderId: string | number = deps.orderId || '';
      const activeOrderRes: any = await getOrderActive();
      targetOrderId = activeOrderRes?.id ? activeOrderRes.id : targetOrderId;
      // 如果后端需要字符串格式可以显式转String,如果后端支持number直接传值,保留原始格式防止隐式转换截断
      // 如果原本因为 Number() 导致大整数丢失精度,去掉 Number() 即可
      await orderChange({
        endPointId: previewEndSite.id || 0,
        orderId: targetOrderId,
        selectedRouteId: selectedRoute?.route?.routeID || selectedRoute?.routeId || '',
        selectedRoutePlanRule: selectedRoute?.amapStrategy || ''
      });
      Taro.hideLoading();
      // 清除本地修改状态,不再作为“本地覆盖”存在,因为订单已真正变更
      // 同步更新缓存中新的终点信息
      const newEndSite = {
        ...deps.endSite,
        ...previewEndSite,
        name: previewEndSite.name,
        latitude: previewEndSite.latitude,
        longitude: previewEndSite.longitude,
        id: previewEndSite.id
      };
      Taro.setStorageSync('endSite', newEndSite);
      deps.setEndSite(newEndSite);
      setPreviewEndSite(null);
      setPreviousEndSite(null);
      setPreviousFareEstimate(null);
      setPreviousRoutePoints([]);
      clearModifyDestinationSession();
      clearLocalDestinationOverride();
      Taro.showToast({
        title: '目的地已更新',
        icon: 'none'
      });
      if (typeof deps.onModifySuccess === 'function') {
        deps.onModifySuccess();
      }
    } catch (e: any) {
      Taro.hideLoading();
      Taro.showToast({
        title: e?.message || '修改失败,请重试',
        icon: 'none'
      });
    }
  };
  const isModifyDestinationReviewVisible = isValidSite(previewEndSite);
  const hasLocalDestinationOverride = isValidSite(overrideEndSite);
  const effectiveEndSite = useMemo(() => (
    previewEndSite || overrideEndSite || deps.endSite
  ), [previewEndSite, overrideEndSite, deps.endSite]);
  return {
    effectiveEndSite,
    previewEndSite,
    previousEndSite,
    previousFareEstimate,
    previousRoutePoints,
    isModifyDestinationReviewVisible,
    hasLocalDestinationOverride,
    clearLocalDestinationOverride,
    handleStartModifyDestination,
    handleConfirmModifyDestination
  };
}
src/robotaxi/pages/begin-route/hooks/use-route-manager.ts
@@ -11,6 +11,7 @@
  tripStatusRef: React.MutableRefObject<number>;
  statusRule: any;
  shouldShowRoute: boolean;
  allowSelectableRoutes: boolean;
  startSite: any;
  endSite?: any;
  setStartSite?: (site: any) => void;
@@ -27,6 +28,7 @@
  tripStatusRef: { current: 1 },
  statusRule: { showWalking: false },
  shouldShowRoute: false,
  allowSelectableRoutes: false,
  startSite: null,
  resolveSafeTopInset: () => 0,
  computeBottomPadding: () => 280,
@@ -217,7 +219,7 @@
        newEndLng = routeData.destination.longitude ?? newEndLng;
    }
    if (Number.isFinite(newStartLat) && Number.isFinite(newStartLng) && typeof deps.setStartSite === 'function') {
    if (deps.tripStatusRef.current === 1 && Number.isFinite(newStartLat) && Number.isFinite(newStartLng) && typeof deps.setStartSite === 'function') {
        deps.setStartSite({
           ...deps.startSite,
           latitude: newStartLat,
@@ -226,7 +228,7 @@
        });
    }
    if (Number.isFinite(newEndLat) && Number.isFinite(newEndLng) && typeof deps.setEndSite === 'function') {
    if (deps.tripStatusRef.current === 1 && Number.isFinite(newEndLat) && Number.isFinite(newEndLng) && typeof deps.setEndSite === 'function') {
        deps.setEndSite({
           ...deps.endSite,
           latitude: newEndLat,
@@ -246,8 +248,9 @@
    
    if (Array.isArray(points) && points.length > 0) {
      applyRoutePoints(points);
      if (deps.tripStatusRef.current === 1) {
        // 在 tripStatus === 1 时,我们需要包含所有路线的点,以便自适应视野
      // 如果允许选择路线(当前业务下未发单及修改目的地确认态时 allowSelectableRoutes 为 true)
      if (deps.allowSelectableRoutes) {
        // 在 tripStatus === 1 或 修改目的地确认态 时,我们需要包含所有路线的点,以便自适应视野
        const allRoutesPoints: any[] = [];
        allRoutePoints.forEach(pts => {
           if (Array.isArray(pts)) {
@@ -270,8 +273,8 @@
  };
  const resolveRouteIndexByTap = (e: any) => {
    // 确保当前处于发单状态(有路线选项),并且有路线可供选择
    if (deps.tripStatusRef.current !== 1 || allRoutePoints.length <= 1) return -1;
    // 确保允许选择路线并且有多条路线可供选择
    if (!deps.allowSelectableRoutes || allRoutePoints.length <= 1) return -1;
    // 优先使用 polylineId;如果运行时未返回 id,则退化为用点击坐标匹配最近路线
    let targetIndex = -1;
@@ -412,8 +415,7 @@
      const points = Array.isArray(route?.polyline) ? route.polyline : [];
      if (Array.isArray(points) && points.length > 0) {
        applyRoutePoints(points);
        if (deps.tripStatusRef.current === 1) {
          // 在 tripStatus === 1 时,我们需要包含所有路线的点,以便自适应视野
        if (deps.allowSelectableRoutes) {
          const allRoutesPoints: any[] = [];
          allPointsArr.forEach(pts => {
             if (Array.isArray(pts)) {
@@ -547,8 +549,8 @@
  const buildPolylineLines = (displayRoutePoints: any[]) => {
    const lines: any[] = [];
    
    // 如果有多条路线且在未发单状态(tripStatus === 1),渲染所有路线
    if (deps.tripStatusRef.current === 1 && allRoutePoints.length > 1) {
    // 如果有多条路线且在未发单状态或修改目的地状态(allowSelectableRoutes === true),渲染所有路线
    if (deps.allowSelectableRoutes && allRoutePoints.length > 1) {
      allRoutePoints.forEach((points, index) => {
        if (!points || points.length === 0) return;
        
@@ -633,6 +635,7 @@
    walkingTime,
    setWalkingTime,
    routeOptions,
    setRouteOptions,
    selectedRouteIndex,
    handleSelectRoute,
    handlePolylineTap,
src/robotaxi/pages/begin-route/hooks/use-trip-runtime.ts
@@ -231,7 +231,7 @@
  useEffect(() => {
    const wasHold = [4, 6].includes(deps.lastTripStatusRef.current);
    const isHold = [4, 6].includes(deps.tripStatus);
    if (deps.tripStatus != 4) Taro.removeStorageSync('pickupEndTime');
    if (deps.tripStatus != 5) Taro.removeStorageSync('pickupEndTime');
    if (deps.tripStatus != 7) Taro.removeStorageSync('finishEndTime');
    if (!wasHold && isHold) {
      deps.orderRouteInitializedRef.current = false;
src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx
@@ -3,6 +3,7 @@
import ReleaseRoute from '../components/release-route';
import ReleaseBeBeing from '../components/release-be-being';
import CallingCar from '../components/calling-car';
import ModifyDestinationReview from '../components/modify-destination-review';
import { formatDurationDisplay } from '../../../../utils/Robotaxi-public-method';
type Deps = {
@@ -30,12 +31,16 @@
  arriveCalloutVisible: boolean;
  /** 到达气泡样式 */
  arriveCalloutStyle: any;
  /** 到达态气泡渲染就绪(角度与缩放均就绪) */
  arrivedTooltipReady: boolean;
  /** 路线距离 */
  routeDistance: { value: number; unit: 'km' | 'm' } | null;
  /** 路线时长 */
  routeDuration: number | null;
  /** 自定义气泡图标 */
  customCalloutIconSrc: string;
  /** 是否使用下单前端点气泡样式 */
  showTrip1StyleEndpoint: boolean;
  /** 上车点 */
  startSite: any;
  /** 下车点 */
@@ -98,6 +103,18 @@
  selectedRouteIndex: number;
  /** 切换路线回调 */
  handleSelectRoute: (index: number) => void;
  /** 地图点击 */
  handleMapTap?: (e: any) => void;
  /** 修改目的地确认态 */
  isModifyDestinationReviewVisible?: boolean;
  /** 修改前目的地 */
  previousEndSite?: any;
  /** 修改前费用 */
  previousFareEstimate?: any;
  /** 修改后目的地 */
  previewEndSite?: any;
  /** 确认修改目的地 */
  handleConfirmModifyDestination?: (selectedRoute?: any) => void;
};
/**
@@ -145,7 +162,7 @@
            <CoverView className={`custom-callout-content ${deps.isAndroid && 'txt-center '}`} style={{ zIndex: 1 }}>
              <CoverView style={{ lineHeight: '22px', display: 'flex', alignItems: 'center' }}>{item.name}</CoverView>
              {deps.isAndroid && <CoverView style={{ width: '2px' }}></CoverView>}
              {deps.tripStatus == 1 && <CoverImage className='custom-callout-icon' src={deps.customCalloutIconSrc} />}
              {(deps.tripStatus == 1 || deps.showTrip1StyleEndpoint) && <CoverImage className='custom-callout-icon' src={deps.customCalloutIconSrc} />}
            </CoverView>
          )}
          {(item.name === '车辆位置' && [4, 6].includes(deps.tripStatus) && !deps.isAndroid) && (
@@ -158,7 +175,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>
@@ -201,7 +218,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">
            已到达
@@ -233,6 +250,22 @@
        />
      );
    }
    if (deps.isModifyDestinationReviewVisible) {
      return (
        <ModifyDestinationReview
          previousEndSite={deps.previousEndSite}
          nextEndSite={deps.previewEndSite}
          previousFareEstimate={deps.previousFareEstimate}
          fareEstimate={deps.fareEstimate}
          routeOptions={deps.routeOptions}
          selectedRouteIndex={deps.selectedRouteIndex}
          isRouteSelectExpanded={deps.isRouteSelectExpanded}
          handleSelectRoute={deps.handleSelectRoute}
          onEditDestination={deps.handleModifyDestination}
          onConfirm={() => deps.handleConfirmModifyDestination?.(deps.routeOptions?.[deps.selectedRouteIndex || 0])}
        />
      );
    }
    if (deps.tripStatus == 2) {
      return (
        <ReleaseBeBeing
src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts
@@ -202,12 +202,10 @@
    const currentTripStatus = Number(deps.tripStatusRef.current);
    if (currentTripStatus === 7) {
        const lastPoint = deps.lastVehiclePointRef.current;
        const lastUpdateAt = Number(deps.lastVehicleUpdateAtRef.current || 0);
        const now = Date.now();
        const latDiff = Math.abs(Number(lastPoint?.latitude || 0) - targetPoint.latitude);
        const lngDiff = Math.abs(Number(lastPoint?.longitude || 0) - targetPoint.longitude);
        const almostSamePoint = latDiff < 0.000003 && lngDiff < 0.000003;
        if (almostSamePoint || (now - lastUpdateAt < 1200)) {
        if (almostSamePoint) {
            return;
        }
    }
@@ -268,6 +266,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
@@ -30,6 +30,7 @@
import useOrderActions from './hooks/use-order-actions';
// 视图委托渲染编排
import useViewDelegates from './hooks/use-view-delegates';
import useModifyDestinationFlow from './hooks/use-modify-destination-flow';
import backIcon from '../../../robotaxi/assets/back-icon.png'
import personalCenter from '../../../robotaxi/assets/personal-center.png'
@@ -116,14 +117,13 @@
    4: { showPickup: true, showDropoff: false, showCar: true, showRoute: true, showWalking: true, viewport: 'route', translateCar: true },
    5: { showPickup: true, showDropoff: false, showCar: true, showRoute: false, showWalking: true, viewport: 'car_user', translateCar: true },
    6: { showPickup: false, showDropoff: true, showCar: true, showRoute: true, showWalking: false, viewport: 'route', translateCar: true },
    7: { showPickup: false, showDropoff: true, showCar: true, showRoute: false, showWalking: false, viewport: 'dropoff', translateCar: false },
    7: { showPickup: false, showDropoff: true, showCar: true, showRoute: false, showWalking: false, viewport: 'car_user', translateCar: true },
    8: { showPickup: true, showDropoff: true, showCar: false, showRoute: true, showWalking: false, viewport: 'order_detail', translateCar: false },
    9: { showPickup: true, showDropoff: true, showCar: false, showRoute: true, showWalking: false, viewport: 'order_detail', translateCar: false },
    10: { showPickup: true, showDropoff: false, showCar: false, showRoute: false, showWalking: false, viewport: 'pickup', translateCar: false },
  };
  const statusRule = useMemo(() => STATUS_RULES[tripStatus] || STATUS_RULES[1], [tripStatus]); // 当前行程状态对应的显示规则
  const shouldShowRoute = statusRule.showRoute; // 当前状态是否显示路线
  const orderDetailRouteRequestIdRef = useRef(0); // 订单详情路线请求ID
  const isCheckedEnter = useRef(false); // 是否已检查页面进入状态
  const isAndroid = useMemo(() => {
    try {
      const info = Taro.getSystemInfoSync();
@@ -134,6 +134,68 @@
  }, []);
  const shouldShowWaitingRadar = isAndroid && [2, 3].includes(tripStatus); // 是否显示等待雷达动画(Android且状态2/3)
  const vehicleMarkerId = !isAndroid && tripStatus === 6 ? 30 : 3; // 车辆标记ID,iOS状态6使用不同ID
  const {
    openService,
    toCoupon,
    toOrder,
    toBack,
    handleCalloutTap,
    toSelectSite,
    guardPrivacyThenNavigate
  } = usePageActions({
    tripStatus,
    startSite,
    getCheckedEnter: () => isCheckedEnter.current,
    setCheckedEnter: (checked) => {
      isCheckedEnter.current = checked;
    }
  }); // 页面动作编排
  const {
    effectiveEndSite,
    previewEndSite,
    previousEndSite,
    previousFareEstimate,
    previousRoutePoints,
    isModifyDestinationReviewVisible,
    hasLocalDestinationOverride,
    clearLocalDestinationOverride,
    handleStartModifyDestination,
    handleConfirmModifyDestination
  } = useModifyDestinationFlow({
    tripStatus,
    orderId,
    endSite,
    setEndSite,
    toSelectSite,
    onModifySuccess: () => {
      // 成功修改后触发路线重新获取
      // 下一次渲染时 isModifyDestinationReviewVisible 变为 false
      // 我们需要强制让选路状态退出,同时获取后端的新行程路线
      setIsRouteSelectExpanded(false);
      setRouteOptions([]);
      setRoutePoints([]);
      setFareEstimate({});
      routeRequestOrderRoute(orderId, tripStatus);
    }
  });
  const statusRule = useMemo(() => {
    const baseRule = STATUS_RULES[tripStatus] || STATUS_RULES[1];
    if (isModifyDestinationReviewVisible || hasLocalDestinationOverride) {
      return {
        ...baseRule,
        showPickup: true,
        showDropoff: true,
        showCar: false,
        showRoute: true,
        showWalking: false,
        viewport: 'route'
      };
    }
    return baseRule;
  }, [tripStatus, isModifyDestinationReviewVisible, hasLocalDestinationOverride]);
  const shouldShowRoute = statusRule.showRoute; // 当前状态是否显示路线
  const allowSelectableRoutes = tripStatus === 1 || isModifyDestinationReviewVisible || hasLocalDestinationOverride;
  const showTrip1StyleEndpoint = isModifyDestinationReviewVisible || hasLocalDestinationOverride;
  const vehicleState = useVehicleTracking({
    statusRule,
    vehicleMarkerId
@@ -150,46 +212,15 @@
    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,
    tripStatusRef,
    statusRule,
    shouldShowRoute,
    allowSelectableRoutes,
    startSite,
    endSite,
    endSite: effectiveEndSite,
    setStartSite,
    setEndSite,
    updateVehicleLocationThrottled: vehicleState.updateVehicleLocationThrottled,
@@ -205,11 +236,13 @@
    walkingTime,
    setWalkingTime,
    routeOptions,
    setRouteOptions,
    selectedRouteIndex,
    handleSelectRoute,
    handlePolylineTap,
    handleMapTap,
    fareEstimate,
    setFareEstimate,
    routeAndFareError,
    routeDistance,
    setRouteDistance,
@@ -228,8 +261,25 @@
    requestOrderRoute: routeRequestOrderRoute,
    updateWalkingRouteForStart: routeUpdateWalkingRouteForStart
  } = routeState; // 路线状态解构
  const displayPolyline = useMemo(() => {
    if (!(isModifyDestinationReviewVisible || hasLocalDestinationOverride) || !Array.isArray(previousRoutePoints) || previousRoutePoints.length === 0) {
      return polyline;
    }
    return [
      {
        id: 900001,
        points: previousRoutePoints,
        color: '#D7DDEF',
        width: 8,
        dottedLine: false,
        arrowLine: true,
        clickable: false
      },
      ...polyline
    ];
  }, [polyline, previousRoutePoints, isModifyDestinationReviewVisible, hasLocalDestinationOverride]);
  const routeSummaryMarker = useMemo(() => {
    if (tripStatus !== 1 || !routeDistance || routeDuration === null || !Array.isArray(routePoints) || routePoints.length === 0) {
    if (!allowSelectableRoutes || !routeDistance || routeDuration === null || !Array.isArray(routePoints) || routePoints.length === 0) {
      return null;
    }
    const midpoint = routePoints[Math.max(0, Math.floor(routePoints.length / 2))];
@@ -248,18 +298,14 @@
      isRouteSummaryBubble: true,
      customCallout: { anchorY: 0, anchorX: 0, display: 'ALWAYS' }
    };
  }, [tripStatus, routeDistance, routeDuration, routePoints]);
  const markersWithRouteSummary = useMemo(() => {
    if (!routeSummaryMarker) return markers;
    return [...markers, routeSummaryMarker];
  }, [markers, routeSummaryMarker]);
  }, [allowSelectableRoutes, routeDistance, routeDuration, routePoints]);
  const viewportState = useMapViewport({
    tripStatus,
    statusRule,
    routePoints,
    walkingPoints,
    startSite,
    endSite,
    endSite: effectiveEndSite,
    vehicleLocation,
    longitude,
    latitude,
@@ -278,7 +324,48 @@
    toOrigin,
    getLocationInfo
  } = viewportState; // 视口状态解构
  const isCheckedEnter = useRef(false); // 是否已检查页面进入状态
  const arrivedMarkerReady = tripStatus !== 7 || typeof vehicleDirection === 'number';
  const arrivedTooltipReady = tripStatus !== 7 || (arrivedMarkerReady && mapScale >= 20);
  const markerState = useMapMarkers({
    tripStatus,
    statusRule,
    isAndroid,
    showTrip1StyleEndpoint,
    startSite,
    endSite: effectiveEndSite,
    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: effectiveEndSite,
    vehicleLocation,
    longitude,
    latitude
  }); // Android 覆盖物状态编排
  const markersWithRouteSummary = useMemo(() => {
    if (!routeSummaryMarker) return markers;
    return [...markers, routeSummaryMarker];
  }, [markers, routeSummaryMarker]);
  const { checkDistanceAndCityReady } = useOrderDistanceConfig({
    endSite,
    setTipType,
@@ -306,21 +393,6 @@
    getCurrentLongitude: () => longitude,
    getCurrentLatitude: () => latitude
  }); // 页面恢复与初始化编排
  const {
    openService,
    toCoupon,
    toOrder,
    toBack,
    handleCalloutTap,
    toSelectSite
  } = usePageActions({
    tripStatus,
    startSite,
    getCheckedEnter: () => isCheckedEnter.current,
    setCheckedEnter: (checked) => {
      isCheckedEnter.current = checked;
    }
  }); // 页面动作编排
  const router = Taro.useRouter();
  const tripRuntime = useTripRuntime({
    waitingTimerStartKey: WAITING_TIMER_START_KEY,
@@ -337,7 +409,7 @@
    currentFullRouteRef,
    orderDetailRouteRequestIdRef,
    routePoints,
    endSite,
    endSite: effectiveEndSite,
    longitude,
    latitude,
    elapsedSeconds,
@@ -420,7 +492,7 @@
  const orderActions = useOrderActions({
    tripStatus,
    orderId,
    endSite,
    endSite: effectiveEndSite,
    startSite,
    fareEstimate,
    routeOptions,
@@ -453,6 +525,57 @@
    handleFinishCountdownTimeout,
    handlePay
  } = orderActions;
  const modifyPreviewRouteKeyRef = useRef('');
  useEffect(() => {
    const shouldPreviewModifiedRoute = isModifyDestinationReviewVisible || hasLocalDestinationOverride;
    const targetEndSite = shouldPreviewModifiedRoute ? effectiveEndSite : null;
    const destLat = Number(targetEndSite?.latitude);
    const destLng = Number(targetEndSite?.longitude);
    if (![4, 5, 6].includes(tripStatus) || !Number.isFinite(destLat) || !Number.isFinite(destLng)) {
      modifyPreviewRouteKeyRef.current = '';
      return;
    }
    const cachedStartSite = Taro.getStorageSync('startSite') || {};
    let originLat = Number(startSite?.latitude ?? cachedStartSite?.latitude);
    let originLng = Number(startSite?.longitude ?? cachedStartSite?.longitude);
    if ([4, 5].includes(tripStatus)) {
      if (!Number.isFinite(originLat) || !Number.isFinite(originLng)) {
        return;
      }
    } else if (tripStatus === 6 && Number.isFinite(Number(vehicleLocation?.latitude)) && Number.isFinite(Number(vehicleLocation?.longitude))) {
      originLat = Number(vehicleLocation?.latitude);
      originLng = Number(vehicleLocation?.longitude);
    } else {
      return;
    }
    if (!Number.isFinite(originLat) || !Number.isFinite(originLng)) {
      return;
    }
    const nextKey = `${tripStatus}|${Number(targetEndSite?.id || 0)}|${originLng},${originLat}-${destLng},${destLat}`;
    if (nextKey === modifyPreviewRouteKeyRef.current) {
      return;
    }
    modifyPreviewRouteKeyRef.current = nextKey;
    routeRequestRouteAndFare(originLat, originLng, destLat, destLng, true);
  }, [
    tripStatus,
    isModifyDestinationReviewVisible,
    hasLocalDestinationOverride,
    effectiveEndSite?.latitude,
    effectiveEndSite?.longitude,
    startSite?.latitude,
    startSite?.longitude,
    vehicleLocation?.latitude,
    vehicleLocation?.longitude,
    latitude,
    longitude
  ]);
  const shareOrderId = orderId || Taro.getStorageSync('orderId') || '';
  const onShareAppMessage = () => ({
    title: '快来查看我的 Robotaxi 行程',
@@ -504,7 +627,7 @@
  }, [isRouteSelectExpanded]);
  const handleModifyDestination = () => {
    isCheckedEnter.current = true;
    toSelectSite('end');
    handleStartModifyDestination(fareEstimate, routePoints);
  };
  const handleShareTrip = () => {
    isCheckedEnter.current = true;
@@ -524,11 +647,13 @@
    carCalloutStyle,
    arriveCalloutVisible,
    arriveCalloutStyle,
    arrivedTooltipReady,
    routeDistance,
    routeDuration,
    customCalloutIconSrc,
    showTrip1StyleEndpoint,
    startSite,
    endSite,
    endSite: effectiveEndSite,
    callImmediately,
    fareEstimate,
    handleOpenPassengerNotice,
@@ -556,10 +681,16 @@
    routeOptions,
    selectedRouteIndex,
    handleSelectRoute,
    handleMapTap
    handleMapTap,
    isModifyDestinationReviewVisible,
    previousEndSite,
    previousFareEstimate,
    previewEndSite,
    handleConfirmModifyDestination
  }); // 视图渲染委托
  const mapLongitude = longitude ?? 116.397128;
  const mapLatitude = latitude ?? 39.916527;
  const canToggleRouteSelect = allowSelectableRoutes && routeOptions.length > 1;
  const shouldShowOrigin = ![7, 8, 9].includes(tripStatus);
  const handleServiceEntry = () => {
@@ -594,7 +725,7 @@
      mapLatitude={mapLatitude}
      mapScale={mapScale}
      markers={markersWithRouteSummary}
      polyline={polyline}
      polyline={displayPolyline}
      showLocation={!!longitude && showLocationFlag}
      handleCalloutTap={handleCalloutTap}
      handleRegionChange={handleRegionChange}
@@ -618,6 +749,7 @@
      packUpIcon={packUpIcon}
      onSelectRouteClick={() => setIsRouteSelectExpanded(!isRouteSelectExpanded)}
      isRouteSelectExpanded={isRouteSelectExpanded}
      canToggleRouteSelect={canToggleRouteSelect}
      walkIcon={walkIcon}
      originIcon={originIcon}
      renderTripCard={renderTripCard}
@@ -626,7 +758,7 @@
      showCancelPopup={showCancelPopup}
      cancelType={cancelType}
      startSite={startSite}
      endSite={endSite}
      endSite={effectiveEndSite}
      fareEstimate={fareEstimate}
      tripStatus={tripStatus}
      routeDuration={routeDuration}
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('');
  };
  // 重新拉取初始化数据(历史记录和附近站点)
@@ -150,6 +186,8 @@
    historySite,
    nonOperationalSite,
    nearbySiteList,
    isSearching,
    lastCompletedKeyword,
    searchSite,
    debouncedSearch,
    clearSearchResults,
src/robotaxi/pages/site-search/index.tsx
@@ -20,6 +20,10 @@
  splitHighlightParts,
  filterNearbyListByCurrentSite
} from './utils/site-search';
import {
  MODIFY_DESTINATION_CANDIDATE_KEY,
  MODIFY_DESTINATION_SESSION_KEY
} from '../begin-route/constants/modify-destination';
@@ -42,6 +46,8 @@
    historySite,
    nonOperationalSite,
    nearbySiteList,
    isSearching,
    lastCompletedKeyword,
    searchSite,
    debouncedSearch,
    clearSearchResults,
@@ -172,7 +178,12 @@
          }
        }
      } catch { }
      Taro.setStorageSync('endSite', item)
      const modifySession = Taro.getStorageSync(MODIFY_DESTINATION_SESSION_KEY);
      if (modifySession?.active) {
        Taro.setStorageSync(MODIFY_DESTINATION_CANDIDATE_KEY, item);
      } else {
        Taro.setStorageSync('endSite', item);
      }
      Taro.reLaunch({
        url: Taro.getStorageSync('orderId') ? '/robotaxi/pages/begin-route/index?skipPageFlag=1' : '/robotaxi/pages/begin-route/index',
      })
@@ -242,6 +253,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());
  // 使用数据截断展示,不再通过高度裁剪内容
@@ -326,7 +338,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'>
src/robotaxi/utils/car-control/RtAuthBluetoothManager.js
New file
@@ -0,0 +1,432 @@
// RtAuthBluetoothManager.js
// 精简版蓝牙管理器,只支持 RtAuth 鉴权模式(Robotaxi 场景)
import Taro from '@tarojs/taro';
import { ab2hex, madAddressToLocalName, hexStringToArray } from '../../../utils/canControlTools';
// 引入日志和WASM模块
var log = require('./log.js');
const bleWasmModule = require('./bleMethod');
export default class RtAuthBluetoothManager {
  constructor(options) {
    // 回调函数
    this.onStatusChange = options.onStatusChange || (() => {}); // 连接状态回调
    this.onError = options.onError || (() => {}); // 错误回调
    this.onRtAuthResult = options.onRtAuthResult || (() => {}); // RtAuth 鉴权结果回调
    this.onRtNotification = options.onRtNotification || (() => {}); // RtNotification 通知回调
    // 常量
    this.serviceId = '0000ED28-ABAB-406D-832D-7AD799B479F7';
    this.writeId = '0000ED28-ABAB-9C8C-2801-B1A015E46D6C';
    this.MAX_RETRY_TIMES = 1;
    // 内部状态
    this.state = {
      deviceId: '',
      macAddress: '',
      certificate: '',   // 鉴权凭证
      deviceModel: '',   // 设备型号
      isIOS: false,
      connected: false,
      isScaning: false,
      connectStatus: 0, // 0:断开, 1:连接中, 2:已连接, 3:已鉴权
      tryTimes: 0,
      // 特征值
      readServiceId: '',
      writeServiceId: '',
      readChs: '',
      writeChs: '',
      indecateChs: '',
      indecateServiceId: ''
    };
    this.initSystemInfo();
  }
  initSystemInfo() {
    Taro.getSystemInfo({
      success: res => {
        this.state.isIOS = res.system.toLowerCase().indexOf('ios') >= 0;
      }
    });
  }
  // 更新车辆配置(macAddress 必填,certificate 和 deviceModel 可选,用于自动鉴权)
  updateCarConfig(config) {
    this.state.macAddress = config.macAddress;
    this.state.certificate = config.certificate || '';
    this.state.deviceModel = config.deviceModel || '';
    this.resetState();
  }
  resetState() {
    this.state.connectStatus = 0;
    this.state.connected = false;
    this.state.tryTimes = 0;
    this.notifyStatus(0);
  }
  notifyStatus(status) {
    this.state.connectStatus = status;
    this.onStatusChange(status);
  }
  // --- 蓝牙连接流程 ---
  openBluetoothAdapter() {
    if (this.state.connectStatus === 2) return;
    log.info('RtAuth: 准备初始化蓝牙...');
    Taro.getLocation({
      type: 'wgs84',
      success: () => this._realOpenAdapter(),
      fail: () => this._realOpenAdapter()
    });
  }
  _realOpenAdapter() {
    wx.openBluetoothAdapter({
      success: () => {
        log.info('RtAuth: 打开蓝牙适配器成功');
        this.findAndConnect();
      },
      fail: (res) => {
        log.info('RtAuth: 打开蓝牙适配器失败', res);
        if (res.errMsg.includes('already opened')) {
          wx.closeBluetoothAdapter({
            success: () => this._realOpenAdapter()
          });
        } else {
          this.onError({ type: 'ADAPTER_ERR', msg: '请打开蓝牙和定位功能' });
        }
      }
    });
  }
  findAndConnect() {
    const { macAddress, isIOS } = this.state;
    let key = madAddressToLocalName(macAddress);
    if (isIOS) key = key + 'IOS';
    const cacheDeviceId = Taro.getStorageSync(key);
    if (cacheDeviceId) {
      log.info('RtAuth: 使用缓存DeviceId连接', cacheDeviceId);
      this.state.deviceId = cacheDeviceId;
      this.connectDevice();
    } else {
      this.startScan();
    }
  }
  startScan() {
    if (this.state.isScaning) return;
    this.state.isScaning = true;
    wx.startBluetoothDevicesDiscovery({
      allowDuplicatesKey: true,
      success: () => this.onBluetoothDeviceFound()
    });
  }
  onBluetoothDeviceFound() {
    const { macAddress, isIOS } = this.state;
    const targetLocalName = madAddressToLocalName(macAddress);
    wx.onBluetoothDeviceFound((res) => {
      res.devices.forEach(device => {
        if (!device.name && !device.localName) return;
        if (device.localName === targetLocalName) {
          log.info('RtAuth: 扫描到目标设备', device.deviceId);
          const keyS = device.localName + (isIOS ? 'IOS' : '');
          Taro.setStorage({ key: keyS, data: device.deviceId });
          this.state.deviceId = device.deviceId;
          this.stopScan();
          this.connectDevice();
        }
      });
    });
  }
  stopScan() {
    wx.stopBluetoothDevicesDiscovery();
    this.state.isScaning = false;
  }
  connectDevice() {
    if (this.state.connectStatus === 2) return;
    this.notifyStatus(1);
    log.info('RtAuth: 开始连接设备', this.state.deviceId);
    wx.createBLEConnection({
      deviceId: this.state.deviceId,
      success: () => {
        log.info('RtAuth: 连接设备成功');
        this.state.tryTimes = 0;
        this.state.connected = true;
        if (this.state.isScaning) this.stopScan();
        this.getServices();
      },
      fail: (err) => {
        log.info('RtAuth: 连接失败', err);
        if (err.errno === 1509007 || err.errMsg.includes('already connected')) {
          this.state.tryTimes = 0;
          this.state.connected = true;
          if (this.state.isScaning) this.stopScan();
          this.getServices();
          return;
        }
        if (this.state.tryTimes < this.MAX_RETRY_TIMES) {
          this.state.tryTimes++;
          wx.closeBLEConnection({ deviceId: this.state.deviceId });
          setTimeout(() => this.openBluetoothAdapter(), 1500);
        } else {
          this.notifyStatus(0);
          this.closeConnection();
          this.onError({ type: 'CONNECT_ERR', msg: '蓝牙连接失败,请靠近车辆重试' });
        }
      }
    });
  }
  getServices() {
    wx.getBLEDeviceServices({
      deviceId: this.state.deviceId,
      success: (res) => {
        const targetService = res.services.find(s =>
          s.uuid === this.serviceId || s.uuid.toUpperCase() === this.serviceId
        );
        if (targetService) {
          this.getCharacteristics(targetService.uuid);
        }
      }
    });
  }
  getCharacteristics(serviceId) {
    wx.getBLEDeviceCharacteristics({
      deviceId: this.state.deviceId,
      serviceId,
      success: (res) => {
        res.characteristics.forEach(item => {
          if (item.properties.read) {
            this.state.readChs = item.uuid;
            this.state.readServiceId = serviceId;
          }
          if (item.properties.write && item.uuid === this.writeId) {
            this.state.writeChs = item.uuid;
            this.state.writeServiceId = serviceId;
          }
          if (item.properties.indicate) {
            this.state.indecateChs = item.uuid;
            this.state.indecateServiceId = serviceId;
            this.setIndicateListener();
          }
        });
      }
    });
  }
  setIndicateListener() {
    wx.notifyBLECharacteristicValueChange({
      deviceId: this.state.deviceId,
      serviceId: this.state.indecateServiceId,
      characteristicId: this.state.indecateChs,
      state: true,
      success: () => {
        log.info('RtAuth: 蓝牙连接就绪');
        this.notifyStatus(2);
        wx.onBLECharacteristicValueChange((characteristic) => {
          this.handleReceivedData(characteristic.value);
        });
        // 如果已配置 certificate 和 deviceModel,自动发送鉴权
        if (this.state.certificate && this.state.deviceModel) {
          log.info('RtAuth: 自动发送鉴权');
          this.sendRtAuthData(this.state.certificate, this.state.deviceModel);
        }
      }
    });
    this.listenConnectionState();
  }
  listenConnectionState() {
    wx.onBLEConnectionStateChange((res) => {
      if (!res.connected && this.state.connected) {
        log.info('RtAuth: 蓝牙异常断开');
        this.closeConnection();
      }
    });
  }
  closeConnection() {
    log.info('RtAuth: 断开连接');
    this.stopScan();
    if (this.state.connected) {
      wx.closeBLEConnection({ deviceId: this.state.deviceId });
    }
    this.resetState();
  }
  // --- 数据处理 ---
  handleReceivedData(value) {
    const dataHex = ab2hex(value);
    log.info('RtAuth: 收到数据', dataHex);
    // RtAuth 响应 (MsgId = 0x0B)
    if (dataHex.startsWith('0b') || dataHex.startsWith('0B')) {
      this.handleRtAuthResponse(dataHex);
      return;
    }
    // RtNotification (MsgId = 0x0C)
    if (dataHex.startsWith('0c') || dataHex.startsWith('0C')) {
      this.handleRtNotification(dataHex);
      return;
    }
    log.info('RtAuth: 忽略非 0x0B/0x0C 数据');
  }
  handleRtAuthResponse(dataHex) {
    log.info('RtAuth: 解析 RtAuth 响应');
    try {
      const wasmData = bleWasmModule.allocateUTF8(dataHex);
      const wasmResult = bleWasmModule._parseRtAuthResponse(wasmData, dataHex.length / 2);
      const resultStr = bleWasmModule.UTF8ToString(wasmResult);
      bleWasmModule._free(wasmData);
      log.info('RtAuth: 解析结果', resultStr);
      const result = JSON.parse(resultStr);
      if (result.error) {
        this.onRtAuthResult({ success: false, error: result.error });
      } else if (result.rtAuthResult) {
        const isSuccess = result.rtAuthResult.status === 0;
        // 鉴权成功,更新状态为 3(已鉴权)
        if (isSuccess) {
          this.notifyStatus(3);
        }
        this.onRtAuthResult({
          success: isSuccess,
          data: result.rtAuthResult
        });
      }
    } catch (e) {
      log.info('RtAuth: 解析异常', e);
      this.onRtAuthResult({ success: false, error: e.message || e });
    }
  }
  handleRtNotification(dataHex) {
    log.info('RtAuth: 解析 RtNotification');
    try {
      const wasmData = bleWasmModule.allocateUTF8(dataHex);
      const wasmResult = bleWasmModule._parseRtNotification(wasmData, dataHex.length / 2);
      const resultStr = bleWasmModule.UTF8ToString(wasmResult);
      bleWasmModule._free(wasmData);
      log.info('RtAuth: 解析结果', resultStr);
      const result = JSON.parse(resultStr);
      if (result.error) {
        this.onRtNotification({ type: 'error', error: result.error });
      } else if (result.rtNotification) {
        this.onRtNotification({
          type: 'pollingUnlock',
          success: result.rtNotification.result === 0,
          data: result.rtNotification
        });
      }
    } catch (e) {
      log.info('RtAuth: 解析异常', e);
      this.onRtNotification({ type: 'error', error: e.message || e });
    }
  }
  // --- 发送数据 ---
  writeBLEData(data) {
    const buffer = Uint8Array.from(data).buffer;
    wx.writeBLECharacteristicValue({
      deviceId: this.state.deviceId,
      serviceId: this.state.writeServiceId,
      characteristicId: this.state.writeChs,
      value: buffer,
      success: () => log.info('RtAuth: 发送成功'),
      fail: (err) => log.info('RtAuth: 发送失败', err)
    });
  }
  // 发送 RtAuth 鉴权数据
  sendRtAuthData(certificate, deviceModel) {
    log.info('RtAuth: 发送鉴权数据');
    if (this.state.connectStatus !== 2) {
      this.onError({ type: 'RT_AUTH_ERR', msg: '蓝牙未连接' });
      return false;
    }
    if (!certificate || !deviceModel) {
      this.onError({ type: 'RT_AUTH_ERR', msg: '缺少 certificate 或 deviceModel' });
      return false;
    }
    try {
      // certificate: hex 字符串,长度除以 2 得到字节数
      // deviceModel: 普通字符串,使用 lengthBytesUTF8 计算 UTF-8 编码后的字节长度
      const deviceModelLength = bleWasmModule.lengthBytesUTF8(deviceModel);
      const wasmCertificate = bleWasmModule.allocateUTF8(certificate);
      const wasmDeviceModel = bleWasmModule.allocateUTF8(deviceModel);
      const wasmResult = bleWasmModule._generateRtAuthData(
        wasmCertificate, certificate.length / 2,  // certificate 是 hex,字节数 = 长度/2
        wasmDeviceModel, deviceModelLength        // deviceModel 使用 UTF-8 字节长度
      );
      const rtAuthHex = bleWasmModule.UTF8ToString(wasmResult);
      bleWasmModule._free(wasmCertificate);
      bleWasmModule._free(wasmDeviceModel);
      log.info('RtAuth: 生成数据', rtAuthHex);
      this.writeBLEData(hexStringToArray(rtAuthHex));
      return true;
    } catch (e) {
      log.info('RtAuth: 发送失败', e);
      this.onError({ type: 'RT_AUTH_ERR', msg: e.message || e });
      return false;
    }
  }
  // 重试连接
  retryConnection() {
    log.info('RtAuth: 重连');
    this.closeConnection();
    this.state.tryTimes = 0;
    setTimeout(() => {
      if (!this.state.macAddress) {
        this.onError({ type: 'CONFIG_ERR', msg: '缺少 macAddress' });
        return;
      }
      this.openBluetoothAdapter();
    }, 500);
  }
}
src/robotaxi/utils/car-control/bleMethod.js
New file
Diff too large
src/robotaxi/utils/car-control/bleMethod.wasm.br
Binary files differ