liaof
2026-08-05 d52f11cda799af80b514c022da684fa339e05015
Merge branch 'feature/xj-rt-0807-music' into 'feature_v2.5.0'

feat: 曲风 (甄雪君)

See merge request robotaxi1/xp-robotaxi-miniprogram!246
1个文件已删除
4个文件已添加
10个文件已修改
962 ■■■■■ 已修改文件
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/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 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car.tsx 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/calling-car.less 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/fragrance-control.less 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/css-folder/music-control.less 265 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/debug-order-simulator-panel.tsx 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/index.tsx 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/utils/api-logger.ts 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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/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
@@ -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_0803.webp';
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 = '座椅调节暂不可用';
src/robotaxi/pages/begin-route/components/calling-car.tsx
@@ -9,6 +9,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'; // 费用明细卡片
@@ -241,6 +242,21 @@
            />
            {
                /* 行程操作栏:所有状态下均紧随状态卡、位于蓝牙提示条上方(对齐设计稿) */
                [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup, TripStatus.InService, TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(tripStatus) &&
                <TripActionBar
                  tripStatus={tripStatus}
                  handleCancelTrip={handleCancelTrip}
                  showCancelTrip={showCancelTrip}
                  showShareTrip={showShareTrip}
                  handleModifyDestination={handleModifyDestination}
                  handleModifyDestinationDisabledClick={handleModifyDestinationDisabledClick}
                  showModifyDestination={showModifyDestination}
                  handleShareTrip={handleShareTrip}
                />
            }
            {
                [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup].includes(tripStatus) && bluetoothStatusReady &&
                <BluetoothOperateCard
                  description={bluetoothCardDescription}
@@ -274,6 +290,7 @@
                <View className='car-card'>
                    <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>
@@ -312,20 +329,6 @@
                    />
                </View>
            }
            {
                [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup, TripStatus.InService, TripStatus.TripFinished, TripStatus.PaymentSuccess].includes(tripStatus) &&
                <TripActionBar
                  tripStatus={tripStatus}
                  handleCancelTrip={handleCancelTrip}
                  showCancelTrip={showCancelTrip}
                  showShareTrip={showShareTrip}
                  handleModifyDestination={handleModifyDestination}
                  handleModifyDestinationDisabledClick={handleModifyDestinationDisabledClick}
                  showModifyDestination={showModifyDestination}
                  handleShareTrip={handleShareTrip}
                />
            }
            <DoorTrunkControl
                tripStatus={tripStatus === TripStatus.ArrivedDestination ? TripStatus.ArrivedDestination : -1}
                orderId={orderId}
src/robotaxi/pages/begin-route/components/css-folder/calling-car.less
@@ -582,7 +582,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;
@@ -998,9 +999,10 @@
  position: relative;
  z-index: 2;
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 16px;
  margin-top: 16px;
  gap: 28px;
  margin-top: 8px;
}
/* 空调/香氛入口卡片开关 loading 旋转动画 */
@@ -1021,7 +1023,7 @@
  box-sizing: border-box;
  width: 312px;
  height: 240px;
  padding: 24px;
  padding: 8px 24px 24px;
  border-radius: 24px;
  overflow: hidden;
  display: flex;
@@ -1051,8 +1053,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/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;
  }
  /* 香氛入口卡片:背景随开关状态切换 */
@@ -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,265 @@
@import '../../../../style/ui-variables.less';
// 曲风磁贴下发 loading 旋转动画
@keyframes music-genre-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}
// 副标题上下滚动轮播(5 条文案 + 末尾克隆首条实现无缝循环,单条行高 20px)
@keyframes music-subtitle-roll {
  0%, 16% {
    transform: translateY(0);
  }
  20%, 36% {
    transform: translateY(-20px);
  }
  40%, 56% {
    transform: translateY(-40px);
  }
  60%, 76% {
    transform: translateY(-60px);
  }
  80%, 96% {
    transform: translateY(-80px);
  }
  100% {
    transform: translateY(-100px);
  }
}
/* 音乐入口卡片: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;
  }
  // 副标题上下滚动轮播:仅露出一行(20px),轨道竖排承载多条文案
  .tf-card-subtitle {
    flex: 1;
    min-width: 0;
    height: 20px;
    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 {
    height: 20px;
    line-height: 20px;
    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/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/index.tsx
@@ -2645,7 +2645,8 @@
        onClose={handleCloseCreditPayPopup}
        onConfirm={handleConfirmCreditPayPopup}
      />
      {/* <DebugOrderSimulatorPanel
      {/* 模拟器入口已关闭,如需本地调试可取消下方注释
      <DebugOrderSimulatorPanel
        orderId={orderId}
        setOrderId={setOrderId}
        setTripStatus={setTripStatus}
@@ -2656,7 +2657,8 @@
        setShowCancelPopup={setShowCancelPopup}
        setShowModifyDestinationFailPopup={setShowModifyDestinationFailPopup}
        setModifyDestinationFailMessage={setModifyDestinationFailMessage}
      /> */}
      />
      */}
    </>
  )
}
src/robotaxi/utils/api-logger.ts
File was deleted