src/robotaxi/constants/music-control-feature.ts
New file @@ -0,0 +1,13 @@ /** * 曲风(车载音乐)卡片展示开关。 * * true → 正常展示曲风卡片与控制面板,并按原逻辑发起查询/下发请求; * false → 曲风卡片与面板完全不展示,且不发起任何曲风相关接口请求 * (非接驾态、连环单等原有守卫逻辑不变)。 * * 当前:暂时关闭曲风卡片。 */ export const MUSIC_CONTROL_ENABLED = false; /** 曲风卡片是否展示 */ export const isMusicControlEnabled = () => MUSIC_CONTROL_ENABLED; src/robotaxi/pages/begin-route/components/calling-car-modules/ac-switch/index.less
@@ -12,6 +12,8 @@ flex-shrink: 0; width: 104px; height: 64px; /* 图片右侧自带 14px 透明留白,负 margin 抵消,使开关视觉右边界与容器对齐 */ margin-right: -14px; } .ac-switch-loading { src/robotaxi/pages/begin-route/components/calling-car-modules/ac-switch/index.tsx
@@ -2,8 +2,11 @@ import './index.less'; // 图片开关三态图标(复用入口卡片同款) const SWITCH_OPEN_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/T-switch-open-20260709.png'; const SWITCH_CLOSE_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/T-switch-close-20260709.png'; const SWITCH_CLOSE_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/T-switch-close-20260812.png'; // 开启态:金色轨道 + 白色滑块(浅色背景) const SWITCH_OPEN_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/T-switch-open-20260812.png'; // 开启态:白色轨道 + 金色滑块(彩色/深色卡片背景) const SWITCH_OPEN_WHITE_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/T-switch-open-white-20260812.png'; const SWITCH_LOADING_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/T-switch-loading-20260715.png'; type Props = { @@ -15,16 +18,23 @@ onChange?: () => void; /** 自定义 class */ className?: string; /** * 开启态样式(关闭态两者一致): * - gold(默认):金色轨道 + 白色滑块,用于浅色背景(香氛卡片、弹窗内开关); * - white:白色轨道 + 金色滑块,用于彩色/深色卡片背景(空调入口卡片)。 */ theme?: 'gold' | 'white'; }; /** * 空调风格图片开关(开/关/loading 三态),供卡片与面板统一复用。 */ export default function AcSwitch({ checked, loading = false, onChange, className = '' }: Props) { export default function AcSwitch({ checked, loading = false, onChange, className = '', theme = 'gold' }: Props) { const openIcon = theme === 'white' ? SWITCH_OPEN_WHITE_ICON : SWITCH_OPEN_ICON; return ( <Image className={`ac-switch${loading ? ' ac-switch-loading' : ''} ${className}`.trim()} src={loading ? SWITCH_LOADING_ICON : (checked ? SWITCH_OPEN_ICON : SWITCH_CLOSE_ICON)} src={loading ? SWITCH_LOADING_ICON : (checked ? openIcon : SWITCH_CLOSE_ICON)} onClick={(e: any) => { e?.stopPropagation?.(); onChange && onChange(); }} /> ); src/robotaxi/pages/begin-route/components/calling-car-modules/music-control/index.tsx
@@ -4,6 +4,7 @@ import type { GenreItem } from '../../../../../api/types/genre'; import { getOrderId } from '../../../../../utils/order'; import { TripStatus } from '../../../../../constants/trip-status'; import { isMusicControlEnabled } from '../../../../../constants/music-control-feature'; import useBottomPopupAnimation from '../../hooks/use-bottom-popup-animation'; import type { ControlNodes } from '../types'; import AcSwitch from '../ac-switch'; @@ -90,9 +91,9 @@ // 开关 loading 仅在由开关(而非磁贴)触发下发时展示 const switchLoading = applying && applyingGenreId === ''; // 进入接驾态查询曲风状态;连环单不查询、不展示 // 进入接驾态查询曲风状态;功能开关关闭、连环单不查询、不展示 useEffect(() => { if (tripStatus !== TripStatus.VehicleApproaching || hasPreviousOrder) { if (!isMusicControlEnabled() || tripStatus !== TripStatus.VehicleApproaching || hasPreviousOrder) { // 离开接驾态:重置去重标记,允许下次进入接驾态时重新查询一次。 queriedOrderIdRef.current = ''; return; @@ -202,8 +203,8 @@ applyGenre(nextSwitchOn, targetGenreId, ''); }; // 连环单或非接驾态不展示音乐卡片 if (tripStatus !== TripStatus.VehicleApproaching || hasPreviousOrder) { // 功能开关关闭、连环单或非接驾态不展示音乐卡片 if (!isMusicControlEnabled() || tripStatus !== TripStatus.VehicleApproaching || hasPreviousOrder) { return { card: null, popup: null }; } src/robotaxi/pages/begin-route/components/calling-car-modules/seat-hv-control/index.tsx
@@ -50,6 +50,10 @@ */ export default function SeatHvArea({ tripStatus, orderId, pageDidShowVersion = 0, hasPreviousOrder = false, active = true }: Props) { const [seats, setSeats] = useState<SeatHvItem[]>([]); // 座椅状态查询是否已出结果(成功或失败)。未出结果前整块透明,避免先闪「不可用」再切成可用 const [queried, setQueried] = useState(false); // 座舱背景图是否加载完成(用于背景渐现;一旦加载过就不再重置,重开面板直接显示) const [bgLoaded, setBgLoaded] = useState(false); // 正在下发的按钮标识 `${position}:${kind}`(该按钮显示 loading,其余按钮置灰防重复),空串表示无 const [applyingKey, setApplyingKey] = useState(''); const querySeqRef = useRef(0); @@ -82,11 +86,13 @@ } const list: SeatHvItem[] = Array.isArray(res?.seats) ? res.seats : []; setSeats(list); setQueried(true); }).catch(() => { if (querySeqRef.current !== snapshotSeq) { return; } setSeats([]); setQueried(true); }); }, [active, tripStatus, orderId, pageDidShowVersion, hasPreviousOrder]); @@ -141,13 +147,24 @@ } }; // 查询未出结果:整块透明占位(只占据区域高度、不画任何内容),避免先闪一下「不可用」 if (!queried) { return <View className='seat-hv-area'></View>; } if (!hasSeats) { return <View className='seat-hv-empty'>{NO_SEAT_TIP}</View>; } return ( <View className='seat-hv-area'> <Image className='seat-hv-bg' src={SEAT_BG_URL} mode='widthFix' /> <Image className={`seat-hv-bg${bgLoaded ? ' is-loaded' : ''}`} src={SEAT_BG_URL} mode='widthFix' onLoad={() => setBgLoaded(true)} onError={() => setBgLoaded(true)} /> <View className='seat-hv-list'> {seats.map((seat) => { const showVentilation = seat.ventilationLevels > 0; src/robotaxi/pages/begin-route/components/calling-car-modules/temperature-control.tsx
@@ -24,7 +24,10 @@ // 入口卡片图标(复用 temperature-fragrance-control 的 v2 资源) const AC_HEADER_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/air-conditioner-20260716.png'; // 开启态入口卡片图标与箭头:白色版本(配合白色文字与开启态背景) const AC_HEADER_ICON_WHITE = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/air-conditioner-white-20260812.png'; const AC_ARROW_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/temperature-fragrance-arrow-20260706.png'; const AC_ARROW_ICON_WHITE = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/arrow-white-20260812.png'; const PANEL_CLOSE_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/close-icon.png'; const AC_TOGGLE_FAIL_TEXT = '空调开关操作失败,请重试'; const AC_TEMP_FAIL_TEXT = '温度设置失败,请重试'; @@ -32,7 +35,7 @@ const AC_TEMP_MAX_TIP_TEXT = '已到空调最高设定温度'; const TEMP_ITEM_WIDTH = 130; // 温度滑条单项宽度(所有项等宽) const TEMP_SCROLL_SETTLE_MS = 120; // 滑动停止判定延时 const TEMP_LEFT = 10; // 滚动位移误差(适配中间放大效果) const TEMP_LEFT = 20; // 滚动位移误差(适配中间放大效果) type Props = { tripStatus: TripStatus; @@ -470,10 +473,11 @@ > <View className='tf-card-header'> <View className='tf-card-header-left'> <Image className='tf-card-header-icon' src={AC_HEADER_ICON} /> <Image className='tf-card-header-icon' src={acSwitchOn ? AC_HEADER_ICON_WHITE : AC_HEADER_ICON} /> <Text className='tf-card-header-title'>空调</Text> </View> <AcSwitch checked={acSwitchOn} loading={isTogglingAc} onChange={handleToggleAcSwitch} /> {/* 空调卡片为彩色背景,开启态用白色轨道 + 金色滑块 */} <AcSwitch checked={acSwitchOn} loading={isTogglingAc} onChange={handleToggleAcSwitch} theme='white' /> </View> <View className='tf-card-bottom'> {acStatusLoaded && ( @@ -484,7 +488,7 @@ )} <View className='tf-card-footer'> <Text className='tf-card-footer-text'>调节温度|通风加热</Text> <Image className='tf-card-footer-arrow' src={AC_ARROW_ICON} /> <Image className='tf-card-footer-arrow' src={acSwitchOn ? AC_ARROW_ICON_WHITE : AC_ARROW_ICON} /> </View> </View> </View> @@ -532,7 +536,7 @@ onClick={() => handleTempItemClick(index, value)} > <Text className='popup-temp-item-value'>{value.toFixed(1)}</Text> {selected && <Text className='popup-temp-item-unit'>℃</Text>} {selected && <Text className='popup-temp-item-unit'>°C</Text>} </View> ); })} src/robotaxi/pages/begin-route/components/calling-car.tsx
@@ -1,5 +1,5 @@ import { View, Image, ScrollView } from "@tarojs/components"; import { useState, useEffect, useRef } from 'react'; import { View, Image } from "@tarojs/components"; import { useState, useEffect } from 'react'; import './css-folder/calling-car.less' import { formatRemainingDurationDisplay } from '../../../utils/format'; import { safeParse, formatTripDistance } from '../../../../utils/Robotaxi-public-method'; @@ -176,23 +176,12 @@ const isCallingCar = tripStatus === TripStatus.CallingCar; /** * 车控(空调 / 音乐 / 香氛):卡片与弹窗分开挂载。 * 卡片放进横向滑动的 ScrollView;弹窗是 fixed 全屏浮层,必须挂在 ScrollView 之外, * 否则 iOS(WKWebView)下滚动容器会成为 fixed 的包含块,弹窗被限制在卡片滑动区内。 * 卡片放进 2 列网格容器;弹窗是 fixed 全屏浮层,单独挂在网格容器之外,避免受父容器定位影响。 * 三个 hook 内部均按 tripStatus / hasPreviousOrder 守卫,非接驾态不发请求、返回空节点。 */ const acControl = useAcControl({ tripStatus, orderId, pageDidShowVersion, hasPreviousOrder }); const musicControl = useMusicControl({ tripStatus, orderId, pageDidShowVersion, hasPreviousOrder }); const fragranceControl = useFragranceControl({ tripStatus, orderId, pageDidShowVersion, hasPreviousOrder }); /** * 车控卡片当前横向滚动位置。 * 弹窗开合与其弹出动画会驱动本组件重渲染,weapp 的 scroll-view 在重渲染 / 重建后会回到 * 初始位置(最左),表现为「弹窗一出现卡片就滑回去了」。这里用 ref 记录实时位置并回传 * scroll-left,使重渲染后停留在原处;用 ref 而非 state 是为了滚动过程中不触发额外渲染。 */ const cardScrollLeftRef = useRef(0); const handleCardScroll = (e: any) => { cardScrollLeftRef.current = e?.detail?.scrollLeft ?? 0; }; /** 费用明细是否展开(拦截 FareDetailCard 的回调本地记录,用于切换背景与卡片布局) */ const [detailExpanded, setDetailExpanded] = useState(false); /** 大号里程文案:取全程里程,单位参考 formatTripDistance(米→M,公里→KM) */ @@ -264,6 +253,22 @@ /> { /* 行程操作栏:紧跟行程状态之下、蓝牙卡片之上(对齐设计稿)。 到达终点/已支付状态下 TripActionBar 内部按钮集合为空、自动返回 null,不占位。 */ [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} @@ -295,23 +300,15 @@ { [TripStatus.VehicleApproaching, TripStatus.VehicleArrivedPickup, TripStatus.InService].includes(tripStatus) && <View className='car-card'> {/* 车控卡片横向滑动(仅接驾态展示):weapp 中普通 View 的 overflow 滚动无法响应拖动,必须用 ScrollView。 注意:这里只放卡片,三个控制弹窗在下方 ScrollView 之外单独挂载(iOS fixed 会被滚动容器裁切) */} {/* 车控卡片(仅接驾态展示):2 列网格布局,列间隔 32px,超过 2 个自动换行。 注意:这里只放卡片,三个控制弹窗在下方单独挂载(弹窗为 fixed 全屏浮层) */} { TripStatus.VehicleApproaching === tripStatus && <ScrollView className='temp-fragrance-scroll' scrollX showScrollbar={false} scrollLeft={cardScrollLeftRef.current} onScroll={handleCardScroll} > <View className='temp-fragrance-control'> {acControl.card} {musicControl.card} {fragranceControl.card} </View> </ScrollView> <View className='temp-fragrance-control'> {acControl.card} {musicControl.card} {fragranceControl.card} </View> } <DoorTrunkControl @@ -359,20 +356,6 @@ onOpenCouponSelect={onOpenCouponSelect} /> </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} src/robotaxi/pages/begin-route/components/css-folder/calling-car.less
@@ -996,45 +996,29 @@ } } /* 车控入口卡片横向滑动容器(空调 / 音乐 / 香氛,卡片固定宽度,超出宽度可横向滑动) 用 ScrollView 而非 View + overflow:weapp WebView 渲染器会拦截 View 上的触摸手势, CSS overflow 滚动在真机与模拟器均无法拖动。 */ .temp-fragrance-scroll { /* 车控入口卡片 2 列网格容器(空调 / 音乐 / 香氛):不再横向滑动, 每行 2 个卡片、列间隔 32px,卡片宽度按内容区两等分,超过 2 个自动换行(行间隔同为 32px)。 */ .temp-fragrance-control { position: relative; z-index: 2; 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; display: flex; flex-wrap: wrap; align-items: flex-start; gap: 24px; /* 静止时首卡对齐 48px 内容左边界;滑到底时末卡右侧同样保留 48px 留白 */ padding-left: 48px; padding-right: 48px; gap: 32px; /* 卡片不参与压缩,保证各卡片保持设计稿宽度并整体溢出滚动 */ /* 卡片外层容器:两等分内容宽度(减去一个 32px 列间隔) */ > .ac-control, > .music-control, > .fragrance-control { flex-shrink: 0; flex: 0 0 calc((100% - 32px) / 2); width: calc((100% - 32px) / 2); min-width: 0; } /* 卡片撑满所在列(全局 .tf-card 为横滑布局下的固定 304px 宽) */ .tf-card { width: 100%; } } @@ -1128,6 +1112,7 @@ .tf-card-ac-temp-unit { margin-top: 8px; margin-left: 6px; font-family: @font-xpsans-bold; font-size: 28px; font-weight: 500; line-height: 32px; @@ -1139,6 +1124,7 @@ display: flex; align-items: center; justify-content: space-between; margin-top: 32px; } .tf-card-footer-text { @@ -1160,14 +1146,21 @@ padding-top: 8px; &.is-open { background-image: url('https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/aircondition-open-20260703.png'); background-image: url('https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/aircondition-open-20260812.png'); // 开启态卡片文字统一为白色 .tf-card-header-title, .tf-card-ac-temp-value, .tf-card-ac-temp-unit, .tf-card-footer-text { color: #FFFFFF; } } &.is-close { background-image: url('https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/aircondition-close-20260703.png'); background-image: url('https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/aircondition-close-20260812.png'); // 关闭态卡片文字统一弱化 .tf-card-header-title, // 关闭态卡片文字统一弱化(标题保持主题色,与香氛卡片一致) .tf-card-ac-temp-value, .tf-card-ac-temp-unit, .tf-card-footer-text { src/robotaxi/pages/begin-route/components/css-folder/fragrance-control.less
@@ -28,11 +28,11 @@ /* 香氛入口卡片:背景随开关状态切换 */ .tf-card-fragrance { &.is-open { background-image: url('https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/fragrance-open-20260703.png'); background-image: url('https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/fragrance-open-20260812.png'); } &.is-close { background-image: url('https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/fragrance-close-20260703.png'); background-image: url('https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/fragrance-close-20260812.png'); // 关闭态卡片文字统一弱化 .tf-card-fragrance-name, src/robotaxi/pages/begin-route/components/css-folder/seat-hv-control.less
@@ -9,16 +9,32 @@ } } // 座椅区背景图为 750×1232,容器按同比例固定高度: // 按钮以百分比定位,容器高度提前确定后,按钮首帧即在正确位置(不再等背景图 widthFix 撑开高度) @seat-hv-area-height: 1232px; // 可见座椅区高度:弹窗顶部到空调白卡顶部,对应 temperature-control.less 中 .popup-ac 的 margin-top @seat-hv-visible-height: 772px; // 座椅通风加热区(内嵌于「温度调节」面板) .seat-hv-area { position: relative; width: 100%; height: @seat-hv-area-height; overflow: hidden; // 座舱俯视图背景(widthFix,高度按图片比例自适应,撑起区域高度) // 座舱俯视图背景:绝对定位不参与高度计算,加载完成后渐现 .seat-hv-bg { position: absolute; top: 0; left: 0; width: 100%; display: block; opacity: 0; transition: opacity 240ms ease; &.is-loaded { opacity: 1; } } // 座椅节点叠放层 @@ -139,13 +155,12 @@ } } // 无座椅占位 // 无座椅占位(不可用态):无背景色,铺满整个可见座椅区 .seat-hv-empty { height: 320px; height: @seat-hv-visible-height; display: flex; align-items: center; justify-content: center; background-color: #E9EBF0; font-size: 28px; color: rgba(0, 0, 0, 0.4); } src/robotaxi/pages/begin-route/components/css-folder/temperature-control.less
@@ -128,7 +128,8 @@ // 居中选中项:放大加粗(空调开启时高亮) &.active { width: 220px; // 220px(原值)+ 左右各 20px:加大选中温度与相邻温度的间距 width: 260px; .popup-temp-item-value { font-size: 64px; line-height: 72px;