lucl
2026-08-06 99596c04c5aa4bfa859ba0128443d3027b15ac36
Merge branch 'feature_v2.5.0' into feature_v2.4.0_ui_animation
1个文件已删除
6个文件已添加
53个文件已修改
1640 ■■■■■ 已修改文件
src/pages/robotaxi/home/components/home-map-panel.tsx 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/components/map-overlay-tips/index.tsx 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/components/privacy-popup/index.less 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/components/privacy-popup/index.tsx 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/index.less 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/api/begin-route.ts 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/api/fragrance.ts 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/api/genre.ts 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/api/seat-hv.ts 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/api/types/genre.ts 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/components/common-popup/index.less 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/components/common-popup/index.tsx 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/components/coupon-select-popup/index.less 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/components/customer-service/index.less 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/components/demo/index.tsx 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/components/tip-popup/index.less 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/bluetooth-operate-card.tsx 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/door-trunk-control.tsx 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/fare-detail-card.tsx 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/fragrance-control/index.tsx 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/music-control/index.tsx 287 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/seat-hv-control/index.tsx 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car-modules/temperature-control.tsx 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car.tsx 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/cancel-popup.tsx 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/announcements-popup.less 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/calling-car.less 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/cancel-popup.less 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/credit-pay-popup.less 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/evaluate-popup.less 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/fragrance-control.less 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/music-control.less 271 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/no-route-popup.less 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/over-route.less 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/passenger-notice.less 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/pickup-exception-popup.less 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/seat-hv-control.less 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/temperature-control.less 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/debug-order-simulator-panel.tsx 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/pickup-exception-popup.tsx 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/release-route.tsx 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/route-preference-popup/index.less 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-map-markers.ts 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-page-actions.ts 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-page-restore.ts 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.less 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.tsx 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/coupon/index.less 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/coupon/index.tsx 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/feedback/index.less 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/operating-area/index.tsx 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/order/detail/index.less 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/order/detail/index.tsx 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/select-map-site/hooks/use-select-map-site-walking-route.ts 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/share-trip/hooks/use-map-markers.ts 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/share-trip/hooks/use-view-delegates.tsx 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/share-trip/index.less 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/site-search/components/favorite-popup/index.less 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/utils/api-logger.ts 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/robotaxi/home/components/home-map-panel.tsx
@@ -159,7 +159,25 @@
        }}
      >
        <View className='origin-box-container'>
          <Image onClick={props.onToOrigin} className="origin-icon" src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/origin-icon-20260716.png' />
          <View className='tips-and-origin'>
            <Image onClick={props.onToOrigin} className="origin-icon" src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/origin-icon-20260716.png' />
            <MapOverlayTips
              isGuest={props.shouldShowGuestTip}
              isLocationAuthDenied={props.isLocationAuthDenied}
              isOperatingArea={props.isOperatingArea}
              isMaintain={props.isMaintain}
              maintainBounce={props.maintainBounce}
              hotfixEndText={props.hotfixEndText}
              anchorBottom={props.overlayAnchorBottom ?? undefined}
              opacity={props.originOpacity}
              isDragging={props.isDragging}
              onShowPrivacy={props.onShowPrivacy}
              onOpenSetting={props.onOpenSetting}
              onViewOperatingArea={props.onViewOperatingArea}
              shouldShowOrderStatusCard={props.shouldShowOrderStatusCard}
            />
          </View>
          {props.shouldShowOrderStatusCard && (
            <View  className="order-status-overlay">
              <OrderStatusCard
@@ -172,21 +190,7 @@
          )}
        </View>
      </View>
      <MapOverlayTips
        isGuest={props.shouldShowGuestTip}
        isLocationAuthDenied={props.isLocationAuthDenied}
        isOperatingArea={props.isOperatingArea}
        isMaintain={props.isMaintain}
        maintainBounce={props.maintainBounce}
        hotfixEndText={props.hotfixEndText}
        anchorBottom={props.overlayAnchorBottom ?? undefined}
        opacity={props.originOpacity}
        isDragging={props.isDragging}
        onShowPrivacy={props.onShowPrivacy}
        onOpenSetting={props.onOpenSetting}
        onViewOperatingArea={props.onViewOperatingArea}
        shouldShowOrderStatusCard={props.shouldShowOrderStatusCard}
      />
      <View className="map-entry">
        <View className="map-entry-box">
          <Image onClick={props.onToPersonalCenter} className="map-entry-icon" src='https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/personal-center-20260716.png' />
src/pages/robotaxi/home/components/map-overlay-tips/index.tsx
@@ -32,33 +32,23 @@
 * 地图顶部提示层。
 */
export default function MapOverlayTips(props: Props) {
  const anchorStyle = typeof props.anchorBottom === 'number'
    ? {
        top: 'auto',
        bottom: `${props.anchorBottom}px`,
        opacity: props.opacity !== undefined ? props.opacity : 1,
        visibility: props.opacity !== undefined && props.opacity <= 0 ? 'hidden' : 'visible' as any,
        pointerEvents: props.opacity !== undefined && props.opacity <= 0 ? 'none' : 'auto' as any,
        transition: props.isDragging ? 'none' : 'opacity 0.2s ease-out'
      }
    : undefined;
  return (
    <View className="content-box-top" style={anchorStyle}>
    <View className="content-box-top">
      <View className="position-box">
        {
          props.isGuest && <View className="visitor-stste ml-32">
          props.isGuest && <View className="visitor-stste">
            <View className="visitor-stste-tip">同意隐私协议体验更多约车体验</View>
            <View onClick={props.onShowPrivacy} className="visitor-stste-go">去同意</View>
          </View>
        }
        {
          props.isLocationAuthDenied && <View className="visitor-stste ml-32 mt-20">
          props.isLocationAuthDenied && <View className="visitor-stste">
            <View className="visitor-stste-tip">未开启位置权限,开启后即可发布行程</View>
            <View onClick={props.onOpenSetting} className="visitor-stste-go">去开启</View>
          </View>
        }
        {
          props.isMaintain && <View className={`is-maintain ml-32 ${props.maintainBounce ? 'maintain-bounce' : ''}`}>系统维护中,预计{props.hotfixEndText || '-:-'}恢复服务</View>
          props.isMaintain && <View className={`is-maintain ${props.maintainBounce ? 'maintain-bounce' : ''}`}>系统维护中,预计{props.hotfixEndText || '-:-'}恢复服务</View>
        }
        {
          !props.isOperatingArea && !props.shouldShowOrderStatusCard && <View onClick={props.onViewOperatingArea} className="is-operating-area mt-20">查看运营区域</View>
src/pages/robotaxi/home/components/privacy-popup/index.less
@@ -85,8 +85,8 @@
    }
    .check-all-box {
      display: flex;
      align-items: center;
      justify-content: center;
      align-items: flex-start;
      justify-content: flex-start;
      margin-top: 14px;
      .all-name{
        font-size: 28px;
@@ -96,6 +96,7 @@
      .rt-item-radio {
        flex-shrink: 0;
        margin-right: 8px;
        margin-top: 6px;
      }
    }
src/pages/robotaxi/home/components/privacy-popup/index.tsx
@@ -1,6 +1,6 @@
import Taro from '@tarojs/taro';
import { View, Text, Image } from '@tarojs/components';
import { useEffect, useMemo, useState } from 'react';
import { useEffect, useState } from 'react';
import './index.less';
import { privicyShowManager } from './utils'
import { submitAgreement } from '../../../../../robotaxi/api/home'
@@ -15,24 +15,12 @@
  onAgree?: () => void;
}
/**
 * 统一格式化协议名展示文案,避免协议名写死在页面中。
 */
const formatAgreementNamesText = (list: LoginAgreementItem[]) => {
  const names = list
    .map((item: LoginAgreementItem) => String(item?.name || '').trim())
    .filter((name: string) => !!name);
  if (names.length === 0) return '';
  return names.map((name: string) => `《${name}》`).join('、');
};
export default function PrivacyPopup({ show = false, privacyUpdateInfo, onClose, onAgree }: Props) {
  const [visible, setVisible] = useState(false);
  const [animationData, setAnimationData] = useState<any>();
  const [agreementList, setAgreementList] = useState<LoginAgreementItem[]>([]);
  const [isAgree, setIsAgree] = useState<Record<string, boolean>>({});
  const [isAllAgree, setIsAllAgree] = useState(false);
  const agreementNamesText = useMemo(() => formatAgreementNamesText(agreementList), [agreementList]); // 协议说明文案由协议列表动态生成
  // 拉取登录协议列表并初始化勾选状态
  const loadLoginAgreement = () => {
@@ -164,13 +152,13 @@
        <View className='rt-privacy-text'>
          <Text className='rt-text-content'>
            {privacyUpdateInfo?.isUpdateAuth
              ? `感谢您使用小鹏自动驾驶出行服务!为了更好地保障您隐私保护、个人信息保护等个人权益,我们更新了 ${agreementNamesText || ''},为保障您的出行权益,请您点击下方链接查阅完整协议,并完成授权确认后方可继续使用服务。`
              : `欢迎使用小鹏自动驾驶出行服务!为了更好地保障您隐私保护、个人信息保护等个人权益,辛苦查阅 ${agreementNamesText || ''} 。`
              ? `感谢您选择小鹏 Robotaxi 小程序!为更好保障您的个人合法权益,我们已更新相关协议。请点击下方链接查阅完整协议,完成授权确认后,即可继续使用服务。`
              : `欢迎使用小鹏自动驾驶出行服务!为充分保障您的隐私与个人信息相关合法权益,请点击下方链接查阅完整协议,完成授权确认后,即可使用叫车服务。`
            }
          </Text>
          <View className='rt-link-item mt-22 pt-10 check-all-box' onClick={checkAll}>
            <Image className='rt-item-radio' src={isAllAgree ? 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/check-20260630.png' : 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/uncheck.png'} mode='aspectFill' />
            <View className='rt-item-name all-name'>我已满18周岁且具有完全民事行为能力,已阅读并同意:</View>
            <Image className='rt-item-radio' src={isAllAgree ? 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/check-20260630.png' : 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/uncheck-20260804.png'} mode='aspectFill' />
            <View className='rt-item-name all-name'>我已满18周岁,已阅读并同意:</View>
          </View>
        </View>
src/pages/robotaxi/home/index.less
@@ -103,8 +103,16 @@
  }
  .origin-icon {
    position: absolute;
    bottom: 0px;
    right: 24px;
    width: 80px;
    height: 80px;
  }
  .tips-and-origin {
    position: relative;
    width: 100%;
    min-height: 80px;
  }
}
@@ -215,10 +223,6 @@
.content-box-top {
  width: 100%;
  position: fixed;
  top: 55%;
  left: 0px;
  z-index: 2;
  .position-box {
    .is-operating-area {
@@ -255,9 +259,6 @@
      }
    }
    .ml-32 {
      margin-left: 32px;
    }
    .mt-20 {
      margin-top: 20px;
src/robotaxi/api/begin-route.ts
@@ -42,7 +42,6 @@
    QueryOrderPaymentResultResponse,
    QueryLedgerCreditOrderResultResponse
} from './types/begin-route';
import { withApiLog } from '../utils/api-logger';
// 导航-获取路线和费用
export const queryRouteAndFare = async (payload: QueryRouteAndFarePayload): Promise<QueryRouteAndFareResponse> => {
@@ -152,23 +151,23 @@
// 设置温度
export const setTemperature = async (payload: SetTemperaturePayload): Promise<SetTemperatureResponse> => {
    return withApiLog('设置空调温度', payload, request({
    return request({
        url: _ROBOTAXI_HOST + '/vehicleConvoy/api/v1/mini-program/order/ac/set-temperature',
        method: 'POST',
        payload,
        showToast: false
    }, true));
    }, true);
}
// 设置温度并跳过业务异常拦截,供初始化越界温度纠正场景使用
export const setTemperatureIgnoreBusinessError = async (payload: SetTemperaturePayload): Promise<SetTemperatureResponse> => {
    return withApiLog('设置空调温度(忽略业务异常)', payload, request({
    return request({
        url: _ROBOTAXI_HOST + '/vehicleConvoy/api/v1/mini-program/order/ac/set-temperature',
        method: 'POST',
        payload,
        skipException: true,
        showToast: false
    }, true));
    }, true);
}
// 解锁车辆
@@ -186,32 +185,32 @@
export const queryTemperatureByOrderId = async (
    payload: OrderIdPayload
): Promise<QueryTemperatureByOrderIdResponse> => {
    return withApiLog('查询车辆温度', payload, request({
    return request({
        url: _ROBOTAXI_HOST + '/vehicleConvoy/api/v1/mini-program/order/queryTemperatureByOrderId',
        method: 'GET',
        payload
    }, true));
    }, true);
}
// 查询车辆空调开关状态
export const queryVehicleAirConditionerSwitch = async (
    payload: OrderIdPayload
): Promise<QueryVehicleAirConditionerSwitchResponse> => {
    return withApiLog('查询空调开关状态', payload, request({
    return request({
        url: _ROBOTAXI_HOST + '/vehicleConvoy/api/v1/mini-program/order/queryVehicleAirConditionerSwitch',
        method: 'GET',
        payload
    }, true));
    }, true);
}
// 车辆空调开关控制
export const airConditionerUnlock = async (payload: AirConditionerUnlockPayload): Promise<void> => {
    return withApiLog('空调开关控制', payload, request({
    return request({
        url: _ROBOTAXI_HOST + '/vehicleConvoy/api/v1/mini-program/order/airConditionerUnlock',
        method: 'POST',
        payload,
        showToast: false
    }, true));
    }, true);
}
// 查询后备箱开启状态
src/robotaxi/api/fragrance.ts
@@ -1,6 +1,5 @@
import request from '../../utils/request';
import { _ROBOTAXI_HOST } from '../../constants/api';
import { withApiLog } from '../utils/api-logger';
import type {
    QueryCabinFragranceResponse,
    SetCabinFragrancePayload
@@ -10,22 +9,21 @@
export const queryCabinFragrance = async (
    orderId: string
): Promise<QueryCabinFragranceResponse> => {
    const payload = { orderId };
    return withApiLog('查询车载香氛状态', payload, request({
    return request({
        url: _ROBOTAXI_HOST + '/vehicleConvoy/base/v1/mini-program/cabin/fragrance',
        method: 'GET',
        payload
    }, true));
        payload: { orderId }
    }, true);
};
// 设置车载香氛(聚合下发开关状态,开启时同时下发香型通道)
export const setCabinFragrance = async (
    payload: SetCabinFragrancePayload
): Promise<void> => {
    return withApiLog('设置车载香氛', payload, request({
    return request({
        url: _ROBOTAXI_HOST + '/vehicleConvoy/base/v1/mini-program/cabin/fragrance',
        method: 'POST',
        payload,
        skipException: true
    }, true));
    }, true);
};
src/robotaxi/api/genre.ts
New file
@@ -0,0 +1,31 @@
import request from '../../utils/request';
import { _ROBOTAXI_HOST } from '../../constants/api';
import type {
    QueryOrderGenreResponse,
    SetOrderGenrePayload
} from './types/genre';
const GENRE_API_URL = _ROBOTAXI_HOST + '/vehicleConvoy/api/v1/mini-program/order/genre';
// 查询当前订单曲风状态(音乐开关、当前曲风、已启用曲风列表)
export const queryOrderGenre = async (
    orderId: string
): Promise<QueryOrderGenreResponse> => {
    return request({
        url: GENRE_API_URL,
        method: 'GET',
        payload: { orderId }
    }, true);
};
// 设置当前订单曲风(下发音乐开关状态,开启时同时下发曲风 ID)
export const setOrderGenre = async (
    payload: SetOrderGenrePayload
): Promise<void> => {
    return request({
        url: GENRE_API_URL,
        method: 'POST',
        payload,
        skipException: true
    }, true);
};
src/robotaxi/api/seat-hv.ts
@@ -1,24 +1,22 @@
import request from '../../utils/request';
import { _ROBOTAXI_HOST } from '../../constants/api';
import { withApiLog } from '../utils/api-logger';
import type { QuerySeatHvResponse, SetSeatHvPayload } from './types/seat-hv';
// 查询座椅通风/加热状态
export const querySeatHv = async (orderId: string): Promise<QuerySeatHvResponse> => {
    const payload = { orderId };
    return withApiLog('查询座椅通风加热状态', payload, request({
    return request({
        url: _ROBOTAXI_HOST + '/vehicleConvoy/base/v1/mini-program/cabin/seat/hv',
        method: 'GET',
        payload
    }, true));
        payload: { orderId }
    }, true);
};
// 设置座椅通风/加热(seats 仅含被改动的座椅,通风与加热一起下发)
export const setSeatHv = async (payload: SetSeatHvPayload): Promise<void> => {
    return withApiLog('设置座椅通风加热', payload, request({
    return request({
        url: _ROBOTAXI_HOST + '/vehicleConvoy/base/v1/mini-program/cabin/seat/hv',
        method: 'POST',
        payload,
        skipException: true
    }, true));
    }, true);
};
src/robotaxi/api/types/genre.ts
New file
@@ -0,0 +1,37 @@
import type { OrderId } from './begin-route';
/** 单个曲风。 */
export interface GenreItem {
    /** 曲风唯一标识 / 曲风 ID(下发时使用)。 */
    id: string;
    /** 曲风显示名称。 */
    name: string;
    /** 曲风描述(可选,后端未下发时前端可留空)。 */
    desc?: string;
}
/** 订单曲风状态对象。 */
export interface OrderGenreState {
    /** 音乐开关状态。 */
    switchOn: boolean;
    /** 当前选中曲风 ID(关闭时可为空)。 */
    currentGenreId?: string;
    /** 后台已启用曲风列表(动态),空数组表示暂无可选曲风。 */
    availableGenres?: GenreItem[];
}
/** 查询订单曲风状态响应对象。 */
export interface QueryOrderGenreResponse {
    /** 曲风状态。 */
    genre?: OrderGenreState;
}
/** 设置订单曲风请求。 */
export interface SetOrderGenrePayload {
    /** 订单 ID。 */
    orderId: OrderId;
    /** 目标开关状态。 */
    switchOn: boolean;
    /** 目标曲风 ID;switchOn 为 true 时必传,false 时可不传。 */
    genreId?: string;
}
src/robotaxi/components/common-popup/index.less
New file
@@ -0,0 +1,74 @@
.common-popup {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 99999;
    display: flex;
    align-items: center;
    justify-content: center;
    .common-mask {
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background: rgba(0, 0, 0, 0.5);
    }
    .common-content {
        position: relative;
        width: 600rpx;
        height: 388rpx;
        box-sizing: border-box;
        display: flex;
        flex-direction: column;
        align-items: center;
        padding: 72rpx 40rpx 48rpx 40rpx;
        gap: 54rpx;
        border-radius: 48rpx;
        background: #FFFFFF;
        .close-icon {
            position: absolute;
            top: 24rpx;
            right: 24rpx;
            width: 48rpx;
            height: 48rpx;
        }
        .common-title {
            font-family: PingFang SC;
            font-size: 36rpx;
            font-weight: 500;
            line-height: 52rpx;
            text-align: center;
            letter-spacing: normal;
            color: #141E27;
        }
        .common-btn {
            width: 404rpx;
            height: 110rpx;
            box-sizing: border-box;
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            padding: 32rpx 80rpx;
            gap: 16rpx;
            z-index: 0;
            border-radius: 1000rpx;
            border: 4rpx solid #141E27;
            background: #FFFFFF;
            font-family: PingFang SC;
            font-size: 36rpx;
            font-weight: 500;
            line-height: 52rpx;
            letter-spacing: normal;
            color: #141E27;
        }
    }
}
src/robotaxi/components/common-popup/index.tsx
New file
@@ -0,0 +1,57 @@
import Taro from '@tarojs/taro';
import { View, Image } from '@tarojs/components';
import { useEffect, useState } from 'react';
import './index.less'
type Props = {
    show?: boolean;
    onClose?: () => void;
    showCloseIcon?: boolean;
    title?: string;
    btnText?: string;
    onBtnClick?: () => void;
}
export default function CommonPopup({ show = false, onClose, showCloseIcon = true, title = '', btnText = '', onBtnClick }: Props) {
    const [visible, setVisible] = useState(false);
    const [animationData, setAnimationData] = useState<any>();
    useEffect(() => {
        if (show && !visible) {
            setVisible(true);
            const animation = Taro.createAnimation({
                duration: 300,
                timingFunction: 'ease-out'
            });
            animation.opacity(1).scale(1).step();
            setAnimationData(animation.export());
        } else if (!show && visible) {
            const animation = Taro.createAnimation({
                duration: 300,
                timingFunction: 'ease-in'
            });
            animation.opacity(0).scale(0.8).step();
            setAnimationData(animation.export());
            setTimeout(() => {
                setVisible(false);
            }, 300);
        }
    }, [show]);
    if (!visible) return null;
    return (
        <View className='common-popup' catchMove>
            <View className='common-mask'></View>
            <View className='common-content' animation={animationData}>
                {showCloseIcon && (
                    <Image onClick={onClose} className='close-icon' src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/close-icon.png" />
                )}
                <View className='common-title'>
                    {title}
                </View>
                <View onClick={onBtnClick} className='common-btn'>{btnText}</View>
            </View>
        </View>
    )
}
src/robotaxi/components/coupon-select-popup/index.less
@@ -20,7 +20,7 @@
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.4);
    background: rgba(0, 0, 0, 0.8);
    z-index: 1;
  }
@@ -50,7 +50,7 @@
      left: 0;
      right: 0;
      bottom: 0;
      background: rgba(0, 0, 0, 0.5);
      background: rgba(0, 0, 0, 0.8);
    }
    .coupon-detail-content {
src/robotaxi/components/customer-service/index.less
@@ -12,7 +12,7 @@
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(0, 0, 0, 0.5);
    background: rgba(0, 0, 0, 0.8);
  }
  .customer-service-content {
@@ -41,4 +41,4 @@
      background: #ffffff;
    }
  }
}
}
src/robotaxi/components/demo/index.tsx
@@ -10,6 +10,8 @@
import { TripStatus } from '../../constants/trip-status';
// TODO(仅用于测试): 上线前删除以下通用 Toast 组件的 Demo 引入
import { showToast, hideToast } from '../toast';
// TODO(仅用于测试): 上线前删除以下通用弹窗组件的 Demo 引入
import CommonPopup from '../common-popup';
const DEMO_ORDER_ID = '2063814874050772993';
@@ -32,6 +34,7 @@
}
type State = {
  tripStatus: TripStatus | number;
  showCommonPopup: boolean;
}
export default class Loading extends PureComponent<Props, State> {
@@ -46,7 +49,8 @@
  constructor (props) {
    super(props)
    this.state = {
      tripStatus: TripStatus.ArrivedDestination,
      tripStatus: TripStatus.VehicleApproaching,
      showCommonPopup: false,
    }
  }
@@ -112,6 +116,28 @@
          </View>
        </View>
        <Text>通用弹窗 Demo</Text>
        {/* TODO(仅用于测试): 上线前删除 CommonPopup 测试渲染 */}
        <View className='toast-demo'>
          <View
            className='toast-demo__btn'
            onClick={() => this.setState({ showCommonPopup: true })}
          >
            打开通用弹窗
          </View>
        </View>
        <CommonPopup
          show={this.state.showCommonPopup}
          showCloseIcon
          title='这是一个通用弹窗的提示文案'
          btnText='知道了'
          onClose={() => this.setState({ showCommonPopup: false })}
          onBtnClick={() => {
            showToast({ title: '按钮点击(Demo)', icon: 'none' });
            this.setState({ showCommonPopup: false });
          }}
        />
        <Text>空调卡片 Demo</Text>
        {/* TODO(仅用于测试): 上线前删除 TemperatureControl 测试渲染,.calling-car 包裹仅用于补全样式层级 */}
        <View className='calling-car'>
src/robotaxi/components/tip-popup/index.less
@@ -15,7 +15,7 @@
        left: 0;
        right: 0;
        bottom: 0;
        background: rgba(0, 0, 0, 0.5);
        background: rgba(0, 0, 0, 0.8);
    }
    .tip-content {
src/robotaxi/pages/begin-route/components/calling-car-modules/bluetooth-operate-card.tsx
@@ -33,7 +33,7 @@
            {shouldRenderRight && (
                <View className="bluetooth-operate-card-right">
                    {/* {!!actionText && <View>{actionText}</View>} */}
                    <Image src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/blue-right-icon-20260706.png" />
                    <Image src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/bluetooth-arrow-20260706.png" />
                </View>
            )}
        </View>
src/robotaxi/pages/begin-route/components/calling-car-modules/door-trunk-control.tsx
@@ -306,7 +306,7 @@
                </View>
                <View className={`car-card-arrive-item ${lockStatus && 'close-btn-item'} ${loadingControlType ? 'trip-action-bar-item-disabled' : ''}`.trim()} onClick={() => handleUnlock('door', onDoorUnlockSuccess)}>
                    <View className="car-card-arrive-item-text">车锁</View>
                    <View className="car-card-arrive-item-status">{lockStatus ? '点击关闭' : '点击打开'}</View>
                    <View className="car-card-arrive-item-status">{lockStatus ? '车门已解锁' : '点击打开'}</View>
                    {renderControlIcon('door', lockStatus ? LOCK_OPEN_ICON_URL : LOCK_CLOSE_ICON_URL, 'car-card-arrive-item-img')}
                </View>
            </View>
src/robotaxi/pages/begin-route/components/calling-car-modules/fare-detail-card.tsx
@@ -7,6 +7,7 @@
type Props = {
    tripStatus: TripStatus;
    fareBreakdown: any;
    manuallySelectedCouponName?: string;
    orderId?: string;
    resetDetailVersion?: number;
    onToggleDetailFade?: (expanded: boolean, hasFare: boolean) => void;
@@ -17,6 +18,7 @@
export default function FareDetailCard({
    tripStatus,
    fareBreakdown,
    manuallySelectedCouponName = '',
    orderId,
    resetDetailVersion,
    onToggleDetailFade,
@@ -25,6 +27,11 @@
}: Props) {
    const [showDetail, setShowDetail] = useState(false); // 费用明细展开状态
    const canOpenCouponSelect = tripStatus === TripStatus.TripFinished && typeof onOpenCouponSelect === 'function'; // 仅待支付支持切换优惠券
    const discountTitleText = manuallySelectedCouponName || '最佳优惠';
    const discountDescText = manuallySelectedCouponName
        ? `已选择${manuallySelectedCouponName}`
        : '已选择最佳优惠';
    const isCreditPayProcessing = (
        tripStatus === TripStatus.TripFinished
        && !!orderId
@@ -202,8 +209,8 @@
                    (fareBreakdown?.operateFree != 1 && [TripStatus.TripFinished].includes(tripStatus)) && Number(fareBreakdown?.redPacketMoney + fareBreakdown?.couponMoney) > 0 &&
                    <View className="car-card-end-through" onClick={canOpenCouponSelect ? onOpenCouponSelect : undefined}>
                        <View className="car-card-end-through-left">
                            <View className="car-card-end-through-content-title">最佳优惠</View>
                            <View className="car-card-end-through-desc">已为您选择最佳优惠</View>
                            <View className="car-card-end-through-content-title">{discountTitleText}</View>
                            <View className="car-card-end-through-desc">{discountDescText}</View>
                        </View>
                        <View className="car-card-end-through-right">
                            <View className="car-card-end-through-content-money">-{formatMoney(fareBreakdown?.redPacketMoney + fareBreakdown?.couponMoney)}元</View>
src/robotaxi/pages/begin-route/components/calling-car-modules/fragrance-control/index.tsx
@@ -7,6 +7,7 @@
import { getOrderId } from '../../../../../utils/order';
import { TripStatus } from '../../../../../constants/trip-status';
import useBottomPopupAnimation from '../../hooks/use-bottom-popup-animation';
import AcSwitch from '../ac-switch';
import '../../css-folder/fragrance-control.less';
const FC_HEADER_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/fragrance-icon-20260716.png';
@@ -27,8 +28,9 @@
    const cnNum = scentCount <= 10 ? CN_NUMBERS[scentCount] : String(scentCount);
    return `可选${cnNum}款香型`;
}
const FRAGRANCE_OPEN_SUCCESS_TEXT = '已开启香氛'; // 开启香氛成功提示文案
const FRAGRANCE_OPEN_PREFIX = '已开启香氛:'; // 开启/切换香氛成功提示前缀(后接香氛名称)
const CLOSE_SUCCESS_TEXT = '已关闭香氛';
const PANEL_CLOSE_DELAY_MS = 150; // 下发成功后延迟关闭弹窗,优化交互体验
const APPLY_FAIL_TEXT = '网络异常,请重试';
/** 香调描述本地兜底表:接口未返回 desc 时按 id 取用。 */
@@ -64,24 +66,24 @@
}
/**
 * 计算车端开启态下当前生效的香型通道。
 * 计算初始选中香型通道:优先车端当前香型,其次列表首项(关闭态也保留选中,用于卡片展示当前香氛)。
 */
function resolveActiveChannelId(scents: FragranceScent[], switchOn: boolean, currentScent?: string): string {
    if (!switchOn || !scents.length) {
        return '';
function resolveInitialChannelId(scents: FragranceScent[], currentScent?: string): string {
    if (currentScent) {
        const matched = scents.find((item) => item.id === currentScent);
        if (matched) {
            return matched.channelId;
        }
    }
    const matched = currentScent ? scents.find((item) => item.id === currentScent) : undefined;
    return (matched || scents[0]).channelId;
    return scents.length ? scents[0].channelId : '';
}
export default function FragranceControl({ tripStatus, orderId, pageDidShowVersion = 0, hasPreviousOrder = false }: Props) {
    // 车端真实状态
    const [switchOn, setSwitchOn] = useState(false);
    const [scents, setScents] = useState<FragranceScent[]>([]);
    // 当前生效香型通道(关闭时为空)
    const [activeChannelId, setActiveChannelId] = useState('');
    // 卡片主标题(开启时展示香型名,关闭时展示"已关闭")
    const [cardTitle, setCardTitle] = useState(CLOSED_TITLE);
    // 当前选中香型通道(关闭态也保留,用于卡片展示当前香氛)
    const [selectedChannelId, setSelectedChannelId] = useState('');
    // 面板显隐 + 下发锁
    const [panelVisible, setPanelVisible] = useState(false);
    const [applying, setApplying] = useState(false);
@@ -94,6 +96,11 @@
    const { animationData, shouldRender } = useBottomPopupAnimation(panelVisible);
    const hasScents = scents.length > 0;
    const selectedScent = findScentByChannel(scents, selectedChannelId);
    // 卡片主标题:始终展示当前选中香型名(关闭态由样式置灰),无香型时兜底"已关闭"
    const cardTitle = selectedScent?.name || (hasScents ? scents[0].name : CLOSED_TITLE);
    // 开关 loading 仅在由开关(而非香型磁贴)触发下发时展示
    const switchLoading = applying && applyingChannelId === '';
    // 进入接驾态查询香氛状态;连环单不查询、不展示
    useEffect(() => {
@@ -119,12 +126,9 @@
            const fragrance = res || {};
            const nextSwitchOn = fragrance.switchOn === true;
            const nextScents: FragranceScent[] = Array.isArray(fragrance.availableScents) ? fragrance.availableScents : [];
            const nextActiveChannelId = resolveActiveChannelId(nextScents, nextSwitchOn, fragrance.currentScent);
            setSwitchOn(nextSwitchOn);
            setScents(nextScents);
            setActiveChannelId(nextActiveChannelId);
            const activeName = findScentByChannel(nextScents, nextActiveChannelId)?.name || '';
            setCardTitle(nextSwitchOn && activeName ? activeName : CLOSED_TITLE);
            setSelectedChannelId(resolveInitialChannelId(nextScents, fragrance.currentScent));
        }).catch(() => {
            if (querySeqRef.current !== snapshotSeq) {
                return;
@@ -132,8 +136,7 @@
            // 查询失败按「已关闭」兜底
            setSwitchOn(false);
            setScents([]);
            setActiveChannelId('');
            setCardTitle(CLOSED_TITLE);
            setSelectedChannelId('');
        });
    }, [tripStatus, orderId, pageDidShowVersion, hasPreviousOrder]);
@@ -146,41 +149,66 @@
        setPanelVisible(false);
    };
    // 点击香型即时下发:点选中项 → 关闭香氛;点其它项 → 开启并切换香型
    const handleScentClick = async (channelId: string) => {
        if (applying) {
    // 点击卡片:只有一个香氛时卡片本体不可点击(仅右上角开关控制);多个香氛时打开选择面板
    const handleCardClick = () => {
        if (scents.length <= 1) {
            return;
        }
        togglePanel();
    };
    // 点击香型即时下发:点选中项 → 关闭香氛;点其它项 → 开启并切换香型
    /**
     * 下发香氛设置到车端;成功后同步开关与选中香型、关闭弹窗并 toast,失败提示重试。
     * @param loadingChannelId 触发下发的香型通道 ID(用于在该磁贴展示 loading);由开关触发时传空串。
     */
    const applyFragrance = async (nextSwitchOn: boolean, channelId: string, loadingChannelId = '') => {
        const targetOrderId = getOrderId(orderId);
        if (!targetOrderId) {
            return;
        }
        // 点击当前生效香型 → 关闭;否则 → 开启该香型
        const turningOff = switchOn && channelId === activeChannelId;
        const nextSwitchOn = !turningOff;
        setApplying(true);
        setApplyingChannelId(channelId);
        setApplyingChannelId(loadingChannelId);
        try {
            await setCabinFragrance({
                orderId: targetOrderId,
                switchOn: nextSwitchOn,
                ...(nextSwitchOn ? { channelId } : {})
            });
            // 下发成功后同步车端真实状态与卡片主标题
            // 下发成功后同步车端真实状态(关闭态保留选中香型不重置)
            setSwitchOn(nextSwitchOn);
            const nextActiveChannelId = nextSwitchOn ? channelId : '';
            setActiveChannelId(nextActiveChannelId);
            const activeName = findScentByChannel(scents, nextActiveChannelId)?.name || '';
            setCardTitle(nextSwitchOn && activeName ? activeName : CLOSED_TITLE);
            // 开启/关闭香氛均使用通用 Toast 组件提示(success),并关闭香氛设置弹窗
            showToast({ title: nextSwitchOn ? FRAGRANCE_OPEN_SUCCESS_TEXT : CLOSE_SUCCESS_TEXT, icon: 'success' });
            closePanel();
            if (nextSwitchOn && channelId) {
                setSelectedChannelId(channelId);
            }
            // 开启/关闭香氛:通用 Toast 提示(success),并延迟 150ms 关闭香氛设置弹窗(优化交互体验)
            const appliedScentName = findScentByChannel(scents, channelId)?.name || '';
            showToast({ title: nextSwitchOn ? `${FRAGRANCE_OPEN_PREFIX}${appliedScentName}` : CLOSE_SUCCESS_TEXT, icon: 'success' });
            setTimeout(() => closePanel(), PANEL_CLOSE_DELAY_MS);
        } catch (error) {
            showToast({ title: APPLY_FAIL_TEXT, icon: 'none' });
        } finally {
            setApplying(false);
            setApplyingChannelId('');
        }
    };
    // 点击香型磁贴:点当前生效香型 → 关闭;点其它 → 开启并切换到该香型
    const handleScentClick = (channelId: string) => {
        if (applying) {
            return;
        }
        const turningOff = switchOn && channelId === selectedChannelId;
        applyFragrance(!turningOff, channelId, channelId);
    };
    // 切换总开关(卡片主开关 / 弹窗底部开关共用):打开→下发当前选中香型;关闭→下发关闭并保留选中
    const handleToggleSwitch = () => {
        if (applying || !hasScents) {
            return;
        }
        const nextSwitchOn = !switchOn;
        const targetChannelId = selectedChannelId || scents[0]?.channelId || '';
        applyFragrance(nextSwitchOn, targetChannelId, '');
    };
    // 连环单或非接驾态不展示香氛卡片
@@ -196,19 +224,20 @@
            {/* 香氛入口卡片(复用 tf-card 视觉):点击打开面板 */}
            <View
                className={`tf-card tf-card-fragrance ${fragranceCardStateClass}`}
                onClick={togglePanel}
                onClick={handleCardClick}
            >
                <View className='tf-card-header'>
                    <View className='tf-card-header-left'>
                        <Image className='tf-card-header-icon' src={FC_HEADER_ICON} />
                        <Text className='tf-card-header-title'>香氛</Text>
                    </View>
                    <AcSwitch checked={switchOn} loading={switchLoading} onChange={handleToggleSwitch} />
                </View>
                <View className='tf-card-bottom'>
                    <View className='tf-card-fragrance-name'>{cardTitle}</View>
                    <View className='tf-card-footer'>
                        <Text className='tf-card-footer-text'>{resolveCardSubtitle(scents.length)}</Text>
                        <Image className='tf-card-footer-arrow' src={ARROW_ICON_URL} />
                        {scents.length > 1 && <Image className='tf-card-footer-arrow' src={ARROW_ICON_URL} />}
                    </View>
                </View>
            </View>
@@ -227,7 +256,7 @@
                            {hasScents ? (
                                <View className='popup-scent-list'>
                                    {scents.map((item) => {
                                        const isActive = switchOn && item.channelId === activeChannelId;
                                        const isActive = switchOn && item.channelId === selectedChannelId;
                                        const isLoading = applyingChannelId === item.channelId;
                                        const desc = resolveScentDesc(item);
                                        return (
src/robotaxi/pages/begin-route/components/calling-car-modules/music-control/index.tsx
New file
@@ -0,0 +1,287 @@
import { View, Text, Image } from '@tarojs/components';
import { useEffect, useRef, useState } from 'react';
import { queryOrderGenre, setOrderGenre } from '../../../../../api/genre';
import type { GenreItem } from '../../../../../api/types/genre';
import { getOrderId } from '../../../../../utils/order';
import { TripStatus } from '../../../../../constants/trip-status';
import useBottomPopupAnimation from '../../hooks/use-bottom-popup-animation';
import AcSwitch from '../ac-switch';
import { showToast } from '../../../../../components/toast';
import '../../css-folder/music-control.less';
const GENRE_HEADER_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/genre-0731@2x.png';
const ARROW_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/temperature-fragrance-arrow-20260706.png';
const CLOSE_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/close-icon.png';
const LOADING_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/T-switch-loading-20260715.png';
const CARD_DEFAULT_TITLE = '车载音乐'; // 无可选曲风时卡片主标题兜底
const NO_GENRE_TIP = '当前暂无可选曲风'; // 空曲风列表占位
const PLAY_TIP_TEXT = '音乐将在您上车后开始播放'; // 弹窗底部开关文案
const OPEN_PLAY_PREFIX = '上车后将自动播放:'; // 开启/切换曲风成功提示前缀(后接曲风名称)
const CLOSE_SUCCESS_TEXT = '音乐已关闭';
const PANEL_CLOSE_DELAY_MS = 150; // 下发成功后延迟关闭弹窗,优化交互体验
const APPLY_FAIL_TEXT = '网络异常,请重试';
// 车端下发结果失败标识:接口 code 可能为 200,但 data 返回 'FAILED' 表示车端下发失败
const APPLY_FAIL_RESULT = 'FAILED';
// 卡片副标题轮播文案(上下滚动轮播,动效由 CSS 实现)
const SUBTITLE_TEXTS = [
    '下一站,跟着节奏出发',
    '好音乐,陪你出发',
    '长风相送,雅韵随行',
    '带上音乐,轻松启程',
    '给旅程多一些旋律'
];
type Props = {
    tripStatus: TripStatus;
    orderId?: string;
    pageDidShowVersion?: number;
    /** 后端推送:当前被指派车辆是否仍在执行上一单(连环单) */
    hasPreviousOrder?: boolean;
};
/**
 * 从曲风列表中按 ID 查找曲风。
 */
function findGenreById(genres: GenreItem[], genreId: string): GenreItem | undefined {
    return genres.find((item) => item.id === genreId);
}
/**
 * 计算初始选中曲风:优先车端当前曲风,其次列表首项。
 */
function resolveInitialGenreId(genres: GenreItem[], currentGenreId?: string): string {
    if (currentGenreId && findGenreById(genres, currentGenreId)) {
        return currentGenreId;
    }
    return genres.length ? genres[0].id : '';
}
export default function MusicControl({ tripStatus, orderId, pageDidShowVersion = 0, hasPreviousOrder = false }: Props) {
    // 车端真实开关状态
    const [switchOn, setSwitchOn] = useState(false);
    // 后台配置曲风列表
    const [genres, setGenres] = useState<GenreItem[]>([]);
    // 当前选中曲风 ID(关闭态下也可有值,仅本地记录,不下发)
    const [selectedGenreId, setSelectedGenreId] = useState('');
    // 面板显隐 + 下发锁
    const [panelVisible, setPanelVisible] = useState(false);
    const [applying, setApplying] = useState(false);
    // 正在下发的曲风磁贴 ID(该磁贴显示 loading),空串表示由开关触发(loading 落在开关上)
    const [applyingGenreId, setApplyingGenreId] = useState('');
    const querySeqRef = useRef(0);
    // 记录本次接驾态周期内已查询过曲风状态的订单 ID,避免重复请求(同一订单在初始化恢复
    // 流程中 tripStatus/orderId/hasPreviousOrder 会分批异步更新,触发本 effect 多次)。
    const queriedOrderIdRef = useRef('');
    const { animationData, shouldRender } = useBottomPopupAnimation(panelVisible);
    const hasGenres = genres.length > 0;
    const selectedGenre = findGenreById(genres, selectedGenreId);
    // 卡片主标题:始终展示曲风名(关闭态由样式置灰),无曲风时兜底
    const cardTitle = selectedGenre?.name || (hasGenres ? genres[0].name : CARD_DEFAULT_TITLE);
    // 开关 loading 仅在由开关(而非磁贴)触发下发时展示
    const switchLoading = applying && applyingGenreId === '';
    // 进入接驾态查询曲风状态;连环单不查询、不展示
    useEffect(() => {
        if (tripStatus !== TripStatus.VehicleApproaching || hasPreviousOrder) {
            // 离开接驾态:重置去重标记,允许下次进入接驾态时重新查询一次。
            queriedOrderIdRef.current = '';
            return;
        }
        const targetOrderId = getOrderId(orderId);
        if (!targetOrderId) {
            return;
        }
        // 去重:同一订单在本次接驾态周期内只查询一次。
        if (queriedOrderIdRef.current === targetOrderId) {
            return;
        }
        queriedOrderIdRef.current = targetOrderId;
        const snapshotSeq = ++querySeqRef.current;
        queryOrderGenre(targetOrderId).then((res: any) => {
            if (querySeqRef.current !== snapshotSeq) {
                return;
            }
            const genre = res || {};
            const nextSwitchOn = genre.switchOn === true;
            const nextGenres: GenreItem[] = Array.isArray(genre.availableGenres) ? genre.availableGenres : [];
            setSwitchOn(nextSwitchOn);
            setGenres(nextGenres);
            setSelectedGenreId(resolveInitialGenreId(nextGenres, genre.currentGenreId));
        }).catch(() => {
            if (querySeqRef.current !== snapshotSeq) {
                return;
            }
            // 查询失败按「已关闭」兜底
            setSwitchOn(false);
            setGenres([]);
            setSelectedGenreId('');
        });
    }, [tripStatus, orderId, pageDidShowVersion, hasPreviousOrder]);
    // 点击卡片:切换面板开合
    const togglePanel = () => {
        setPanelVisible((prev) => !prev);
    };
    const closePanel = () => {
        setPanelVisible(false);
    };
    /**
     * 下发曲风设置到车端;成功后同步开关状态、关闭弹窗并 toast,失败回滚并提示。
     * @param loadingGenreId 触发下发的曲风磁贴 ID(用于在该磁贴上展示 loading);由开关触发时传空串。
     */
    const applyGenre = async (nextSwitchOn: boolean, genreId: string, loadingGenreId = '') => {
        const targetOrderId = getOrderId(orderId);
        if (!targetOrderId) {
            return;
        }
        setApplying(true);
        setApplyingGenreId(loadingGenreId);
        try {
            const res: any = await setOrderGenre({
                orderId: targetOrderId,
                switchOn: nextSwitchOn,
                ...(nextSwitchOn ? { genreId } : {})
            });
            // skipException 下业务失败会以数据形式 resolve,需显式校验:
            // ① HTTP 状态码非 200;② code 为 200 但 data 返回 'FAILED'(车端下发失败)
            if (res && ((res.code !== undefined && res.code !== 200) || res.data === APPLY_FAIL_RESULT)) {
                throw res;
            }
            setSwitchOn(nextSwitchOn);
            if (nextSwitchOn && genreId) {
                setSelectedGenreId(genreId);
            }
            // 打开/关闭成功:toast 提示,并延迟 150ms 关闭控制弹窗(优化交互体验)
            const appliedGenreName = findGenreById(genres, genreId)?.name || '';
            showToast({ title: nextSwitchOn ? `${OPEN_PLAY_PREFIX}${appliedGenreName}` : CLOSE_SUCCESS_TEXT, icon: 'success' });
            setTimeout(() => setPanelVisible(false), PANEL_CLOSE_DELAY_MS);
        } catch (error) {
            // 下发失败:不改动本地状态(回滚),仅提示
            showToast({ title: APPLY_FAIL_TEXT, icon: 'none' });
        } finally {
            setApplying(false);
            setApplyingGenreId('');
        }
    };
    /**
     * 点击曲风磁贴:点当前生效曲风 → 关闭;点其它曲风 → 开启并切换到该曲风。
     * 该磁贴上方展示 loading,成功后同步底部总开关状态。
     */
    const handleGenreClick = (genreId: string) => {
        if (applying) {
            return;
        }
        const turningOff = switchOn && genreId === selectedGenreId;
        applyGenre(!turningOff, genreId, genreId);
    };
    /**
     * 切换总开关(卡片主开关 / 弹窗底部开关共用):
     * - 打开:下发 switchOn=true + 当前选中曲风(无则取列表首项);
     * - 关闭:下发 switchOn=false,保留已选曲风不重置。
     */
    const handleToggleSwitch = () => {
        if (applying || !hasGenres) {
            return;
        }
        const nextSwitchOn = !switchOn;
        const targetGenreId = selectedGenreId || genres[0]?.id || '';
        applyGenre(nextSwitchOn, targetGenreId, '');
    };
    // 连环单或非接驾态不展示音乐卡片
    if (tripStatus !== TripStatus.VehicleApproaching || hasPreviousOrder) {
        return null;
    }
    // 音乐卡片背景随开关状态切换
    const musicCardStateClass = switchOn ? 'is-open' : 'is-close';
    return (
        <View className='music-control'>
            {/* 音乐入口卡片(复用 tf-card 视觉):点击打开面板 */}
            <View
                className={`tf-card tf-card-music ${musicCardStateClass}`}
                onClick={togglePanel}
            >
                <View className='tf-card-header'>
                    <View className='tf-card-header-left'>
                      <Image className='tf-card-header-icon' src={GENRE_HEADER_ICON} />
                        <Text className='tf-card-header-title'>音乐</Text>
                    </View>
                    <AcSwitch
                        checked={switchOn}
                        loading={switchLoading}
                        onChange={handleToggleSwitch}
                    />
                </View>
                <View className='tf-card-bottom'>
                    <View className='tf-card-music-name'>{cardTitle}</View>
                    <View className='tf-card-footer'>
                        {/* 副标题上下滚动轮播:末尾克隆首条实现无缝循环,动效由 CSS 驱动 */}
                        <View className='tf-card-footer-text tf-card-subtitle'>
                            <View className='tf-card-subtitle-track'>
                                {SUBTITLE_TEXTS.map((text, idx) => (
                                    <View key={idx} className='tf-card-subtitle-item'>{text}</View>
                                ))}
                                <View className='tf-card-subtitle-item'>{SUBTITLE_TEXTS[0]}</View>
                            </View>
                        </View>
                        <Image className='tf-card-footer-arrow' src={ARROW_ICON_URL} />
                    </View>
                </View>
            </View>
            {/* 控制面板(底部弹窗) */}
            {shouldRender && (
                <View className={`music-popup ${panelVisible ? 'show' : ''}`} catchMove>
                    <View className='popup-mask' onClick={closePanel}></View>
                    <View className='popup-content' animation={animationData}>
                        <View className='popup-header'>
                            <Text className='popup-title'>车载音乐</Text>
                            <Image className='popup-close' src={CLOSE_ICON_URL} onClick={closePanel} />
                        </View>
                        <View className='popup-genre-area'>
                            {hasGenres ? (
                                <View className='popup-genre-list'>
                                    {genres.map((item) => {
                                        const isActive = switchOn && item.id === selectedGenreId;
                                        const isLoading = applyingGenreId === item.id;
                                        return (
                                            <View
                                                key={item.id}
                                                className={`popup-genre-item ${isActive ? 'active' : ''} ${applying ? 'disabled' : ''}`}
                                                onClick={() => handleGenreClick(item.id)}
                                            >
                                                {
                                                  isLoading ?
                                                    <Image className='popup-genre-loading' src={LOADING_ICON_URL} /> :
                                                    <Text className='popup-genre-name'>{item.name}</Text>
                                                }
                                            </View>
                                        );
                                    })}
                                </View>
                            ) : (
                                <View className='popup-genre-empty'>{NO_GENRE_TIP}</View>
                            )}
                        </View>
                        {/* 底部播放开关 */}
                        <View className='popup-play-switch'>
                            <Text className='popup-play-text'>{PLAY_TIP_TEXT}</Text>
                            <AcSwitch checked={switchOn} loading={switchLoading} onChange={handleToggleSwitch} />
                        </View>
                    </View>
                </View>
            )}
        </View>
    );
}
src/robotaxi/pages/begin-route/components/calling-car-modules/seat-hv-control/index.tsx
@@ -1,6 +1,6 @@
import Taro from '@tarojs/taro';
import { showToast } from '../../../../../components/toast';
import { View, Image } from '@tarojs/components';
import { View, Image, Text } from '@tarojs/components';
import { useEffect, useRef, useState } from 'react';
import { querySeatHv, setSeatHv } from '../../../../../api/seat-hv';
import type { SeatHvItem, SeatSetItem } from '../../../../../api/types/seat-hv';
@@ -8,7 +8,7 @@
import { TripStatus } from '../../../../../constants/trip-status';
import '../../css-folder/seat-hv-control.less';
const SEAT_BG_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/seat_bg_0715.png';
const SEAT_BG_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/seat_bg_0805@2x.png';
const IMG_BASE = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/';
const LOADING_ICON_URL = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/T-switch-loading-20260715.png';
const NO_SEAT_TIP = '座椅调节暂不可用';
@@ -176,6 +176,7 @@
                                        {ventilationLoading
                                            ? <Image className='seat-hv-btn-icon seat-hv-btn-loading' src={LOADING_ICON_URL} />
                                            : <Image className='seat-hv-btn-icon' src={resolveSeatIcon('ventilation', seat.ventilation)} />}
                                        <Text className='seat-hv-btn-label'>通风</Text>
                                    </View>
                                )}
                                {isPillLayout && <View className='seat-hv-divider'></View>}
@@ -187,6 +188,7 @@
                                        {heatingLoading
                                            ? <Image className='seat-hv-btn-icon seat-hv-btn-loading' src={LOADING_ICON_URL} />
                                            : <Image className='seat-hv-btn-icon' src={resolveSeatIcon('heating', seat.heating)} />}
                                        <Text className='seat-hv-btn-label'>加热</Text>
                                    </View>
                                )}
                            </View>
src/robotaxi/pages/begin-route/components/calling-car-modules/temperature-control.tsx
@@ -501,7 +501,7 @@
                        {/* 空调温度滑动选择(底部白色圆角卡片) */}
                        <View className='popup-ac'>
                            <Text className='popup-ac-title'>温度调节</Text>
                            <Text className='popup-ac-title'>二排温度调节</Text>
                            <View className='popup-temp-wrap'>
                                <ScrollView
                                    className='popup-temp-scroll'
src/robotaxi/pages/begin-route/components/calling-car.tsx
@@ -1,4 +1,4 @@
import { View, Image } from "@tarojs/components";
import { View, Image, ScrollView } from "@tarojs/components";
import { useState, useEffect } from 'react';
import './css-folder/calling-car.less'
import { formatRemainingDurationDisplay } from '../../../utils/format';
@@ -11,6 +11,7 @@
import VehicleInfoCard from './calling-car-modules/vehicle-info-card'; // 车辆信息卡片
import TemperatureControl from './calling-car-modules/temperature-control'; // 空调温控(含温度调节面板 + 座椅通风加热)
import FragranceControl from './calling-car-modules/fragrance-control'; // 车载香氛控制
import MusicControl from './calling-car-modules/music-control'; // 车载音乐曲风控制
import TripActionBar from './calling-car-modules/trip-action-bar'; // 行程操作栏
import DoorTrunkControl from './calling-car-modules/door-trunk-control'; // 门箱控制
import FareDetailCard from './calling-car-modules/fare-detail-card'; // 费用明细卡片
@@ -74,6 +75,8 @@
    finishCountdownSeconds?: number | null;
    /** 费用明细 */
    fareBreakdown?: any;
    /** 当前流程内用户手动切换后的优惠券名称 */
    manuallySelectedCouponName?: string;
    /** 车辆信息 */
    vehicleInfo?: {
        vehiclePictures?: string;
@@ -138,6 +141,7 @@
    pickupCountdownSeconds = null,
    finishCountdownSeconds = null,
    fareBreakdown = null,
    manuallySelectedCouponName = '',
    vehicleInfo,
    orderId,
    hasBeforeUserEndSite = false,
@@ -305,10 +309,17 @@
            {
                [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup, TripStatus.InService].includes(tripStatus) &&
                <View className='car-card'>
                    <View className='temp-fragrance-control'>
                      <TemperatureControl tripStatus={tripStatus} orderId={orderId} pageDidShowVersion={pageDidShowVersion} hasPreviousOrder={hasPreviousOrder} />
                      <FragranceControl tripStatus={tripStatus} orderId={orderId} pageDidShowVersion={pageDidShowVersion} hasPreviousOrder={hasPreviousOrder} />
                    </View>
                    {/* 车控卡片横向滑动(仅接驾态展示):weapp 中普通 View 的 overflow 滚动无法响应拖动,必须用 ScrollView */}
                    {
                        TripStatus.VehicleApproaching === tripStatus &&
                        <ScrollView className='temp-fragrance-scroll' scrollX showScrollbar={false}>
                          <View className='temp-fragrance-control'>
                            <TemperatureControl tripStatus={tripStatus} orderId={orderId} pageDidShowVersion={pageDidShowVersion} hasPreviousOrder={hasPreviousOrder} />
                            <MusicControl tripStatus={tripStatus} orderId={orderId} pageDidShowVersion={pageDidShowVersion} hasPreviousOrder={hasPreviousOrder} />
                            <FragranceControl tripStatus={tripStatus} orderId={orderId} pageDidShowVersion={pageDidShowVersion} hasPreviousOrder={hasPreviousOrder} />
                          </View>
                        </ScrollView>
                    }
                    <DoorTrunkControl
                        tripStatus={tripStatus}
@@ -323,6 +334,7 @@
                    <FareDetailCard
                      tripStatus={tripStatus}
                      fareBreakdown={fareBreakdown}
                      manuallySelectedCouponName={manuallySelectedCouponName}
                      orderId={orderId}
                      resetDetailVersion={resetDetailVersion}
                      onToggleDetailFade={onToggleDetailFade}
@@ -337,6 +349,7 @@
                    <FareDetailCard
                      tripStatus={tripStatus}
                      fareBreakdown={fareBreakdown}
                      manuallySelectedCouponName={manuallySelectedCouponName}
                      orderId={orderId}
                      resetDetailVersion={resetDetailVersion}
                      onToggleDetailFade={handleFareToggle}
@@ -346,6 +359,7 @@
                </View>
            }
            {
                /* 行程操作栏:位于费用明细卡之后、门箱控制之前(对齐设计稿) */
                [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup, TripStatus.InService, TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(tripStatus) &&
                <TripActionBar
                  tripStatus={tripStatus}
@@ -358,7 +372,6 @@
                  handleShareTrip={handleShareTrip}
                />
            }
            <DoorTrunkControl
                tripStatus={tripStatus === TripStatus.ArrivedDestination ? TripStatus.ArrivedDestination : -1}
                orderId={orderId}
src/robotaxi/pages/begin-route/components/cancel-popup.tsx
@@ -122,9 +122,9 @@
                                    <View className='cancel-site-box-icon-end'></View>
                                </View>
                                <View className='cancel-site-box-content'>
                                    <View>{startSiteName}</View>
                                    <View className='cancel-site-box-content-name'>{startSiteName}</View>
                                    <View className='cancel-site-box-divider'></View>
                                    <View>{endSiteName}</View>
                                    <View className='cancel-site-box-content-name'>{endSiteName}</View>
                                </View>
                            </View>
                            <View className='cancel-btn-box'>
src/robotaxi/pages/begin-route/components/css-folder/announcements-popup.less
@@ -12,7 +12,7 @@
        left: 0;
        right: 0;
        bottom: 0;
        background: rgba(0, 0, 0, 0);
        background: rgba(0, 0, 0, 0.8);
    }
    .announcements-content {
@@ -48,4 +48,4 @@
            height: 313px;
        }
    }
}
}
src/robotaxi/pages/begin-route/components/css-folder/calling-car.less
@@ -239,6 +239,7 @@
  .car-card {
    position: relative;
    z-index: 3;
    margin-top: 8px;
    border-radius: 60rpx;
  }
  .fare-box-card {
@@ -636,7 +637,7 @@
    align-items: center;
  }
  .car-card-end-through-desc {
    margin-left: 10px;
    margin-left: 8px;
    font-size: 24px;
    line-height: 40px;
    letter-spacing: normal;
@@ -673,7 +674,8 @@
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 32px 8px 0 8px;
    margin-top: 8px;
    padding: 32px 8px;
  }
  .trip-action-bar-item {
    display: flex;
@@ -919,6 +921,7 @@
  display: flex;
  align-items: center;
  justify-content: flex-start;
  margin-top: 8px;
  padding: 24px 0px;
  border-radius: 16px;
  background: @gary-theme;
@@ -953,7 +956,7 @@
    font-weight: normal;
    line-height: 40px;
    letter-spacing: normal;
    color: #6d7fd9;
    color:  @black-text-main;
    image {
      flex-shrink: 0;
      width: 24px;
@@ -1092,14 +1095,46 @@
  }
}
/* 空调 + 香氛 左右布局容器(迁移自 temperature-fragrance-control.less) */
.temp-fragrance-control {
/* 车控入口卡片横向滑动容器(空调 / 音乐 / 香氛,卡片固定宽度,超出宽度可横向滑动)
   用 ScrollView 而非 View + overflow:weapp WebView 渲染器会拦截 View 上的触摸手势,
   CSS overflow 滚动在真机与模拟器均无法拖动。 */
.temp-fragrance-scroll {
  position: relative;
  z-index: 2;
  display: flex;
  justify-content: space-between;
  gap: 16px;
  margin-top: 16px;
  width: 100vw; /* 滑动区铺满整屏宽 */
  height: 240px; /* 与卡片等高,避免 scroll-view 高度塌陷 */
  margin-top: 8px;
  /* 左移 48px 抵消 .calling-car-content-box 的左内边距,使滑动区左边界对齐屏幕左边缘,
     卡片可一直滑到屏幕两侧边缘(首/末卡片的 48px 视觉留白由内部轨道的 padding 承担)。
     已显式指定 width: 100vw,故无需再写 margin-right(会因过约束被忽略)。 */
  margin-left: -48px;
  white-space: nowrap;
  /* 隐藏横向滚动条(show-scrollbar 仅 enhanced 模式生效,这里用 CSS 兜底) */
  &::-webkit-scrollbar,
  ::-webkit-scrollbar {
    display: none;
    width: 0;
    height: 0;
  }
}
/* 滑动轨道:宽度由卡片内容决定 */
.temp-fragrance-control {
  display: inline-flex;
  flex-wrap: nowrap;
  align-items: flex-start;
  gap: 24px;
  /* 静止时首卡对齐 48px 内容左边界;滑到底时末卡右侧同样保留 48px 留白 */
  padding-left: 48px;
  padding-right: 48px;
  /* 卡片不参与压缩,保证各卡片保持设计稿宽度并整体溢出滚动 */
  > .ac-control,
  > .music-control,
  > .fragrance-control {
    flex-shrink: 0;
  }
}
/* 空调/香氛入口卡片开关 loading 旋转动画 */
@@ -1118,9 +1153,9 @@
  flex-shrink: 0;
  position: relative;
  box-sizing: border-box;
  width: 312px;
  width: 304px; /* 设计稿 152px(375 设计宽)× 2 */
  height: 240px;
  padding: 24px;
  padding: 8px 24px 24px;
  border-radius: 24px;
  overflow: hidden;
  display: flex;
@@ -1150,8 +1185,8 @@
  .tf-card-header-icon {
    flex-shrink: 0;
    width: 28px;
    height: 28px;
    width: 32px;
    height: 32px;
    margin-right: 8px;
  }
src/robotaxi/pages/begin-route/components/css-folder/cancel-popup.less
@@ -12,7 +12,7 @@
        left: 0;
        right: 0;
        bottom: 0;
        background: rgba(0, 0, 0, 0.5);
        background: rgba(0, 0, 0, 0.8);
    }
    .cancel-content {
@@ -105,10 +105,14 @@
                line-height: 48px;
                letter-spacing: normal;
                color: #141E27;
                font-weight: 500;
                .cancel-site-box-divider {
                  margin: 34px 32px 40px 0px;
                  border-top: 1px solid rgba(204, 208, 215, 0.25);
                }
                .cancel-site-box-content-name {
                  font-weight: 500;
                }
            }
        }
src/robotaxi/pages/begin-route/components/css-folder/credit-pay-popup.less
@@ -7,7 +7,7 @@
.credit-pay-mask {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.45);
  background: rgba(0, 0, 0, 0.8);
}
.credit-pay-content {
src/robotaxi/pages/begin-route/components/css-folder/evaluate-popup.less
@@ -12,7 +12,7 @@
        left: 0;
        right: 0;
        bottom: 0;
        background: rgba(0, 0, 0, 0.5);
        background: rgba(0, 0, 0, 0.8);
    }
    .evaluate-content {
@@ -68,7 +68,7 @@
        }
        .evaluate-rating-switcher {
            padding: 0 49px;
            display: flex;
            flex-direction: column;
@@ -257,7 +257,7 @@
                    flex-direction: column;
                    align-items: center;
                    justify-content: center;
                    background: rgba(0, 0, 0, 0.4);
                    background: rgba(0, 0, 0, 0.8);
                    color: #FFF;
                    gap: 8px;
src/robotaxi/pages/begin-route/components/css-folder/fragrance-control.less
@@ -11,56 +11,8 @@
  }
}
/* 香氛入口卡片 tf-card 样式(自包含,迁移自 temperature-fragrance-control.less) */
/* 香氛入口卡片:tf-card 基础样式(盒模型 + header/footer 等)统一由 calling-car.less 的全局 .tf-card 提供,此处只保留香氛专属样式 */
.fragrance-control {
  .tf-card {
    flex-shrink: 0;
    position: relative;
    box-sizing: border-box;
    width: 312px;
    height: 240px;
    padding: 24px;
    border-radius: 24px;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    background-repeat: no-repeat;
    background-position: center;
    background-size: 100% 100%;
  }
  .tf-card-bottom {
    display: flex;
    flex-direction: column;
  }
  .tf-card-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
  }
  .tf-card-header-left {
    display: flex;
    align-items: center;
  }
  .tf-card-header-icon {
    flex-shrink: 0;
    width: 28px;
    height: 28px;
    margin-right: 8px;
  }
  .tf-card-header-title {
    font-size: 24px;
    font-weight: 500;
    line-height: normal;
    letter-spacing: normal;
    color: @black-text-main;
  }
  /* 香氛名称,大小类似温度显示 */
  .tf-card-fragrance-name {
    font-size: 40px;
@@ -71,25 +23,6 @@
    white-space: nowrap;
    text-overflow: ellipsis;
    text-align: left;
  }
  .tf-card-footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
  }
  .tf-card-footer-text {
    font-size: 20px;
    line-height: normal;
    letter-spacing: normal;
    color: rgba(20, 30, 39, 0.5);
  }
  .tf-card-footer-arrow {
    flex-shrink: 0;
    width: 20px;
    height: 20px;
  }
  /* 香氛入口卡片:背景随开关状态切换 */
@@ -132,7 +65,7 @@
      left: 0;
      width: 100%;
      height: 100%;
      background: rgba(0, 0, 0, 0.4);
      background: rgba(0, 0, 0, 0.8);
      z-index: 102;
    }
@@ -185,7 +118,8 @@
    .popup-scent-list {
      display: flex;
      align-items: stretch;
      justify-content: space-between;
      justify-content: center;
      gap: 48px;
      // 香型卡片:竖排香型名 + 香调描述,背景使用设计切图(选中/未选)
      .popup-scent-item {
src/robotaxi/pages/begin-route/components/css-folder/music-control.less
New file
@@ -0,0 +1,271 @@
@import '../../../../style/ui-variables.less';
// 曲风磁贴下发 loading 旋转动画
@keyframes music-genre-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}
// 副标题上下滚动轮播:5 条文案 + 末尾克隆首条 = 轨道共 6 条,单条行高 24px
// 位移量必须用「相对轨道自身高度」的百分比而非 px:px 经 pxtransform 变成 rpx 后,
// 小程序会对每个 rpx 值独立换算成物理像素并取整,条目高度(24rpx)与位移量(96rpx/120rpx)
// 的取整结果不再是整数倍,误差逐条累计 —— 表现为滚到最后一条时露出下一条顶部的少量文字,
// 且回环瞬间克隆条与首条位置错开、出现上下抖动。
// 百分比按轨道实际渲染高度解析,1 步恒等于 1 条(100% / 6 ≈ 16.66667%),不会累计误差。
@keyframes music-subtitle-roll {
  0%, 16% {
    transform: translateY(0);
  }
  20%, 36% {
    transform: translateY(-16.66667%);
  }
  40%, 56% {
    transform: translateY(-33.33333%);
  }
  60%, 76% {
    transform: translateY(-50%);
  }
  80%, 96% {
    transform: translateY(-66.66667%);
  }
  100% {
    transform: translateY(-83.33333%);
  }
}
/* 音乐入口卡片:tf-card 基础样式(盒模型 + header/footer 等)统一由 calling-car.less 的全局 .tf-card 提供,此处只保留音乐专属样式 */
.music-control {
  /* 音符图标(暂用字形占位,待设计切图后替换为 <Image>) */
  .tf-card-header-note {
    flex-shrink: 0;
    width: 28px;
    height: 28px;
    margin-right: 8px;
    font-size: 28px;
    line-height: 28px;
    text-align: center;
    color: @black-text-main;
  }
  /* 曲风名称,大小类似温度/香氛名 */
  .tf-card-music-name {
    font-size: 40px;
    font-weight: normal;
    line-height: 64px;
    color: @black-text-main;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
    text-align: left;
  }
  // 副标题上下滚动轮播:仅露出一行(24px),轨道竖排承载多条文案
  .tf-card-subtitle {
    flex: 1;
    min-width: 0;
    height: 24px;
    overflow: hidden;
  }
  .tf-card-subtitle-track {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    animation: music-subtitle-roll 25s ease infinite;
  }
  .tf-card-subtitle-item {
    flex-shrink: 0; // 锁定单条高度,避免被父级压缩后「1 步 = 1 条」的对应关系被破坏
    height: 24px;
    line-height: 24px;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
  }
  /* 音乐入口卡片:背景随开关状态切换 */
  .tf-card-music {
    &.is-open {
      background-image: url('https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/genre-close-0731@2x.png');
    }
    &.is-close {
      background-image: url('https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/genre-open-0731@2x.png');
      // 关闭态卡片文字统一弱化
      .tf-card-header-note,
      .tf-card-header-title,
      .tf-card-music-name,
      .tf-card-footer-text {
        color: rgba(20, 30, 39, 0.3);
      }
    }
  }
  // 底部弹窗面板
  .music-popup {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 1000;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    pointer-events: none;
    &.show {
      pointer-events: auto;
    }
    .popup-mask {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: rgba(0, 0, 0, 0.4);
      z-index: 102;
    }
    .popup-content {
      position: relative;
      z-index: 103;
      background: #F1F5FA;
      border-radius: 60px 60px 0 0;
      width: 100%;
      padding: 0 0 calc(48px + env(safe-area-inset-bottom));
      box-sizing: border-box;
      display: flex;
      flex-direction: column;
      transform: translateY(100%);
    }
    .popup-header {
      height: 112px;
      padding-top: 8px;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
      flex-shrink: 0;
      .popup-title {
        font-size: 32px;
        font-weight: 500;
        line-height: 32px;
        color: rgba(0, 0, 0, 0.9);
      }
      .popup-close {
        position: absolute;
        right: 32px;
        top: 50%;
        transform: translateY(-50%);
        width: 40px;
        height: 40px;
      }
    }
    // 曲风磁贴区(每行 3 个自动换行)
    .popup-genre-area {
      margin-top: 24px;
      padding: 0 40px;
    }
    .popup-genre-list {
      display: flex;
      flex-wrap: wrap;
      gap: 22px;
    }
    // 曲风磁贴
    .popup-genre-item {
      position: relative;
      box-sizing: border-box;
      width: calc((100% - 22px) / 2);
      height: 152px;
      flex-shrink: 0;
      border-radius: 32px;
      display: flex;
      align-items: center;
      justify-content: center;
      background-repeat: no-repeat;
      background-position: center;
      background-size: 100% 100%;
      background-image: url('https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/genre-item-close-0731@2x.png');
      // 下发中 loading 图标(叠加于磁贴上方,水平居中,旋转)
      .popup-genre-loading {
        //position: absolute;
        //top: 50%;
        //left: 50%;
        width: 64px;
        height: 64px;
        //transform: translate(-50%, -50%);
        //z-index: 1;
        animation: music-genre-spin 1s linear infinite;
      }
      .popup-genre-name {
        font-size: 28px;
        font-weight: 500;
        line-height: 40px;
        color: rgba(20, 30, 39, 0.3);
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
      }
      // 选中态:高亮描边 + 主题渐变底
      &.active {
        background-image: url('https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/genre-item-open-0731@2x.png');
        .popup-genre-name {
          color: #fff;
        }
      }
      // 下发中禁止重复点击
      &.disabled {
        opacity: 0.6;
      }
    }
    .popup-genre-empty {
      padding: 96px 0;
      text-align: center;
      font-size: 28px;
      line-height: 40px;
      color: rgba(0, 0, 0, 0.4);
    }
    // 底部播放开关行
    .popup-play-switch {
      margin-top: 48px;
      padding: 0 48px;
      display: flex;
      align-items: center;
      justify-content: center;
      position: relative;
      .popup-play-text {
        font-size: 24px;
        line-height: 40px;
        color: rgba(20, 30, 39, 0.5);
      }
      .ac-switch {
        position: absolute;
        bottom: -12px;
        right: 40px;
      }
    }
  }
}
src/robotaxi/pages/begin-route/components/css-folder/no-route-popup.less
@@ -14,7 +14,7 @@
        left: 0;
        right: 0;
        bottom: 0;
        background: rgba(0, 0, 0, 0.6);
        background: rgba(0, 0, 0, 0.8);
    }
    .no-route-content {
src/robotaxi/pages/begin-route/components/css-folder/over-route.less
@@ -15,7 +15,7 @@
        left: 0;
        right: 0;
        bottom: 0;
        background: rgba(0, 0, 0, 0.5);
        background: rgba(0, 0, 0, 0.8);
    }
    .over-content {
src/robotaxi/pages/begin-route/components/css-folder/passenger-notice.less
@@ -60,6 +60,7 @@
                height: 100%;
                overflow-y: scroll;
                margin-top: 0;
                padding-right: 8px;
            }
            .passenger-content-desc-inner {
src/robotaxi/pages/begin-route/components/css-folder/pickup-exception-popup.less
@@ -1,3 +1,5 @@
@import '../../../../style/ui-variables.less';
.pickup-exception-popup {
  position: fixed;
  inset: 0;
@@ -7,7 +9,7 @@
.pickup-exception-mask {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.6);
  background: rgba(0, 0, 0, 0.8);
}
.pickup-exception-content {
@@ -15,9 +17,9 @@
  left: 0;
  right: 0;
  bottom: 0;
  background: #ffffff;
  border-radius: 32px 32px 0 0;
  padding: 48px 40px 80px;
  background: @gary-theme;
  border-radius: 60px 60px 0 0;
  padding: 48px 48px 80px 48px;
  box-sizing: border-box;
}
@@ -39,7 +41,7 @@
  font-weight: 500;
  line-height: 44px;
  letter-spacing: normal;
  color: rgba(0, 0, 0, 0.9);
  color: @black-text-main;
}
.pickup-exception-desc {
@@ -48,7 +50,7 @@
  font-weight: normal;
  letter-spacing: normal;
  /* 文字色/黑色/辅助文字 40% */
  color: rgba(0, 0, 0, 0.4);
  color: @black-text-04;
  margin-top: 8px;
}
@@ -56,9 +58,9 @@
  position: relative;
  width: 100%;
  height: 358px;
  border-radius: 16px;
  border-radius: 32px;
  overflow: hidden;
  background: #D8D8D8;
  background: @gary-theme;
  margin-top: 48px;
}
@@ -86,7 +88,7 @@
.pickup-exception-message {
  margin-top: 32px;
  color: rgba(0, 0, 0, 0.9);
  color: @black-text-main;
  font-size: 28px;
  line-height: 44px;
  text-align: center;
@@ -100,9 +102,8 @@
  line-height: 88px;
  text-align: center;
  color: #ffffff;
  border-radius: 16px;
  background: linear-gradient(96deg, #8475FF 1%, #7DD6FF 103%);
  border-radius: 2000px;
  background: @black-text-main;
  font-size: 28px;
  font-weight: 500;
  border-radius: 16px;
}
src/robotaxi/pages/begin-route/components/css-folder/seat-hv-control.less
@@ -38,13 +38,13 @@
  // 二排(目前唯一可控排):按钮位于座舱图中部横排,左右座椅中心分别约 28% / 72%
  .seat-hv-node-SECOND_ROW_LEFT {
    top: 45%;
    left: 28.5%;
    top: 29%;
    left: 22.5%;
  }
  .seat-hv-node-SECOND_ROW_RIGHT {
    top: 45%;
    left: 71.5%;
    top: 29%;
    left: 77.5%;
  }
  // 三排:纵向约 60%,左右座椅中心分别约 28% / 72%
@@ -90,6 +90,7 @@
  .seat-hv-btn {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
@@ -98,6 +99,13 @@
      height: 48px;
    }
    &-label {
      font-size: 20px;
      font-weight: normal;
      line-height: 34px;
      color: #A1A1A1;
    }
    // 下发中 loading 图标旋转(点击后至指令返回)
    &-loading {
      opacity: 1;
src/robotaxi/pages/begin-route/components/css-folder/temperature-control.less
@@ -26,7 +26,7 @@
      left: 0;
      width: 100%;
      height: 100%;
      background: rgba(0, 0, 0, 0.4);
      background: rgba(0, 0, 0, 0.8);
      z-index: 102;
    }
src/robotaxi/pages/begin-route/components/debug-order-simulator-panel.tsx
@@ -166,6 +166,45 @@
    }, 300);
  };
  /**
   * 立即置为接驾态并暂停:一键把页面冻结在「车辆即将到达(4)」态,便于纯静态调 UI/样式。
   *
   * 原理:新建模拟器后调用 start(),其内部 enterApproaching() 会同步注入
   * order_status(4) + 全量接驾路线事件;随即调用 stop() 清除定时器,
   * 使车辆停在接驾路线起点、剩余路线全量绘制,画面固定在接驾态不再向后推进。
   */
  const handleFreezeApproaching = () => {
    // 1) 绑定 socket 业务监听,确保注入事件能被消费(无真实连接也可)
    props.bindSocketListeners();
    // 2) 确保订单 ID 一致(守卫 isSameOrder)
    const orderId = resolveOrderId();
    ensureSimulationOrderId(orderId);
    props.setOrderId(orderId);
    // 3) 由上/下车点生成双腿路线
    const { approachRoute, serviceRoute } = buildDefaultRoutes(props.startSite, props.endSite);
    const preset = SPEED_PRESETS[speedKey];
    // 4) 新建模拟器,start() 后立即 stop(),冻结在接驾态初始帧
    simulatorRef.current?.reset();
    const simulator = new OrderFlowSimulator({
      orderId,
      approachRoute,
      serviceRoute,
      tickIntervalMs: preset.tickIntervalMs,
      pointsPerTick: preset.pointsPerTick,
      onState: (state) => setSimState(state),
    });
    simulatorRef.current = simulator;
    setRunning(false);
    // 延迟一帧等待订单 ID 写入页面 deps,再进入接驾态并立即暂停
    setTimeout(() => {
      simulator.start(); // enterApproaching:注入 status(4) + 全量接驾路线
      simulator.stop(); // 立即停止推进,冻结在接驾态
    }, 300);
  };
  /** 停止模拟(保留当前画面)。 */
  const handleStop = () => {
    simulatorRef.current?.stop();
@@ -457,6 +496,14 @@
      </View>
      <View style={{ display: 'flex', marginBottom: '4px' }}>
        <View
          onClick={handleFreezeApproaching}
          style={buttonStyle('#1d9bf0')}
        >
          接驾态并暂停(4)
        </View>
      </View>
      <View style={{ display: 'flex', marginBottom: '4px' }}>
        <View
          onClick={handleJumpToTripFinished}
          style={buttonStyle('#0e9f6e')}
        >
src/robotaxi/pages/begin-route/components/pickup-exception-popup.tsx
@@ -117,7 +117,7 @@
    if (Array.isArray(linePoints) && linePoints.length > 1) {
      nextPolylines.push({
        points: linePoints,
        color: '#7B91FF',
        color: '#DFB369',
        width: 6,
        dottedLine: true,
        arrowLine: false
src/robotaxi/pages/begin-route/components/release-route.tsx
@@ -10,6 +10,7 @@
    callImmediately: () => void;
    onSelectSite?: (pageType: 'start' | 'end') => void;
    fareEstimate?: any;
    manuallySelectedCouponName?: string;
    onOpenCouponSelect?: () => void;
    handleOpenPassengerNotice?: () => void;
    walkingTime?: number;
@@ -21,7 +22,7 @@
    handleSelectRoute?: (index: number) => void;
};
export default function ReleaseRoute({ startSite, endSite, callImmediately, onSelectSite, fareEstimate = {}, onOpenCouponSelect, handleOpenPassengerNotice, walkingTime = 0, isCallDisabled = false, isRouteSelectExpanded = false, onToggleRouteSelect, routeOptions = [], selectedRouteIndex = 0, handleSelectRoute }: Props) {
export default function ReleaseRoute({ startSite, endSite, callImmediately, onSelectSite, fareEstimate = {}, manuallySelectedCouponName = '', onOpenCouponSelect, handleOpenPassengerNotice, walkingTime = 0, isCallDisabled = false, isRouteSelectExpanded = false, onToggleRouteSelect, routeOptions = [], selectedRouteIndex = 0, handleSelectRoute }: Props) {
    const [boxHeight, setBoxHeight] = useState<number | null>(null);
    const [bottomPaddingHeight, setBottomPaddingHeight] = useState<number | null>(null);
    const startY = useRef(0);
@@ -43,6 +44,11 @@
        }
        return `${parseFloat(discountAmount.toFixed(2))}元`;
    };
    const discountTitleText = manuallySelectedCouponName || '最佳优惠';
    const discountDescText = manuallySelectedCouponName
        ? `已选择${manuallySelectedCouponName}`
        : '已选择最佳优惠';
    useEffect(() => {
        if (!isRouteSelectExpanded && boxHeight !== null && minHeight.current > 0) {
@@ -256,8 +262,8 @@
                          onClick={onOpenCouponSelect}
                        >
                            <View className="flex-center">
                                <View className="discount-amount-text">最佳优惠</View>
                                <View className="discount-amount-desc" >已为您选择最佳优惠</View>
                                <View className="discount-amount-text">{discountTitleText}</View>
                                <View className="discount-amount-desc" >{discountDescText}</View>
                            </View>
                            <View className="flex-center">
                                <View className="discount-amount-num">-{formatDiscountAmountText()}</View>
src/robotaxi/pages/begin-route/components/route-preference-popup/index.less
@@ -21,7 +21,7 @@
        left: 0;
        width: 100%;
        height: 100%;
        background: rgba(0, 0, 0, 0.4);
        background: rgba(0, 0, 0, 0.8);
        z-index: 102;
    }
@@ -96,4 +96,4 @@
            }
        }
    }
}
}
src/robotaxi/pages/begin-route/hooks/use-map-markers.ts
@@ -20,7 +20,7 @@
  color: "#141E27",
  fontSize: 12,
  anchorX: 0,
  anchorY: 9, // 图标下方 9px
  anchorY: 7, // 图标下方 5px
  textAlign: "center" as const,
  bgColor: "#00000000",
  fontWeight: 'bold',
@@ -28,12 +28,12 @@
  borderRadius: 0,
  padding: 0
};
// 到达上车点/待支付/完成态:站点名称在图标下方 8px,11px #141E27 居中
// 到达上车点/待支付/完成态:站点名称在图标下方 7px,11px #141E27 居中
const SITE_NAME_LABEL_BELOW_LG = {
  color: "#141E27",
  fontSize: 12,
  anchorX: 0,
  anchorY: 8,
  anchorY: 7,
  textAlign: "center" as const,
  bgColor: "#00000000",
  fontWeight: 'bold',
src/robotaxi/pages/begin-route/hooks/use-page-actions.ts
@@ -25,6 +25,8 @@
  onPublishRouteSelectSite?: (pageType: 'start' | 'end') => void;
};
const PENDING_SELECTED_COUPON_ID_STORAGE_KEY = 'pendingSelectedCouponId';
/**
 * 页面动作编排:统一处理服务入口、页面跳转、地图气泡点击行为。
 */
@@ -99,6 +101,7 @@
  const toBack = () => {
    deps.onBeforeBack?.();
    Taro.removeStorageSync('selectedCouponId');
    Taro.removeStorageSync(PENDING_SELECTED_COUPON_ID_STORAGE_KEY);
    clearModifyDestinationStorage();
    clearPassedRouteCache();
    if (deps.tripStatus === TripStatus.PaymentSuccess || deps.tripStatus === TripStatus.PublishRoute) {
src/robotaxi/pages/begin-route/hooks/use-page-restore.ts
@@ -17,6 +17,8 @@
/** 已选优惠券缓存键。 */
const SELECTED_COUPON_ID_STORAGE_KEY = 'selectedCouponId';
/** 优惠券列表页“去使用”待消费缓存键。 */
const PENDING_SELECTED_COUPON_ID_STORAGE_KEY = 'pendingSelectedCouponId';
type RequestRouteOptions = {
  startPointId?: string | number;
@@ -89,6 +91,13 @@
  };
  /**
   * 清理优惠券列表页“去使用”的待消费缓存。
   */
  const clearPendingSelectedCouponId = () => {
    Taro.removeStorageSync(PENDING_SELECTED_COUPON_ID_STORAGE_KEY);
  };
  /**
   * 归一化已选优惠券 ID,过滤空值和脏缓存值。
   */
  const normalizeSelectedCouponId = () => {
@@ -101,13 +110,42 @@
  };
  /**
   * 归一化优惠券列表页“去使用”的待消费券 ID。
   */
  const normalizePendingSelectedCouponId = () => {
    const rawCouponId = Taro.getStorageSync(PENDING_SELECTED_COUPON_ID_STORAGE_KEY);
    const normalizedCouponId = String(rawCouponId ?? '').trim();
    if (!normalizedCouponId || ['null', 'undefined'].includes(normalizedCouponId)) {
      return '';
    }
    return normalizedCouponId;
  };
  /**
   * 进入预估态前处理优惠券缓存:
   * 无订单时校验券状态,行程中进入页面则直接清空。
   */
  const prepareSelectedCouponForEstimate = async (cachedOrderId: string, cachedTripStatus: TripStatus) => {
    const cachedCouponId = normalizeSelectedCouponId();
    let cachedCouponId = normalizeSelectedCouponId();
    const rawCouponId = Taro.getStorageSync(SELECTED_COUPON_ID_STORAGE_KEY);
    const rawPendingCouponId = Taro.getStorageSync(PENDING_SELECTED_COUPON_ID_STORAGE_KEY);
    const pendingCouponId = normalizePendingSelectedCouponId();
    const canConsumePendingCoupon = cachedTripStatus === TripStatus.PublishRoute && !cachedOrderId;
    // 优惠券列表页“去使用”的待消费券进入确认行程页后,应只消费一次。
    // 只要存在有效 pending 值,就由它决定本次无订单预估态使用的券,并立即清空 pending,
    // 避免旧 selectedCouponId 抢占优先级或 pending 残留到后续页面恢复中再次被消费。
    if (pendingCouponId) {
      if (canConsumePendingCoupon) {
        Taro.setStorageSync(SELECTED_COUPON_ID_STORAGE_KEY, pendingCouponId);
        cachedCouponId = pendingCouponId;
      }
      clearPendingSelectedCouponId();
    } else if (rawPendingCouponId !== '' && rawPendingCouponId !== undefined && rawPendingCouponId !== null) {
      clearPendingSelectedCouponId();
    }
    if (!cachedCouponId) {
      const rawCouponId = Taro.getStorageSync(SELECTED_COUPON_ID_STORAGE_KEY);
      if (rawCouponId !== '' && rawCouponId !== undefined && rawCouponId !== null) {
        clearSelectedCouponId();
      }
@@ -126,6 +164,7 @@
    ].includes(cachedTripStatus);
    if (cachedOrderId && isTripOrderStatus) {
      clearSelectedCouponId();
      clearPendingSelectedCouponId();
      return;
    }
src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx
@@ -71,6 +71,8 @@
  handleEstimateSelectSite: (pageType: "start" | "end") => void;
  /** 费用信息 */
  fareEstimate: any;
  /** 预估态用户手动切换后的优惠券名称 */
  manuallySelectedCouponName: string;
  /** 打开优惠券选择弹窗 */
  handleOpenCouponSelect: () => void;
  /** 打开乘客须知 */
@@ -784,12 +786,12 @@
                    }`.trim()}
                  style={{ minWidth: "40px", justifyContent: "center", zIndex: 2 }}
                >
                  {!deps.hasBeforeUserEndSite && (
                  {deps.hasBeforeUserEndSite && (
                    <CoverView className="car-time car-before-order-tip">
                      即将完成上一单
                    </CoverView>
                  )}
                  {!deps.hasBeforeUserEndSite ? (
                  {deps.hasBeforeUserEndSite ? (
                    <CoverView className="car-before-order-tip-text">
                      <CoverView>
                        全程
@@ -954,6 +956,7 @@
          callImmediately={deps.callImmediately}
          onSelectSite={deps.handleEstimateSelectSite}
          fareEstimate={deps.fareEstimate}
          manuallySelectedCouponName={deps.manuallySelectedCouponName}
          onOpenCouponSelect={deps.handleOpenCouponSelect}
          handleOpenPassengerNotice={deps.handleOpenPassengerNotice}
          walkingTime={deps.walkingTime}
@@ -1055,6 +1058,7 @@
          pickupCountdownSeconds={pickupCountdownSeconds}
          finishCountdownSeconds={finishCountdownSeconds}
          fareBreakdown={fareBreakdown}
          manuallySelectedCouponName={deps.manuallySelectedCouponName}
          evaluateSubmitVersion={deps.evaluateSubmitVersion}
          evaluateCloseVersion={deps.evaluateCloseVersion}
          evaluatePopupStatus={deps.evaluatePopupStatus}
src/robotaxi/pages/begin-route/index.less
@@ -224,14 +224,13 @@
    width: 24px;
    height: 24px;
    display: block;
    margin-right: 8px;
    margin-right: 4px;
}
.site-countdown-time {
    font-family: @font-xpsans-bold;
    font-size: 24px;
    font-weight: normal;
    line-height: 32px;
    text-align: center;
    letter-spacing: 0.04em;
    color: #FFD181;
src/robotaxi/pages/begin-route/index.tsx
@@ -61,11 +61,11 @@
// 蓝牙车控连接与鉴权编排
import useBluetoothCarControl from './hooks/use-bluetooth-car-control';
import { toTripStatus, TripStatus } from '../../constants/trip-status';
import { MODIFY_DESTINATION_SOCKET_STATUS_KEY } from '../../constants/modify-destination';
import { MODIFY_DESTINATION_SOCKET_STATUS_KEY, MODIFY_DESTINATION_SESSION_KEY } from '../../constants/modify-destination';
import { cancelOrder, selectNavigationRoute, getUpdateAddressNum } from '../../api/begin-route';
import { getAddressByLocation, getDistance, reportEvent } from '../../../utils/Robotaxi-public-method';
import { CurrentUserLocation, RouteDistanceValue } from './hooks/use-route-manager.shared';
import { MODIFY_DESTINATION_SESSION_KEY } from '../../constants/modify-destination';
// import { MODIFY_DESTINATION_SESSION_KEY } from '../../constants/modify-destination';
import { getOrderId } from '../../utils/order';
import { getOrderDetail } from '../../api/order';
@@ -315,6 +315,8 @@
  const [evaluateCloseVersion, setEvaluateCloseVersion] = useState(0); // 评价关闭版本号
  const [evaluateOrderRating, setEvaluateOrderRating] = useState(1); // 当前评价弹窗对应评分
  const [showCouponSelectPopup, setShowCouponSelectPopup] = useState(false); // 优惠券选择弹窗展示状态
  const [defaultPreviewCouponId, setDefaultPreviewCouponId] = useState(''); // 预估态默认命中的优惠券 ID
  const [manuallySelectedCouponName, setManuallySelectedCouponName] = useState(''); // 当前流程内用户手动切换后的优惠券名称
  const [orderDetailViewportRefreshVersion, setOrderDetailViewportRefreshVersion] = useState(0); // 完成态视口刷新版本,关闭优惠券弹窗后触发 includePoints 纠偏
  const [showModifyDestinationFailPopup, setShowModifyDestinationFailPopup] = useState(false); // 修改目的地失败弹窗
  const [modifyDestinationFailMessage, setModifyDestinationFailMessage] = useState(''); // 修改目的地失败文案(默认空,避免闪文案)
@@ -377,6 +379,7 @@
  const lastWaitingForPickupPageViewOrderIdRef = useRef(''); // 等待接驾页页面浏览埋点最近一次上报的订单 ID
  const handledPickupExceptionSignatureRef = useRef(''); // 已处理接驾异常签名,避免同一异常重复弹窗
  const previousOrderIdRef = useRef(''); // 上一个订单 ID,用于跨单清理异常弹窗状态
  const pendingFareCouponSelectionRef = useRef({ couponId: '', couponName: '' }); // 待支付态最近一次手选券,待账单返回后校正名称
  const confirmRideEventSnapshotRef = useRef({
    startSite: null as any,
    endSite: null as any,
@@ -2287,9 +2290,22 @@
      setShowCouponSelectPopup(false);
      return;
    }
    const selectedCouponName = String(item?.title || item?.couponName || '').trim();
    if (tripStatus === TripStatus.PublishRoute) {
      setManuallySelectedCouponName(
        selectedCouponId === defaultPreviewCouponId ? '' : selectedCouponName
      );
      pendingFareCouponSelectionRef.current = { couponId: '', couponName: '' };
    }
    Taro.setStorageSync('selectedCouponId', selectedCouponId);
    setShowCouponSelectPopup(false);
    if ([TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(tripStatus)) {
      // 待支付态先即时回显用户刚选择的券名,接口返回后再按实际生效券校正。
      pendingFareCouponSelectionRef.current = {
        couponId: selectedCouponId,
        couponName: selectedCouponName
      };
      setManuallySelectedCouponName(selectedCouponName);
      setPriceUpdateVersion(v => v + 1);
      return;
    }
@@ -2310,6 +2326,17 @@
  const activeCouponId = String( // 优惠券弹窗默认选中券ID
    fareEstimate?.couponId || fareEstimate?.userCouponId || Taro.getStorageSync('selectedCouponId') || ''
  );
  useEffect(() => {
    if (tripStatus !== TripStatus.PublishRoute) {
      return;
    }
    if (manuallySelectedCouponName) {
      return;
    }
    setDefaultPreviewCouponId(String(
      fareEstimate?.couponId || fareEstimate?.userCouponId || ''
    ).trim());
  }, [tripStatus, fareEstimate?.couponId, fareEstimate?.userCouponId, manuallySelectedCouponName]);
  const canWalkNavigateToPickup = useMemo(() => {
    if (isModifyDestinationReviewVisible) {
      return false;
@@ -2413,6 +2440,7 @@
    isWaitingSignedCreditPayResult,
    handleEstimateSelectSite,
    fareEstimate,
    manuallySelectedCouponName,
    handleOpenCouponSelect,
    handleOpenPassengerNotice,
    walkingTime,
@@ -2480,6 +2508,23 @@
  const couponOrderMoney = tripStatus === TripStatus.TripFinished
    ? (Number(fareBreakdown?.orderMoney) || 0)
    : (Number(fareEstimate?.orderMoney) || 0);
  useEffect(() => {
    if (![TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(tripStatus)) {
      return;
    }
    const pendingSelection = pendingFareCouponSelectionRef.current;
    if (!pendingSelection.couponId) {
      return;
    }
    const resolvedCouponId = String(fareBreakdown?.userCouponId || '').trim();
    // 账单返回后,以实际生效券为准:命中用户所选则保留名称,否则清空避免价格与名称不一致。
    if (resolvedCouponId && resolvedCouponId === pendingSelection.couponId) {
      setManuallySelectedCouponName(pendingSelection.couponName);
    } else {
      setManuallySelectedCouponName('');
    }
    pendingFareCouponSelectionRef.current = { couponId: '', couponName: '' };
  }, [tripStatus, fareBreakdown?.userCouponId]);
  // 渲染时优先使用 record*/set* 记录的最新值(ref),ref 为空再回退到 state,保证下次渲染拿到最新值
  // 【修复频繁切换目的地后地图闪现北京故宫的问题】
  // 当页面重新挂载且定位接口(getLocation)还未返回时,longitude/latitude 状态为 null。
@@ -2648,7 +2693,8 @@
        onConfirm={handleConfirmCreditPayPopup}
      />
      <TripFinishedFireworks tripStatus={tripStatus} />
      {/* <DebugOrderSimulatorPanel
      {/* 模拟器入口已关闭,如需本地调试可取消下方注释
      <DebugOrderSimulatorPanel
        orderId={orderId}
        setOrderId={setOrderId}
        setTripStatus={setTripStatus}
@@ -2659,7 +2705,8 @@
        setShowCancelPopup={setShowCancelPopup}
        setShowModifyDestinationFailPopup={setShowModifyDestinationFailPopup}
        setModifyDestinationFailMessage={setModifyDestinationFailMessage}
      /> */}
      />
      */}
    </>
  )
}
src/robotaxi/pages/coupon/index.less
@@ -206,7 +206,7 @@
      left: 0;
      right: 0;
      bottom: 0;
      background: rgba(0, 0, 0, 0.5);
      background: rgba(0, 0, 0, 0.8);
    }
    .privacy-content {
src/robotaxi/pages/coupon/index.tsx
@@ -2,6 +2,8 @@
import { useState, useEffect, useRef } from "react";
import { View, Text, Image, RichText, ScrollView } from "@tarojs/components";
import "./index.less";
const PENDING_SELECTED_COUPON_ID_STORAGE_KEY = 'pendingSelectedCouponId';
import {
  getCouponList,
  receiveCoupon,
@@ -175,7 +177,9 @@
  const toHome = (couponId?: string) => {
    if (couponId) {
      Taro.setStorageSync("selectedCouponId", couponId);
      // 优惠券列表页“去使用”仅表达一次待带入发单页的意图,
      // 不能污染发单/结算流程内部复用的 selectedCouponId。
      Taro.setStorageSync(PENDING_SELECTED_COUPON_ID_STORAGE_KEY, couponId);
    }
    Taro.reLaunch({
      url: "/pages/robotaxi/home/index"
src/robotaxi/pages/feedback/index.less
@@ -214,7 +214,7 @@
    align-items: center;
    justify-content: center;
    gap: 8px;
    background: rgba(0, 0, 0, 0.4);
    background: rgba(0, 0, 0, 0.8);
    color: #FFFFFF;
    &-text {
src/robotaxi/pages/operating-area/index.tsx
@@ -12,7 +12,7 @@
const DEFAULT_MAP_LONGITUDE = 116.397128;
const DEFAULT_MAP_LATITUDE = 39.916527;
const MAP_INCLUDE_POINTS_PADDING = [120, 10, 50, 10];
const MAP_INCLUDE_POINTS_PADDING = [120, 20, 160, 20];
// includePoints 缩放动画的预估时长,动画结束后再显示地图,避免“先展示再放大”的跳动
const MAP_FIT_ANIMATION_DURATION = 700;
const POLYGON_STROKE_COLOR = '#DFB369'; // rgb(223, 179, 105)
src/robotaxi/pages/order/detail/index.less
@@ -1,3 +1,5 @@
@import '../../../style/ui-variables.less';
.order-detail-page {
  background: #F2F3F7;
  min-height: 100vh;
@@ -6,21 +8,22 @@
  flex-direction: column;
  .car-info {
    padding: 18px 32px 32px 32px;
    border-radius: 24px;
    padding: 48px 48px 40px 48px;
    border-radius: 60px;
    background: #FFFFFF;
    .info-top {
      display: flex;
      justify-content: space-between;
      position: relative;
      .car-name {
        font-family: PingFang SC;
        font-size: 28px;
        font-weight: normal;
        font-weight: 500;
        line-height: 48px;
        letter-spacing: normal;
        color: rgba(0, 0, 0, 0.9);
        color: @black-text-main;
        margin-top: 14px;
      }
@@ -67,19 +70,21 @@
        font-size: 24px;
        font-variation-settings: "opsz" auto;
      }
    }
    .car-box {
      position: absolute;
      top: 50px;
      right: 52px;
      .car-img {
        width: 236px;
        height: 110px;
        margin-right: 4px;
        width: 272px;
        height: 234px;
      }
    }
    .evaluate-card {
      margin-top: 36px;
      margin-top: 32px;
      width: 638px;
      height: 248px;
      border-radius: 16px;
      border-radius: 40px;
      background: #F5F6FA;
      padding: 32px 28px 24px 30px;
@@ -90,7 +95,7 @@
        line-height: 40px;
        letter-spacing: normal;
        /* 文字色/黑色/重要字 90% */
        color: rgba(0, 0, 0, 0.9);
        color: @black-text-main;
        margin: 0px 0px 24px 2px;
      }
@@ -128,12 +133,12 @@
          text-align: center;
          letter-spacing: normal;
          /* 文字色/黑色/辅助文字 40% */
          color: rgba(0, 0, 0, 0.4);
          color: @black-text-04;
        }
        .active-text {
          /* 文字色/黑色/重要字 90% */
          color: rgba(0, 0, 0, 0.9);
          color: @black-text-main;
        }
      }
    }
@@ -141,9 +146,9 @@
  .trip-info {
    margin-top: 24px;
    border-radius: 20.29px;
    border-radius: 60px;
    background: #FFFFFF;
    padding: 32px 24px;
    padding: 48px 48px 40px 48px;
    .trip-top {
      display: flex;
@@ -160,7 +165,8 @@
          font-size: 48px;
          line-height: 40px;
          letter-spacing: normal;
          color: rgba(0, 0, 0, 0.9);
          color: @black-text-main;
          font-weight: 500;
        }
        .unit {
@@ -169,7 +175,7 @@
          font-weight: normal;
          line-height: 26px;
          letter-spacing: normal;
          color: rgba(0, 0, 0, 0.9);
          color: @black-text-main;
          margin: 0 4px;
        }
@@ -180,7 +186,7 @@
          line-height: 26px;
          letter-spacing: normal;
          text-decoration: line-through;
          color: rgba(0, 0, 0, 0.4);
          color: @black-text-04;
        }
      }
@@ -195,7 +201,7 @@
          line-height: 40px;
          letter-spacing: normal;
          /* 文字色/黑色/次要文字 60% */
          color: rgba(0, 0, 0, 0.6);
          color: @black-text-04;
          margin-right: 12px;
        }
@@ -224,7 +230,7 @@
        line-height: 44px;
        letter-spacing: normal;
        /* 文字色/黑色/重要字 90% */
        color: rgba(0, 0, 0, 0.9);
        color: @black-text-main;
        margin-bottom: 8px;
      }
@@ -234,10 +240,10 @@
        .line-icon {
          /* 自动布局子元素 */
          width: 8px;
          height: 8px;
          width: 10px;
          height: 10px;
          z-index: 0;
          background: #8CB654;
          background: @black-text-main;
          border-radius: 50%;
          margin-right: 16px;
          flex-shrink: 0;
@@ -250,15 +256,15 @@
          font-weight: normal;
          line-height: 40px;
          letter-spacing: normal;
          color: rgba(0, 0, 0, 0.4);
          color: @black-text-04;
        }
        .first-icon {
          background: #6D7FD9;
          background: @gold-light;
        }
      }
    }
  }
}
}
src/robotaxi/pages/order/detail/index.tsx
@@ -149,10 +149,10 @@
        id: 1,
        longitude: displayStartPoint.longitude,
        latitude: displayStartPoint.latitude,
        width: 20,
        height: 36,
        anchor: { x: 0.5, y: 1 },
        iconPath: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/start-text.png',
        width: 34,
        height: 46,
        anchor: { x: 0.7, y: 0.8 },
        iconPath: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/start-site-order-20260804-v2.png',
        viewportLongitude: startPoint?.longitude,
        viewportLatitude: startPoint?.latitude,
      } : null,
@@ -160,10 +160,10 @@
        id: 2,
        longitude: endPoint.longitude,
        latitude: endPoint.latitude,
        width: 20,
        height: 36,
        anchor: { x: 0.5, y: 1 },
        iconPath: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/end-text.png',
        width: 34,
        height: 46,
        anchor: { x: 0.7, y: 0.8 },
        iconPath: 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/end-site-order-20260804-v2.png',
      } : null
    ].filter(Boolean) as any[];
    setMarkers(markerList);
@@ -193,15 +193,15 @@
    }
    const polylineData = [{
    points: routePath,
    color: '#6D7BC1',
    width: 10,
    color: '#59B688',
    width: 7,
    dottedLine: false,
    arrowLine: false,
  },
  {
    points: routePath,
    color: '#7B91FF',
    width: 8,
    color: '#59B688',
    width: 7,
    dottedLine: false,
    arrowLine: true,
  }];
@@ -305,18 +305,22 @@
            orderData.vehicleId &&
            <View className='info-top'>
              <View>
                <View className='car-name'>{orderData.plateNumber}</View>
                <View className='car-name'>{orderData.plateNumber} · {orderData.vehicleColor}</View>
                <View className='info'>
                  <Text className='car-color'>小鹏GX</Text>
                  <Text className='info-line'></Text>
                  <Text className='car-color'>{orderData.vehicleColor}</Text>
                  {/* <Text className='car-code'>车身编码: </Text>
                  <Text className='code'>{orderData.vehicleCode}</Text> */}
                </View>
              </View>
              <Image src={safeParse(orderData.vehiclePictures, {})?.url || 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/default-vehicle-img-20260724-v2.png'} className='car-img' />
            </View>
          }
          {
            orderData.vehicleId && (
              <View className='car-box'>
                <Image src={safeParse(orderData.vehiclePictures, {})?.url || 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/default-vehicle-img-20260724-v2.png'} className='car-img' />
            </View>
            )}
          {
            orderData.status >= 9 &&
            <View className='evaluate-card'>
@@ -353,7 +357,7 @@
            </View>
            <View onClick={todescription} className='price-remark'>
              <Text className='remark-text'>费用说明</Text>
              <Image src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/right.png" className='remark-img' />
              <Image src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/text04-arrow-20260804.png" className='remark-img' />
            </View>
          </View>
        }
src/robotaxi/pages/select-map-site/hooks/use-select-map-site-walking-route.ts
@@ -51,7 +51,7 @@
      }
      deps.setPolyline([{
        points: displayPoints,
        color: '#6D7FD9',
        color: '#DFB369',
        width: deps.isAndroid ? 4 : 6,
        dottedLine: true,
        arrowLine: false,
src/robotaxi/pages/share-trip/hooks/use-map-markers.ts
@@ -4,7 +4,7 @@
// 接驾态(前往上车点)/到达终点态 上车点三角图标(与 begin-route 对齐)
const PICKUP_APPROACHING_ICON_URL =
    'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/up-triangle-2026022.png';
    'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/up-triangle-20260722.png';
// 送驾态(前往目的地)/到达终点态 下车点三角图标(与 begin-route 对齐)
const DROPOFF_INSERVICE_ICON_URL =
    'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/down-triangle-20260722.png';
@@ -13,7 +13,7 @@
    color: '#141E27',
    fontSize: 12,
    anchorX: 0,
    anchorY: 9, // 图标下方 9px
    anchorY: 7, // 图标下方 7px
    textAlign: 'center' as const,
    bgColor: '#00000000',
    fontWeight: 'bold',
@@ -26,7 +26,7 @@
    color: '#141E27',
    fontSize: 12,
    anchorX: 0,
    anchorY: 8,
    anchorY: 7,
    textAlign: 'center' as const,
    bgColor: '#00000000',
    fontWeight: 'bold',
@@ -136,6 +136,16 @@
        const isPickupArrivedDest = [TripStatus.ArrivedDestination, TripStatus.TripFinished, TripStatus.CancelOnlyPickup].includes(deps.tripStatus);
        const isPickupTriangle = isPickupApproaching || isPickupArrivedDest;
        const pickupNameLabelConfig = isPickupApproaching ? SITE_NAME_LABEL_BASE : SITE_NAME_LABEL_BELOW_LG;
        const isVehicleArrivedPickup = deps.tripStatus === TripStatus.VehicleArrivedPickup;
        const getCustomCalloutAnchorY = () => {
          if (isPickupApproaching)  {
            return 12;
          }
          if (isVehicleArrivedPickup && deps.isAndroid) {
            return 8;
          }
          return 6;
        }
        return {
            id: 1,
            latitude: pickupSite.latitude,
@@ -171,7 +181,7 @@
                  }
                : {
                      customCallout: {
                          anchorY: [TripStatus.Publishing, TripStatus.CallingCar].includes(deps.tripStatus) ? 150 : -5,
                          anchorY: getCustomCalloutAnchorY(),
                          anchorX: 0,
                          display: 'ALWAYS'
                      }
@@ -229,7 +239,7 @@
                  }
                : {
                      customCallout: {
                          anchorY: -5,
                          anchorY: 6,
                          anchorX: 0,
                          display: 'ALWAYS'
                      }
@@ -356,7 +366,6 @@
            deps.isAndroid && [
                TripStatus.PublishRoute,
                TripStatus.VehicleApproaching,
                TripStatus.VehicleArrivedPickup,
                TripStatus.TripFinished,
                TripStatus.PaymentSuccess,
                TripStatus.CancelOnlyPickup
src/robotaxi/pages/share-trip/hooks/use-view-delegates.tsx
@@ -84,7 +84,7 @@
      : durationDisplay
  ), [durationDisplay]);
  const shouldHideAndroidMarkerCallout = deps.isAndroid
    && [TripStatus.PublishRoute, TripStatus.VehicleArrivedPickup, TripStatus.TripFinished, TripStatus.PaymentSuccess, TripStatus.CancelOnlyPickup].includes(deps.tripStatus);
    && [TripStatus.PublishRoute, TripStatus.TripFinished, TripStatus.PaymentSuccess, TripStatus.CancelOnlyPickup].includes(deps.tripStatus);
  const androidCarCalloutContent = useMemo(() => (
    <AndroidCarCalloutContent
      routeDistance={deps.routeDistance}
src/robotaxi/pages/share-trip/index.less
@@ -226,8 +226,8 @@
  // 车型图:参考 calling-car 的 calling-car-vehicle-pic,浮于背景图顶部右侧
  .calling-car-vehicle-pic {
    position: absolute;
    top: 24px;
    right: 24px;
    top: -4px;
    right: 116px;
    width: 342px;
    height: 294px;
    z-index: 10;
src/robotaxi/pages/site-search/components/favorite-popup/index.less
@@ -20,7 +20,7 @@
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(0, 0, 0, 0.4);
    background: rgba(0, 0, 0, 0.8);
    z-index: 1;
  }
@@ -148,4 +148,4 @@
      z-index: 11;
    }
  }
}
}
src/robotaxi/utils/api-logger.ts
File was deleted