13404089107
2026-04-17 38914f0aceeca1fa3d8fe1b81bf5a32982651e8f
feat(begin-route): 增加行程中修改目的地功能

- 新增修改目的地API接口orderChange
- 新增修改目的地流程管理hook useModifyDestinationFlow
- 新增目的地修改确认组件ModifyDestinationReview
- 新增路线选择面板组件RouteSelectionPanel并抽取复用
- 在站点搜索页支持修改目的地候选点存储逻辑
- 扩展地图标记和路线管理逻辑以支持修改目的地状态
- 在行程状态4-9时显示修改目的地入口
6个文件已添加
8个文件已修改
1260 ■■■■■ 已修改文件
src/robotaxi/api/begin-route.ts 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/begin-route-view.tsx 4 ●●●●● 补丁 | 查看 | 原始文档 | 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/modify-destination-review.tsx 178 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/release-route.tsx 151 ●●●●● 补丁 | 查看 | 原始文档 | 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-map-markers.ts 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-modify-destination-flow.ts 209 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-route-manager.ts 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.tsx 187 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/site-search/index.tsx 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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/begin-route-view.tsx
@@ -39,6 +39,7 @@
  packUpIcon: string;
  onSelectRouteClick: () => void;
  isRouteSelectExpanded: boolean;
  canToggleRouteSelect: boolean;
  walkIcon: string;
  renderTripCard: () => JSX.Element | null;
  showCustomerService: boolean;
@@ -99,6 +100,7 @@
    packUpIcon,
    onSelectRouteClick,
    isRouteSelectExpanded,
    canToggleRouteSelect,
    walkIcon,
    renderTripCard,
    showCustomerService,
@@ -162,7 +164,7 @@
            <View className='origin-box' style={{ opacity: originBoxHidden ? 0 : originOpacity, transition: 'opacity 0.2s ease-out', pointerEvents: originBoxHidden ? 'none' : 'auto' }}>
              <Image onClick={toOrigin} className='origin-icon' src={originIcon} />
              {
                tripStatus == 1 &&
                canToggleRouteSelect &&
                <View>
                  {
                    isRouteSelectExpanded ?
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/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/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) {
@@ -213,41 +150,6 @@
        }
        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 (
        <View className="release-route-container">
@@ -274,45 +176,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 +249,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-map-markers.ts
@@ -5,6 +5,7 @@
  tripStatus: number;
  statusRule: any;
  isAndroid: boolean;
  showTrip1StyleEndpoint: boolean;
  startSite: any;
  endSite: any;
  vehicleLocation: { latitude: number; longitude: number } | null;
@@ -16,6 +17,7 @@
  tripStatus: 1,
  statusRule: { showPickup: true, showDropoff: true, showCar: false },
  isAndroid: false,
  showTrip1StyleEndpoint: false,
  startSite: null,
  endSite: null,
  vehicleLocation: null,
@@ -48,7 +50,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 +83,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,7 +136,7 @@
  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 carMarker = createCarMarker();
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;
        
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 = {
@@ -36,6 +37,8 @@
  routeDuration: number | null;
  /** 自定义气泡图标 */
  customCalloutIconSrc: string;
  /** 是否使用下单前端点气泡样式 */
  showTrip1StyleEndpoint: boolean;
  /** 上车点 */
  startSite: any;
  /** 下车点 */
@@ -98,6 +101,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 +160,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) && (
@@ -233,6 +248,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/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'
@@ -121,9 +122,8 @@
    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
@@ -154,8 +216,9 @@
    tripStatus,
    statusRule,
    isAndroid,
    showTrip1StyleEndpoint,
    startSite,
    endSite,
    endSite: effectiveEndSite,
    vehicleLocation,
    vehicleDirection,
    vehicleMarkerId
@@ -177,7 +240,7 @@
    isAndroid,
    tripStatus,
    startSite,
    endSite,
    endSite: effectiveEndSite,
    vehicleLocation,
    longitude,
    latitude
@@ -188,8 +251,9 @@
    tripStatusRef,
    statusRule,
    shouldShowRoute,
    allowSelectableRoutes,
    startSite,
    endSite,
    endSite: effectiveEndSite,
    setStartSite,
    setEndSite,
    updateVehicleLocationThrottled: vehicleState.updateVehicleLocationThrottled,
@@ -228,8 +292,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,7 +329,7 @@
      isRouteSummaryBubble: true,
      customCallout: { anchorY: 0, anchorX: 0, display: 'ALWAYS' }
    };
  }, [tripStatus, routeDistance, routeDuration, routePoints]);
  }, [allowSelectableRoutes, routeDistance, routeDuration, routePoints]);
  const markersWithRouteSummary = useMemo(() => {
    if (!routeSummaryMarker) return markers;
    return [...markers, routeSummaryMarker];
@@ -259,7 +340,7 @@
    routePoints,
    walkingPoints,
    startSite,
    endSite,
    endSite: effectiveEndSite,
    vehicleLocation,
    longitude,
    latitude,
@@ -278,7 +359,6 @@
    toOrigin,
    getLocationInfo
  } = viewportState; // 视口状态解构
  const isCheckedEnter = useRef(false); // 是否已检查页面进入状态
  const { checkDistanceAndCityReady } = useOrderDistanceConfig({
    endSite,
    setTipType,
@@ -306,22 +386,6 @@
    getCurrentLongitude: () => longitude,
    getCurrentLatitude: () => latitude
  }); // 页面恢复与初始化编排
  const {
    openService,
    toCoupon,
    toOrder,
    toBack,
    handleCalloutTap,
    toSelectSite,
    guardPrivacyThenNavigate
  } = usePageActions({
    tripStatus,
    startSite,
    getCheckedEnter: () => isCheckedEnter.current,
    setCheckedEnter: (checked) => {
      isCheckedEnter.current = checked;
    }
  }); // 页面动作编排
  const router = Taro.useRouter();
  const tripRuntime = useTripRuntime({
    waitingTimerStartKey: WAITING_TIMER_START_KEY,
@@ -338,7 +402,7 @@
    currentFullRouteRef,
    orderDetailRouteRequestIdRef,
    routePoints,
    endSite,
    endSite: effectiveEndSite,
    longitude,
    latitude,
    elapsedSeconds,
@@ -421,7 +485,7 @@
  const orderActions = useOrderActions({
    tripStatus,
    orderId,
    endSite,
    endSite: effectiveEndSite,
    startSite,
    fareEstimate,
    routeOptions,
@@ -454,6 +518,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
  ]);
  useDidShow(() => {
    handleDidShow(router.params?.skipPageFlag);
@@ -498,7 +613,7 @@
  }, [isRouteSelectExpanded]);
  const handleModifyDestination = () => {
    isCheckedEnter.current = true;
    toSelectSite('end');
    handleStartModifyDestination(fareEstimate, routePoints);
  };
  const handleShareTrip = () => {
    isCheckedEnter.current = true;
@@ -522,8 +637,9 @@
    routeDistance,
    routeDuration,
    customCalloutIconSrc,
    showTrip1StyleEndpoint,
    startSite,
    endSite,
    endSite: effectiveEndSite,
    callImmediately,
    fareEstimate,
    handleOpenPassengerNotice,
@@ -551,10 +667,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 = () => {
@@ -576,7 +698,7 @@
      mapLatitude={mapLatitude}
      mapScale={mapScale}
      markers={markersWithRouteSummary}
      polyline={polyline}
      polyline={displayPolyline}
      showLocation={!!longitude && showLocationFlag}
      handleCalloutTap={handleCalloutTap}
      handleRegionChange={handleRegionChange}
@@ -599,6 +721,7 @@
      packUpIcon={packUpIcon}
      onSelectRouteClick={() => setIsRouteSelectExpanded(!isRouteSelectExpanded)}
      isRouteSelectExpanded={isRouteSelectExpanded}
      canToggleRouteSelect={canToggleRouteSelect}
      walkIcon={walkIcon}
      originIcon={originIcon}
      renderTripCard={renderTripCard}
@@ -607,7 +730,7 @@
      showCancelPopup={showCancelPopup}
      cancelType={cancelType}
      startSite={startSite}
      endSite={endSite}
      endSite={effectiveEndSite}
      fareEstimate={fareEstimate}
      tripStatus={tripStatus}
      routeDuration={routeDuration}
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';
@@ -172,7 +176,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',
      })