From aa8038c44dc8e2eda1e801b8f44f54d00fd649e3 Mon Sep 17 00:00:00 2001
From: 卢春龙 <lucl@xiaopeng.com>
Date: 星期五, 24 四月 2026 23:53:02 +0800
Subject: [PATCH] Merge branch 'feature_urgency_fix' into 'develop'
---
src/robotaxi/pages/begin-route/hooks/use-route-manager.ts | 16 ++
src/robotaxi/pages/begin-route/hooks/use-vehicle-tracking.ts | 57 +++++++-
src/pages/robotaxi/home/components/map-overlay-tips/index.tsx | 16 ++
src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx | 7
src/robotaxi/pages/begin-route/components/calling-car-modules/door-trunk-control.tsx | 9 -
src/robotaxi/pages/begin-route/components/calling-car-modules/trip-status-header.tsx | 8
src/pages/robotaxi/home/index.tsx | 46 +++++++
src/robotaxi/pages/begin-route/index.tsx | 13 +
src/robotaxi/pages/begin-route/hooks/use-order-actions.ts | 44 +++++++
src/robotaxi/pages/begin-route/components/calling-car.tsx | 13 -
src/robotaxi/pages/begin-route/hooks/use-popup-manager.ts | 17 ++
src/utils/websocket.ts | 2
src/pages/robotaxi/home/index.less | 2
src/robotaxi/pages/begin-route/components/begin-route-view.tsx | 14 ++
src/robotaxi/pages/begin-route/components/cancel-popup.tsx | 6
src/robotaxi/pages/begin-route/hooks/use-map-markers.ts | 32 +++++
src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts | 16 ++
src/robotaxi/utils/format.ts | 12 ++
18 files changed, 273 insertions(+), 57 deletions(-)
diff --git a/src/pages/robotaxi/home/components/map-overlay-tips/index.tsx b/src/pages/robotaxi/home/components/map-overlay-tips/index.tsx
index 8d03117..b8e8126 100644
--- a/src/pages/robotaxi/home/components/map-overlay-tips/index.tsx
+++ b/src/pages/robotaxi/home/components/map-overlay-tips/index.tsx
@@ -19,14 +19,28 @@
onOpenSetting: () => void;
/** 查看运营区域 */
onViewOperatingArea: () => void;
+ /** 初始化锚点底部偏移 */
+ anchorBottom?: number;
+ /** 透明度,用于配合上划消失 */
+ opacity?: number;
+ /** 拖拽状态,用于控制过渡动画 */
+ isDragging?: boolean;
};
/**
* 地图顶部提示层。
*/
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,
+ transition: props.isDragging ? 'none' : 'opacity 0.2s ease-out'
+ }
+ : undefined;
return (
- <View className="content-box-top">
+ <View className="content-box-top" style={anchorStyle}>
<View className="position-box">
{
props.isGuest && <View className="visitor-stste ml-32">
diff --git a/src/pages/robotaxi/home/index.less b/src/pages/robotaxi/home/index.less
index 62fc6dd..613894b 100644
--- a/src/pages/robotaxi/home/index.less
+++ b/src/pages/robotaxi/home/index.less
@@ -61,7 +61,7 @@
position: fixed;
top: 55%;
left: 0px;
- z-index: 1;
+ z-index: 2;
.position-box {
diff --git a/src/pages/robotaxi/home/index.tsx b/src/pages/robotaxi/home/index.tsx
index 98d07cd..8646df6 100644
--- a/src/pages/robotaxi/home/index.tsx
+++ b/src/pages/robotaxi/home/index.tsx
@@ -101,6 +101,7 @@
const [bannerList, setBannerList] = useState<any[]>([]); // 轮播图列表
const [orderData, setOrderData] = useState<any>({}); // 订单数据
const [isLogin, setIsLogin] = useState(false); // 是否登录
+ const [overlayAnchorBottom, setOverlayAnchorBottom] = useState<number | null>(null); // 浮层初始化锚点(距底部)
const {
boxHeight,
originOpacity,
@@ -320,6 +321,9 @@
useDidShow(() => {
bootstrap.onDidShow();
+ setTimeout(() => {
+ initOverlayAnchor();
+ }, 80);
});
const { handleRegionChange, queryPointerByCenter } = useRegionChange({
@@ -359,6 +363,35 @@
const handleCloseCustomerService = () => {
setShowCustomerService(false);
};
+
+ // 初始化浮层锚点:固定在 content-box 顶部上方 32px
+ const initOverlayAnchor = () => {
+ // content-box 高度受 banner 数量影响:多 banner 复用 boxHeight,单 banner 走渲染后实测高度
+ if (bannerList.length > 1) {
+ const bottom = Math.max(0, 280 + 32);
+ if (bottom <= 0) return;
+ setOverlayAnchorBottom(bottom);
+ return;
+ }
+ const query = Taro.createSelectorQuery();
+ query.select('.content-box').boundingClientRect();
+ query.exec((res) => {
+ const rect = res?.[0];
+ const height = Number(rect?.height);
+ if (!Number.isFinite(height) || height <= 0) return;
+ const bottom = Math.max(0, height + 32);
+ setOverlayAnchorBottom(bottom);
+ });
+ };
+
+ useEffect(() => {
+ const timer = setTimeout(() => {
+ Taro.nextTick(() => {
+ initOverlayAnchor();
+ });
+ }, 80);
+ return () => clearTimeout(timer);
+ }, [bannerList.length, tripStatus]);
// 处理点击地图 marker
const handleMarkerTap = (e: any) => {
@@ -410,7 +443,15 @@
<Image className="select-city-icon" src={selectCityIcon} />
</View>
{/* 定位图标 */}
- <View className="origin-box" style={{ opacity: originOpacity, transition: isDragging.current ? 'none' : 'opacity 0.2s ease-out' }}>
+ <View
+ className="origin-box"
+ style={{
+ opacity: originOpacity,
+ transition: isDragging.current ? 'none' : 'opacity 0.2s ease-out',
+ top: typeof overlayAnchorBottom === 'number' ? 'auto' : undefined,
+ bottom: typeof overlayAnchorBottom === 'number' ? `${overlayAnchorBottom}px` : undefined,
+ }}
+ >
<Image onClick={toOrigin} className="origin-icon" src={originIcon} />
</View>
<MapOverlayTips
@@ -420,6 +461,9 @@
isMaintain={isMaintain}
maintainBounce={maintainBounce}
hotfixEndText={hotfixEndText}
+ anchorBottom={overlayAnchorBottom ?? undefined}
+ opacity={originOpacity}
+ isDragging={isDragging.current}
onShowPrivacy={() => setShowPrivacy(true)}
onOpenSetting={handleOpenSetting}
onViewOperatingArea={handleViewOperatingArea}
diff --git a/src/robotaxi/pages/begin-route/components/begin-route-view.tsx b/src/robotaxi/pages/begin-route/components/begin-route-view.tsx
index 05765bb..74fd3fb 100644
--- a/src/robotaxi/pages/begin-route/components/begin-route-view.tsx
+++ b/src/robotaxi/pages/begin-route/components/begin-route-view.tsx
@@ -9,6 +9,7 @@
import PassengerNotice from './passenger-notice';
import EvaluatePopup from './evaluate-popup';
import TipPopup from '../../select-map-site/components/tip-popup';
+import PrivacyPopup from '../../../../pages/robotaxi/home/components/privacy-popup';
type Props = {
mapLongitude: number;
@@ -38,6 +39,7 @@
handleCloseCustomerService: () => void;
showCancelPopup: boolean;
cancelType: number;
+ cancelPopupWaitMinutes: number | null;
startSite: any;
endSite: any;
fareEstimate: any;
@@ -53,6 +55,8 @@
showTipPopup: boolean;
handleCloseTipPopup: () => void;
tipType: string;
+ showPrivacyPopup: boolean;
+ handleClosePrivacyPopup: () => void;
showPassengerNotice: boolean;
handleClosePassengerNotice: () => void;
changeCheckedEnter: (checked: boolean) => void;
@@ -91,6 +95,7 @@
handleCloseCustomerService,
showCancelPopup,
cancelType,
+ cancelPopupWaitMinutes,
startSite,
endSite,
fareEstimate,
@@ -106,6 +111,8 @@
showTipPopup,
handleCloseTipPopup,
tipType,
+ showPrivacyPopup,
+ handleClosePrivacyPopup,
showPassengerNotice,
handleClosePassengerNotice,
changeCheckedEnter,
@@ -163,7 +170,7 @@
cancelType={cancelType}
startSite={startSite}
endSite={endSite}
- waitMinutes={fareEstimate?.waitingTime}
+ waitMinutes={cancelPopupWaitMinutes ?? fareEstimate?.waitingTime}
tripStatus={tripStatus}
routeDurationSeconds={routeDuration}
discountAmount={fareEstimate?.discountAmount}
@@ -184,6 +191,11 @@
onClose={handleCloseTipPopup}
tipType={tipType}
/>
+ <PrivacyPopup
+ show={showPrivacyPopup}
+ onClose={handleClosePrivacyPopup}
+ onAgree={handleClosePrivacyPopup}
+ />
{!showCancelPopup && (
<PassengerNotice
show={showPassengerNotice}
diff --git a/src/robotaxi/pages/begin-route/components/calling-car-modules/door-trunk-control.tsx b/src/robotaxi/pages/begin-route/components/calling-car-modules/door-trunk-control.tsx
index 0fcba6e..6203ff9 100644
--- a/src/robotaxi/pages/begin-route/components/calling-car-modules/door-trunk-control.tsx
+++ b/src/robotaxi/pages/begin-route/components/calling-car-modules/door-trunk-control.tsx
@@ -8,20 +8,17 @@
orderId?: string;
orderTrunkStatus?: boolean;
handleOpenOverRoutePopup?: () => void;
- handleOpenAnnouncementsPopup?: () => void;
};
export default function DoorTrunkControl({
tripStatus,
orderId,
orderTrunkStatus,
- handleOpenOverRoutePopup,
- handleOpenAnnouncementsPopup
+ handleOpenOverRoutePopup
}: Props) {
const [trunkStatus, setTrunkStatus] = useState(orderTrunkStatus); // 后备箱开关状态
const [lockStatus, setLockStatus] = useState(false); // 车门解锁状态
const trunkPollTimerRef = useRef<any>(null); // 后备箱状态轮询定时器
- const hasShownAnnouncementsRef = useRef(false); // 安全须知是否已展示
// 外部后备箱状态变化时同步本地状态
useEffect(() => {
@@ -157,10 +154,6 @@
if (doorType === 'door') {
Taro.hideLoading();
Taro.showToast({ title: '右侧车门已解锁', icon: 'none' });
- if (!hasShownAnnouncementsRef.current) {
- handleOpenAnnouncementsPopup?.();
- hasShownAnnouncementsRef.current = true;
- }
}
}).catch(() => {
Taro.hideLoading();
diff --git a/src/robotaxi/pages/begin-route/components/calling-car-modules/trip-status-header.tsx b/src/robotaxi/pages/begin-route/components/calling-car-modules/trip-status-header.tsx
index 710c268..cd3e53e 100644
--- a/src/robotaxi/pages/begin-route/components/calling-car-modules/trip-status-header.tsx
+++ b/src/robotaxi/pages/begin-route/components/calling-car-modules/trip-status-header.tsx
@@ -12,7 +12,7 @@
finishCountdownEnabled: boolean;
finishCountdownSeconds: number | null;
limitMinutes: number | null;
- handleOpenCancelPopup?: (durationDisplay: any) => void;
+ handleOpenCancelPopup?: (displayInfo: { durationDisplay: { value: string | number; unit: string }; waitMinutes: number | null }) => void;
};
export default function TripStatusHeader({
@@ -30,7 +30,9 @@
// 标题行文案映射
const titleMap = {
3: <>预计<View className="time">{waitMinutes}</View>分钟内有车应答</>,
- 4: <>车辆还有<View className="time">{durationDisplay.value}</View>{durationDisplay.unit}到达</>,
+ 4: isArriveSoon
+ ? <>即将到达</>
+ : <>车辆还有<View className="time">{durationDisplay.value}</View>{durationDisplay.unit}到达</>,
5: <>车辆已到达上车点<View className="time" style={isPickupCountdownSoon ? { color: '#E03434' } : undefined}>{pickupCountdownSeconds !== null ? formatCountdown(pickupCountdownSeconds) : '--:--'}</View></>,
6: <>预计<View className="time">{durationDisplay.value}</View>{durationDisplay.unit}抵达目的地</>,
7: (finishCountdownEnabled || finishCountdownSeconds !== null)
@@ -63,7 +65,7 @@
{
[3, 4, 5].includes(tripStatus) &&
<View className="cancel-btn">
- <Image onClick={() => handleOpenCancelPopup?.(durationDisplay)} src={require('../../../../assets/cancel-btn.png')} className="cancel" />
+ <Image onClick={() => handleOpenCancelPopup?.({ durationDisplay, waitMinutes })} src={require('../../../../assets/cancel-btn.png')} className="cancel" />
</View>
}
</View>
diff --git a/src/robotaxi/pages/begin-route/components/calling-car.tsx b/src/robotaxi/pages/begin-route/components/calling-car.tsx
index adaa499..bbe8004 100644
--- a/src/robotaxi/pages/begin-route/components/calling-car.tsx
+++ b/src/robotaxi/pages/begin-route/components/calling-car.tsx
@@ -1,6 +1,6 @@
import { View, Image } from "@tarojs/components";
import './css-folder/calling-car.less'
-import { formatDurationDisplay } from '../../../../utils/Robotaxi-public-method';
+import { formatRemainingDurationDisplay } from '../../../utils/format';
import useTripCountdown from './calling-car-modules/use-trip-countdown';
import TripStatusHeader from './calling-car-modules/trip-status-header';
@@ -13,8 +13,8 @@
type Props = {
/** 行程状态:3呼叫中、4即将到达、5已到达上车点、6行程中、7到达终点、8待支付、9已支付 */
tripStatus?: number;
- /** 打开取消弹窗,入参为预计到达展示文案 */
- handleOpenCancelPopup?: (durationDisplay: any) => void;
+ /** 打开取消弹窗,入参为顶部卡片当前展示信息 */
+ handleOpenCancelPopup?: (displayInfo: { durationDisplay: { value: string | number; unit: string }; waitMinutes: number | null }) => void;
/** 上车点超时后的回调 */
handleCancelTimeout?: () => void;
/** 到达终点后倒计时结束回调 */
@@ -35,8 +35,6 @@
handlePay?: () => void;
/** 打开详细评价弹窗 */
handleOpenEvaluatePopup?: () => void;
- /** 首次开门后打开安全须知弹窗 */
- handleOpenAnnouncementsPopup?: () => void;
/** 预估等待分钟数 */
estimatedWaitMinutes?: number;
/** 预估行程秒数 */
@@ -67,7 +65,6 @@
onToggleDetailFade,
handlePay,
handleOpenEvaluatePopup,
- handleOpenAnnouncementsPopup,
estimatedWaitMinutes,
routeDurationSeconds,
finishCountdownEnabled = false,
@@ -94,7 +91,7 @@
});
/** 预计到达时长展示(秒/分钟) */
- const durationDisplay = formatDurationDisplay(routeDurationSeconds);
+ const durationDisplay = formatRemainingDurationDisplay(routeDurationSeconds);
/** 车辆即将到达上车点(<=60秒) */
const isArriveSoon = tripStatus === 4 && (typeof routeDurationSeconds === 'number' && routeDurationSeconds <= 60);
/** 上车倒计时即将结束(<60秒) */
@@ -130,7 +127,6 @@
orderId={orderId}
orderTrunkStatus={orderTrunkStatus}
handleOpenOverRoutePopup={handleOpenOverRoutePopup}
- handleOpenAnnouncementsPopup={handleOpenAnnouncementsPopup}
/>
<FareDetailCard
@@ -149,7 +145,6 @@
orderId={orderId}
orderTrunkStatus={orderTrunkStatus}
handleOpenOverRoutePopup={handleOpenOverRoutePopup}
- handleOpenAnnouncementsPopup={handleOpenAnnouncementsPopup}
/>
<EvaluationCard
diff --git a/src/robotaxi/pages/begin-route/components/cancel-popup.tsx b/src/robotaxi/pages/begin-route/components/cancel-popup.tsx
index 93387dd..7db96d5 100644
--- a/src/robotaxi/pages/begin-route/components/cancel-popup.tsx
+++ b/src/robotaxi/pages/begin-route/components/cancel-popup.tsx
@@ -2,9 +2,9 @@
import { View, Text, Image } from '@tarojs/components';
import { useEffect, useState } from 'react';
import './css-folder/cancel-popup.less'
+import { formatRemainingDurationDisplay } from '../../../utils/format';
import {
- clearOrderTripStorage,
- formatDurationDisplay
+ clearOrderTripStorage
} from '../../../../utils/Robotaxi-public-method'
@@ -29,7 +29,7 @@
const startSiteName = cachedStartSite?.name || cachedStartSite?.address || '-';
const endSiteName = cachedEndSite?.name || cachedEndSite?.address || '-';
const waitMinutesValue = typeof waitMinutes === 'number' && !Number.isNaN(waitMinutes) ? waitMinutes : 2;
- const durationDisplay = formatDurationDisplay(routeDurationSeconds);
+ const durationDisplay = formatRemainingDurationDisplay(routeDurationSeconds);
const discountAmountValue = typeof discountAmount === 'number' && !Number.isNaN(discountAmount) ? discountAmount : 0;
// 不同取消阶段对应的文案配置
diff --git a/src/robotaxi/pages/begin-route/hooks/use-map-markers.ts b/src/robotaxi/pages/begin-route/hooks/use-map-markers.ts
index ff7e315..5980315 100644
--- a/src/robotaxi/pages/begin-route/hooks/use-map-markers.ts
+++ b/src/robotaxi/pages/begin-route/hooks/use-map-markers.ts
@@ -121,11 +121,41 @@
// 应用 marker 状态并处理状态6刷新标记
const applyMarkerState = (nextMarkers: any[], nextMarkersSignature: string, shouldSkipUpdate: boolean, carMarker: any) => {
+ if (shouldSkipUpdate) {
+ // 跳过整包 markers 更新时,仍兜底同步车辆 marker 位置与角度,避免朝向卡旧值
+ setMarkers((prevMarkers: any[]) => {
+ const targetCarIndex = prevMarkers.findIndex((item: any) => item.id === deps.vehicleMarkerId);
+ if (targetCarIndex === -1) {
+ if (markersSignatureRef.current !== nextMarkersSignature) {
+ markersSignatureRef.current = nextMarkersSignature;
+ return nextMarkers;
+ }
+ return prevMarkers;
+ }
+ const prevCar = prevMarkers[targetCarIndex] || {};
+ const nextCarRotate = typeof carMarker?.rotate === 'number' ? carMarker.rotate : prevCar.rotate;
+ const carLocationChanged =
+ Number(prevCar.latitude) !== Number(carMarker?.latitude) ||
+ Number(prevCar.longitude) !== Number(carMarker?.longitude);
+ const carRotateChanged = Number(prevCar.rotate) !== Number(nextCarRotate);
+ if (!carLocationChanged && !carRotateChanged) {
+ return prevMarkers;
+ }
+ const next = [...prevMarkers];
+ next[targetCarIndex] = {
+ ...prevCar,
+ latitude: carMarker?.latitude,
+ longitude: carMarker?.longitude,
+ rotate: nextCarRotate
+ };
+ return next;
+ });
+ }
if (!shouldSkipUpdate && markersSignatureRef.current !== nextMarkersSignature) {
markersSignatureRef.current = nextMarkersSignature;
setMarkers(nextMarkers);
}
- if (!deps.isAndroid && [4, 6].includes(deps.tripStatus) && deps.statusRule.showCar && !!carMarker && !tripStatus6MarkerRefreshedRef.current) {
+ if (!deps.isAndroid && deps.tripStatus === 6 && deps.statusRule.showCar && !!carMarker && !tripStatus6MarkerRefreshedRef.current) {
tripStatus6MarkerRefreshedRef.current = true;
}
carMarkerInitialized.current = deps.statusRule.showCar && !!carMarker;
diff --git a/src/robotaxi/pages/begin-route/hooks/use-order-actions.ts b/src/robotaxi/pages/begin-route/hooks/use-order-actions.ts
index d164442..85c3459 100644
--- a/src/robotaxi/pages/begin-route/hooks/use-order-actions.ts
+++ b/src/robotaxi/pages/begin-route/hooks/use-order-actions.ts
@@ -1,6 +1,7 @@
import Taro from '@tarojs/taro';
import { formatDateTime, clearOrderTripStorage, debounce, reportEvent } from '../../../../utils/Robotaxi-public-method';
import { saveOrder, getOrderPayment, cancelOrder, endOrder } from '../../../api/begin-route';
+import { getAgreement } from '../../../api/home';
import { checkOtp } from '../../../../utils/tools';
import { jumpTo } from '../../../../utils/jump';
@@ -33,6 +34,8 @@
setCancelType: (value: number) => void;
/** 取消弹窗设置 */
setShowCancelPopup: (value: boolean) => void;
+ /** 取消弹窗等待分钟数 */
+ setCancelPopupWaitMinutes: (value: number | null) => void;
/** 步行点设置 */
setWalkingPoints: (value: any[]) => void;
/** 步行时长设置 */
@@ -43,6 +46,8 @@
setLongitude: (value: number) => void;
/** 打开取消弹窗 */
setShowOverRoutePopup: (value: boolean) => void;
+ /** 打开协议弹窗 */
+ handleOpenPrivacyPopup: () => void;
/** 距离与城市校验 */
checkDistanceAndCityReady: (startSite: any) => Promise<number | null>;
/** 绑定 socket 监听 */
@@ -57,6 +62,17 @@
* 订单动作编排:下单、取消、结束、支付。
*/
export default function useOrderActions(deps: Deps) {
+ const NOT_AGREED_TEXT = 'Not agreed to the agreement'; // 协议未同意返回文案关键字
+
+ // 标准化文本,兼容普通空格与不可见空格差异
+ const normalizeText = (value: any) => String(value || '').replace(/\s+/g, ' ').trim().toLowerCase();
+
+ // 判断接口返回是否为“未同意协议”场景
+ const isNotAgreedMessage = (value: any) => normalizeText(value).includes(NOT_AGREED_TEXT);
+
+ // 协议接口约定:true=已同意,false=未同意
+ const resolveAgreementStatus = (result: any) => result === true;
+
/**
* 结束下单中状态。
*/
@@ -90,6 +106,10 @@
coupon_id: cachedCouponId,
status_code
});
+ if (isNotAgreedMessage(res) || isNotAgreedMessage(res?.data) || isNotAgreedMessage(res?.msg)) {
+ deps.handleOpenPrivacyPopup();
+ return null;
+ }
if (typeof res === 'string') {
Taro.showToast({ title: res, icon: 'none', mask: true });
setTimeout(() => {
@@ -150,6 +170,19 @@
deps.setCallSubmitting(true);
const authReady = await checkCallAuth();
if (!authReady) return;
+ try {
+ const agreementResult = await getAgreement();
+ const hasAgreed = resolveAgreementStatus(agreementResult);
+ if (!hasAgreed) {
+ deps.handleOpenPrivacyPopup();
+ stopCallSubmitting();
+ return;
+ }
+ } catch (error) {
+ Taro.showToast({ title: '协议校验失败,请重试', icon: 'none' });
+ stopCallSubmitting();
+ return;
+ }
const routeReady = checkRouteReady();
if (!routeReady) return;
const s = getStartSiteForCall();
@@ -187,6 +220,7 @@
*/
const restartPlanningRoute = () => {
deps.setShowCancelPopup(false);
+ deps.setCancelPopupWaitMinutes(null);
deps.setTripStatus(1);
deps.routeUpdateWalkingRouteForStart(deps.startSite);
const s = Taro.getStorageSync('startSite');
@@ -233,8 +267,14 @@
/**
* 根据状态打开取消弹窗。
*/
- const handleOpenCancelPopupByTripStatus = (status: number, durationDisplay?: any) => {
- if (durationDisplay && ![3, 5].includes(status)) {
+ const handleOpenCancelPopupByTripStatus = (status: number, displayInfo?: any) => {
+ const waitMinutes = Number(displayInfo?.waitMinutes);
+ if (status === 3 && Number.isFinite(waitMinutes)) {
+ deps.setCancelPopupWaitMinutes(waitMinutes);
+ } else {
+ deps.setCancelPopupWaitMinutes(null);
+ }
+ if (displayInfo?.durationDisplay && ![3, 5].includes(status)) {
deps.setCancelType(2);
deps.setShowCancelPopup(true);
return;
diff --git a/src/robotaxi/pages/begin-route/hooks/use-popup-manager.ts b/src/robotaxi/pages/begin-route/hooks/use-popup-manager.ts
index c5843fd..4e409c9 100644
--- a/src/robotaxi/pages/begin-route/hooks/use-popup-manager.ts
+++ b/src/robotaxi/pages/begin-route/hooks/use-popup-manager.ts
@@ -3,6 +3,7 @@
export default function usePopupManager() {
const [showCustomerService, setShowCustomerService] = useState(false);
const [showCancelPopup, setShowCancelPopup] = useState(false);
+ const [cancelPopupWaitMinutes, setCancelPopupWaitMinutes] = useState<number | null>(null);
const [showAnnouncementsPopup, setShowAnnouncementsPopup] = useState(false);
const [showOverRoutePopup, setShowOverRoutePopup] = useState(false);
const [showPassengerNotice, setShowPassengerNotice] = useState(false);
@@ -10,6 +11,7 @@
const [showEvaluatePopup, setShowEvaluatePopup] = useState(false);
const [showTipPopup, setShowTipPopup] = useState(false);
const [tipType, setTipType] = useState('');
+ const [showPrivacyPopup, setShowPrivacyPopup] = useState(false);
const setPopupVisible = (setter: (value: boolean) => void, visible: boolean) => () => setter(visible);
@@ -21,7 +23,10 @@
}, 600);
};
- const handleCloseCancelPopup = setPopupVisible(setShowCancelPopup, false);
+ const handleCloseCancelPopup = () => {
+ setShowCancelPopup(false);
+ setCancelPopupWaitMinutes(null);
+ };
const handleCloseAnnouncementsPopup = setPopupVisible(setShowAnnouncementsPopup, false);
const handleOpenAnnouncementsPopup = setPopupVisible(setShowAnnouncementsPopup, true);
const handleOpenOverRoutePopup = setPopupVisible(setShowOverRoutePopup, true);
@@ -32,6 +37,8 @@
const handleOpenEvaluatePopup = setPopupVisible(setShowEvaluatePopup, true);
const handleCloseEvaluatePopup = setPopupVisible(setShowEvaluatePopup, false);
+ const handleOpenPrivacyPopup = setPopupVisible(setShowPrivacyPopup, true);
+ const handleClosePrivacyPopup = setPopupVisible(setShowPrivacyPopup, false);
return {
showCustomerService,
@@ -39,6 +46,8 @@
handleCloseCustomerService,
showCancelPopup,
setShowCancelPopup,
+ cancelPopupWaitMinutes,
+ setCancelPopupWaitMinutes,
handleCloseCancelPopup,
cancelType,
setCancelType,
@@ -62,6 +71,10 @@
setShowTipPopup,
handleCloseTipPopup,
tipType,
- setTipType
+ setTipType,
+ showPrivacyPopup,
+ setShowPrivacyPopup,
+ handleOpenPrivacyPopup,
+ handleClosePrivacyPopup
};
}
diff --git a/src/robotaxi/pages/begin-route/hooks/use-route-manager.ts b/src/robotaxi/pages/begin-route/hooks/use-route-manager.ts
index c9806f2..fcd3064 100644
--- a/src/robotaxi/pages/begin-route/hooks/use-route-manager.ts
+++ b/src/robotaxi/pages/begin-route/hooks/use-route-manager.ts
@@ -31,6 +31,16 @@
setVehicleDirection: () => { }
};
+// 统一解析接口返回方向
+const resolveDirectionValue = (payload: any) => {
+ const rawDirection = payload?.direction;
+ const direction = Number(rawDirection);
+ if (!Number.isFinite(direction)) {
+ return null;
+ }
+ return ((direction % 360) + 360) % 360;
+};
+
export default function useRouteManager(rawDeps?: Partial<Deps>) {
const deps = { ...defaultDeps, ...(rawDeps || {}) } as Deps;
// 路线渲染线集合
@@ -280,9 +290,9 @@
if (durationSeconds !== null) {
setRouteDuration(durationSeconds);
}
- const dir = Number(res?.direction);
- if (Number.isFinite(dir)) {
- deps.setVehicleDirection(((dir % 360) + 360) % 360);
+ const normalizedDirection = resolveDirectionValue(res);
+ if (typeof normalizedDirection === 'number') {
+ deps.setVehicleDirection(normalizedDirection);
}
})
.catch(() => { });
diff --git a/src/robotaxi/pages/begin-route/hooks/use-vehicle-tracking.ts b/src/robotaxi/pages/begin-route/hooks/use-vehicle-tracking.ts
index e5a8690..514e970 100644
--- a/src/robotaxi/pages/begin-route/hooks/use-vehicle-tracking.ts
+++ b/src/robotaxi/pages/begin-route/hooks/use-vehicle-tracking.ts
@@ -23,6 +23,40 @@
const lastVehiclePointRef = useRef<{ latitude: number; longitude: number } | null>(null);
const lastVehicleUpdateAtRef = useRef<number>(0);
const TRANSLATE_DURATION_MS = 100;
+ const isTranslatingRef = useRef(false); // 当前是否有进行中的平移动画
+ const pendingTranslateRef = useRef<{
+ latitude: number;
+ longitude: number;
+ rotate: number;
+ markerId: number;
+ } | null>(null); // 最新待执行的平移目标(仅保留最后一次)
+
+ // 执行一次平移动画,完成后自动消费最新待执行目标
+ const flushTranslateTask = () => {
+ if (isTranslatingRef.current) return;
+ if (!pendingTranslateRef.current) return;
+ const task = pendingTranslateRef.current;
+ pendingTranslateRef.current = null;
+ isTranslatingRef.current = true;
+ const mapCtx = Taro.createMapContext('map');
+ mapCtx.translateMarker({
+ markerId: task.markerId,
+ destination: {
+ latitude: task.latitude,
+ longitude: task.longitude,
+ },
+ autoRotate: false,
+ moveWithRotate: true,
+ rotate: task.rotate,
+ duration: TRANSLATE_DURATION_MS,
+ complete: () => {
+ isTranslatingRef.current = false;
+ if (pendingTranslateRef.current) {
+ flushTranslateTask();
+ }
+ }
+ });
+ };
// 更新车辆位置并维护最近位置引用
const updateVehicleLocationThrottled = (lat: number, lng: number, force = false) => {
@@ -37,20 +71,21 @@
useEffect(() => {
if (!statusRule.translateCar) return;
if (!vehicleLocation) return;
- const mapCtx = Taro.createMapContext('map');
- mapCtx.translateMarker({
- markerId: vehicleMarkerId,
- destination: {
- latitude: vehicleLocation.latitude,
- longitude: vehicleLocation.longitude,
- },
- autoRotate: false,
- moveWithRotate: true,
+ pendingTranslateRef.current = {
+ latitude: vehicleLocation.latitude,
+ longitude: vehicleLocation.longitude,
rotate: typeof vehicleDirection === 'number' ? ((vehicleDirection % 360) + 360) % 360 : 0,
- duration: TRANSLATE_DURATION_MS
- });
+ markerId: vehicleMarkerId
+ };
+ flushTranslateTask();
}, [statusRule.translateCar, vehicleLocation, vehicleDirection, vehicleMarkerId]);
+ useEffect(() => {
+ if (statusRule.translateCar) return;
+ isTranslatingRef.current = false;
+ pendingTranslateRef.current = null;
+ }, [statusRule.translateCar]);
+
return {
vehicleLocation,
setVehicleLocation,
diff --git a/src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx b/src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx
index 94b5350..3f7090d 100644
--- a/src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx
+++ b/src/robotaxi/pages/begin-route/hooks/use-view-delegates.tsx
@@ -3,7 +3,7 @@
import ReleaseRoute from '../components/release-route';
import ReleaseBeBeing from '../components/release-be-being';
import CallingCar from '../components/calling-car';
-import { formatDurationDisplay } from '../../../../utils/Robotaxi-public-method';
+import { formatRemainingDurationDisplay } from '../../../utils/format';
type Deps = {
/** 是否安卓 */
@@ -74,8 +74,6 @@
evaluateSubmitVersion: number;
/** 评价关闭版本 */
evaluateCloseVersion: number;
- /** 打开公告 */
- handleOpenAnnouncementsPopup: () => void;
/** 打开结束弹窗 */
handleOpenOverRoutePopup: () => void;
/** 支付 */
@@ -93,7 +91,7 @@
*/
export default function useViewDelegates(deps: Deps) {
/** 时长文案 */
- const durationDisplay = formatDurationDisplay(deps.routeDuration);
+ const durationDisplay = formatRemainingDurationDisplay(deps.routeDuration);
/**
* 渲染等待态气泡。
@@ -225,7 +223,6 @@
priceUpdateVersion={deps.priceUpdateVersion}
evaluateSubmitVersion={deps.evaluateSubmitVersion}
evaluateCloseVersion={deps.evaluateCloseVersion}
- handleOpenAnnouncementsPopup={deps.handleOpenAnnouncementsPopup}
handleOpenOverRoutePopup={deps.handleOpenOverRoutePopup}
handlePay={deps.handlePay}
handleOpenEvaluatePopup={deps.handleOpenEvaluatePopup}
diff --git a/src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts b/src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts
index 1a5e5e6..95d1e64 100644
--- a/src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts
+++ b/src/robotaxi/pages/begin-route/hooks/use-web-socket-events.ts
@@ -159,10 +159,20 @@
}
};
+// 统一解析方向值
+const resolveDirectionValue = (payload: any) => {
+ const rawDirection = payload?.content?.direction;
+ const direction = Number(rawDirection);
+ if (!Number.isFinite(direction)) {
+ return null;
+ }
+ return ((direction % 360) + 360) % 360;
+};
+
const updateRouteSummary = (payload: any, deps: UseWebSocketEventsDeps) => {
- const direction = Number(payload?.direction);
- if (Number.isFinite(direction)) {
- deps.setVehicleDirection(((direction % 360) + 360) % 360);
+ const normalizedDirection = resolveDirectionValue(payload);
+ if (typeof normalizedDirection === 'number') {
+ deps.setVehicleDirection(normalizedDirection);
}
const route = safeParse(payload?.content?.route, {});
const distanceKm = normalizeDistanceKm(Number(route?.distance));
diff --git a/src/robotaxi/pages/begin-route/index.tsx b/src/robotaxi/pages/begin-route/index.tsx
index be456fe..d66fc09 100644
--- a/src/robotaxi/pages/begin-route/index.tsx
+++ b/src/robotaxi/pages/begin-route/index.tsx
@@ -49,6 +49,8 @@
handleCloseCustomerService,
showCancelPopup,
setShowCancelPopup,
+ cancelPopupWaitMinutes,
+ setCancelPopupWaitMinutes,
handleCloseCancelPopup,
cancelType,
setCancelType,
@@ -71,7 +73,10 @@
setShowTipPopup,
handleCloseTipPopup,
tipType,
- setTipType
+ setTipType,
+ showPrivacyPopup,
+ handleOpenPrivacyPopup,
+ handleClosePrivacyPopup
} = usePopupManager(); // 弹窗状态编排
const [longitude, setLongitude] = useState<number | null>(null); // 经度
const [latitude, setLatitude] = useState<number | null>(null); // 纬度
@@ -397,11 +402,13 @@
setCallSubmitting,
setCancelType,
setShowCancelPopup,
+ setCancelPopupWaitMinutes,
setWalkingPoints,
setWalkingTime,
setLatitude,
setLongitude,
setShowOverRoutePopup,
+ handleOpenPrivacyPopup,
checkDistanceAndCityReady,
bindSocketListeners,
routeUpdateWalkingRouteForStart,
@@ -486,7 +493,6 @@
priceUpdateVersion,
evaluateSubmitVersion,
evaluateCloseVersion,
- handleOpenAnnouncementsPopup,
handleOpenOverRoutePopup,
handlePay,
handleOpenEvaluatePopup,
@@ -539,6 +545,7 @@
handleCloseCustomerService={handleCloseCustomerService}
showCancelPopup={showCancelPopup}
cancelType={cancelType}
+ cancelPopupWaitMinutes={cancelPopupWaitMinutes}
startSite={startSite}
endSite={endSite}
fareEstimate={fareEstimate}
@@ -554,6 +561,8 @@
showTipPopup={showTipPopup}
handleCloseTipPopup={handleCloseTipPopup}
tipType={tipType}
+ showPrivacyPopup={showPrivacyPopup}
+ handleClosePrivacyPopup={handleClosePrivacyPopup}
showPassengerNotice={showPassengerNotice}
handleClosePassengerNotice={handleClosePassengerNotice}
changeCheckedEnter={changeCheckedEnter}
diff --git a/src/robotaxi/utils/format.ts b/src/robotaxi/utils/format.ts
index 633531b..0f346df 100644
--- a/src/robotaxi/utils/format.ts
+++ b/src/robotaxi/utils/format.ts
@@ -18,3 +18,15 @@
}
return Math.round(num);
};
+
+// 剩余时长展示(begin-route):分钟按向下截断,秒按四舍五入
+export const formatRemainingDurationDisplay = (value: any) => {
+ const num = Number(value);
+ if (Number.isNaN(num)) {
+ return { value: 0, unit: '分钟' as const };
+ }
+ if (num < 60) {
+ return { value: Math.max(0, Math.round(num)), unit: '秒' as const };
+ }
+ return { value: Math.max(0, Math.floor(num / 60)), unit: '分钟' as const };
+};
diff --git a/src/utils/websocket.ts b/src/utils/websocket.ts
index f3a8d38..c5ee781 100644
--- a/src/utils/websocket.ts
+++ b/src/utils/websocket.ts
@@ -18,7 +18,7 @@
private heartbeatTimer: any = null;
private heartbeatTimeoutTimer: any = null; // 用于检测心跳超时的定时器
private heartbeatInterval: number = 10000; // 10秒心跳
- private heartbeatTimeout: number = 3000; // 5秒收不到响应即视为掉线
+ private heartbeatTimeout: number = 3000; // 3秒收不到响应即视为掉线
private listeners: Map<string, MessageCallback[]> = new Map();
private globalEventsBound: boolean = false;
--
Gitblit v1.7.1