From 2ac1335f3819f7085dad672bdcef0f6b10dc9f1e Mon Sep 17 00:00:00 2001
From: hejianhao <15708179461@qq.com>
Date: 星期四, 23 四月 2026 14:57:27 +0800
Subject: [PATCH] feat(home): 添加附近车辆显示与自动缩放功能
---
src/pages/robotaxi/home/hooks/use-nearby-vehicles.ts | 346 ++++++++++++++++++++++++++++++++++
src/robotaxi/constants/subscribe.ts | 36 +++
src/pages/robotaxi/home/hooks/use-map-state.ts | 17 +
src/robotaxi/pages/select-map-site/index.tsx | 18 +
src/robotaxi/pages/begin-route/components/calling-car-modules/fare-detail-card.tsx | 10
src/robotaxi/api/home.ts | 9
src/utils/Robotaxi-public-method.ts | 32 +++
src/pages/robotaxi/home/index.tsx | 52 ++++
src/utils/subscribe.ts | 56 +++++
src/pages/robotaxi/home/hooks/use-region-change.ts | 12 +
src/robotaxi/pages/begin-route/hooks/use-order-actions.ts | 2
11 files changed, 572 insertions(+), 18 deletions(-)
diff --git a/src/pages/robotaxi/home/hooks/use-map-state.ts b/src/pages/robotaxi/home/hooks/use-map-state.ts
index d703760..05188b8 100644
--- a/src/pages/robotaxi/home/hooks/use-map-state.ts
+++ b/src/pages/robotaxi/home/hooks/use-map-state.ts
@@ -1,6 +1,9 @@
import { useRef, useState } from 'react';
import Taro from '@tarojs/taro';
-import { fetchMapData } from '../../../../utils/Robotaxi-public-method';
+import { fetchMapData, getAutoScaleBySites } from '../../../../utils/Robotaxi-public-method';
+
+const AUTO_SCALE_MAX = 17; // 自动缩放最大等级
+const AUTO_SCALE_MIN = 11; // 自动缩放最小等级
/**
* 地图状态管理:拥有地图坐标、围栏、标记点及地图业务数据刷新能力。
@@ -20,6 +23,7 @@
const isReturningToOrigin = useRef(false);
const resetTimer = useRef<any>(null);
const pendingNearbyRequestCountRef = useRef(0);
+ const autoScaleAppliedCityCodeRef = useRef(''); // 已执行自动缩放的城市 code
/**
* 根据经纬度和城市编码刷新地图业务数据(围栏内判断、电子围栏 ID、站点标记)。
@@ -36,6 +40,17 @@
setIsOperatingArea(result.isInside);
setElectronicAreaId(result.electronicAreaId);
setMarkers(result.newMarkers);
+ if (autoScaleAppliedCityCodeRef.current !== code) {
+ const autoScale = getAutoScaleBySites(result?.sites || result?.newMarkers || []);
+ if (typeof autoScale === 'number') {
+ setMapScale((prev) => {
+ const nextScale = Math.max(AUTO_SCALE_MIN, Math.min(AUTO_SCALE_MAX, autoScale));
+ if (Math.abs(prev - nextScale) < 0.01) return prev;
+ return nextScale;
+ });
+ }
+ autoScaleAppliedCityCodeRef.current = code;
+ }
return result;
} finally {
pendingNearbyRequestCountRef.current = Math.max(0, pendingNearbyRequestCountRef.current - 1);
diff --git a/src/pages/robotaxi/home/hooks/use-nearby-vehicles.ts b/src/pages/robotaxi/home/hooks/use-nearby-vehicles.ts
new file mode 100644
index 0000000..e8deca0
--- /dev/null
+++ b/src/pages/robotaxi/home/hooks/use-nearby-vehicles.ts
@@ -0,0 +1,346 @@
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { getNearbyVehicles } from '../../../../robotaxi/api/home';
+import { getDistance } from '../../../../utils/Robotaxi-public-method';
+
+type Deps = {
+ isOperatingArea: boolean;
+};
+
+type NearbyCenter = {
+ lat: number;
+ lon: number;
+};
+
+type NearbyVehicleItem = {
+ id: string;
+ latitude: number;
+ longitude: number;
+ direction: number;
+};
+
+type VehicleTrackItem = {
+ id: string;
+ fromLat: number;
+ fromLon: number;
+ toLat: number;
+ toLon: number;
+ fromDirection: number;
+ toDirection: number;
+};
+
+const POLL_INTERVAL_MS = 5000; // 车辆轮询周期(毫秒)
+const SETTLE_DELAY_MS = 500; // 地图停止后的静止判定时间(毫秒)
+const MOVE_THRESHOLD_METERS = 500; // 触发新搜索的位移阈值(米)
+const ANIMATION_DURATION_MS = 4500; // 车辆位移动画时长(毫秒)
+const ANIMATION_FRAME_MS = 120; // 动画帧间隔(毫秒)
+const VEHICLE_MARKER_ID_BASE = 900000000; // 车辆 marker id 起始段
+const VEHICLE_MARKER_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/marker-car.png'; // 车辆 icon 地址
+
+/**
+ * 归一化角度到 [0, 360) 区间。
+ */
+const normalizeDirection = (value: number) => {
+ const safe = Number(value);
+ if (!Number.isFinite(safe)) return 0;
+ return ((safe % 360) + 360) % 360;
+};
+
+/**
+ * 插值航向角,选择最短旋转路径避免大角度跳变。
+ */
+const interpolateDirection = (from: number, to: number, progress: number) => {
+ const fromSafe = normalizeDirection(from);
+ const toSafe = normalizeDirection(to);
+ const delta = ((((toSafe - fromSafe) % 360) + 540) % 360) - 180;
+ return normalizeDirection(fromSafe + delta * progress);
+};
+
+/**
+ * 将任意字符串稳定映射到正整数,避免与站点 marker id 冲突。
+ */
+const hashToMarkerId = (rawId: string) => {
+ let hash = 0;
+ for (let i = 0; i < rawId.length; i += 1) {
+ hash = (hash << 5) - hash + rawId.charCodeAt(i);
+ hash |= 0;
+ }
+ return VEHICLE_MARKER_ID_BASE + (Math.abs(hash) % 9999999);
+};
+
+/**
+ * 附近车辆编排:处理停稳判定、500 米阈值、5 秒轮询与平滑动画。
+ */
+export default function useNearbyVehicles({ isOperatingArea }: Deps) {
+ const [vehicleMarkers, setVehicleMarkers] = useState<any[]>([]); // 车辆 marker 列表
+ const pollTimerRef = useRef<any>(null); // 轮询定时器句柄
+ const settleTimerRef = useRef<any>(null); // 停稳判定定时器句柄
+ const animationTimerRef = useRef<any>(null); // 动画帧定时器句柄
+ const requestLockRef = useRef(false); // 请求并发锁,防止重入
+ const latestSeqRef = useRef(0); // 请求序号,防止旧响应覆盖新状态
+ const activeSearchCenterRef = useRef<NearbyCenter | null>(null); // 当前轮询中心点
+ const lastSearchCenterRef = useRef<NearbyCenter | null>(null); // 上一次发请求的中心点
+ const renderedVehicleMapRef = useRef<Map<string, NearbyVehicleItem>>(new Map()); // 当前渲染中的车辆
+ const animationStateRef = useRef<{ startAt: number; endAt: number; tracks: VehicleTrackItem[] } | null>(null); // 当前动画轨迹状态
+
+ /**
+ * 清理动画定时器。
+ */
+ const clearAnimationTimer = useCallback(() => {
+ if (animationTimerRef.current) {
+ clearTimeout(animationTimerRef.current);
+ animationTimerRef.current = null;
+ }
+ animationStateRef.current = null;
+ }, []);
+
+ /**
+ * 清理轮询定时器。
+ */
+ const clearPollTimer = useCallback(() => {
+ if (pollTimerRef.current) {
+ clearInterval(pollTimerRef.current);
+ pollTimerRef.current = null;
+ }
+ }, []);
+
+ /**
+ * 清理停稳判定定时器。
+ */
+ const clearSettleTimer = useCallback(() => {
+ if (settleTimerRef.current) {
+ clearTimeout(settleTimerRef.current);
+ settleTimerRef.current = null;
+ }
+ }, []);
+
+ /**
+ * 清空车辆渲染状态。
+ */
+ const clearVehicles = useCallback(() => {
+ renderedVehicleMapRef.current = new Map();
+ setVehicleMarkers([]);
+ clearAnimationTimer();
+ }, [clearAnimationTimer]);
+
+ /**
+ * 停止车辆相关任务并清空车辆。
+ */
+ const disposeVehicles = useCallback(() => {
+ clearSettleTimer();
+ clearPollTimer();
+ clearVehicles();
+ activeSearchCenterRef.current = null;
+ lastSearchCenterRef.current = null;
+ }, [clearPollTimer, clearSettleTimer, clearVehicles]);
+
+ /**
+ * 将车辆位置数据映射为地图 marker。
+ */
+ const buildVehicleMarkers = useCallback((vehicles: NearbyVehicleItem[]) => {
+ return vehicles.map((vehicle) => ({
+ id: hashToMarkerId(vehicle.id),
+ markerType: 'vehicle',
+ vehicleId: vehicle.id,
+ latitude: vehicle.latitude,
+ longitude: vehicle.longitude,
+ rotate: vehicle.direction,
+ width: 20,
+ height: 20,
+ zIndex: 6,
+ anchor: { x: 0.5, y: 0.5 },
+ iconPath: VEHICLE_MARKER_ICON
+ }));
+ }, []);
+
+ /**
+ * 基于插值轨迹推进车辆动画帧。
+ */
+ const playAnimationFrame = useCallback(() => {
+ const animationState = animationStateRef.current;
+ if (!animationState) return;
+ const now = Date.now();
+ const progress = Math.max(0, Math.min(1, (now - animationState.startAt) / Math.max(1, animationState.endAt - animationState.startAt)));
+ const nextVehicleMap = new Map<string, NearbyVehicleItem>();
+ animationState.tracks.forEach((track) => {
+ const lat = track.fromLat + (track.toLat - track.fromLat) * progress;
+ const lon = track.fromLon + (track.toLon - track.fromLon) * progress;
+ const direction = interpolateDirection(track.fromDirection, track.toDirection, progress);
+ nextVehicleMap.set(track.id, {
+ id: track.id,
+ latitude: lat,
+ longitude: lon,
+ direction
+ });
+ });
+ renderedVehicleMapRef.current = nextVehicleMap;
+ setVehicleMarkers(buildVehicleMarkers(Array.from(nextVehicleMap.values())));
+ if (progress >= 1) {
+ animationStateRef.current = null;
+ animationTimerRef.current = null;
+ return;
+ }
+ animationTimerRef.current = setTimeout(() => {
+ playAnimationFrame();
+ }, ANIMATION_FRAME_MS);
+ }, [buildVehicleMarkers]);
+
+ /**
+ * 规范化接口返回,过滤非法经纬度。
+ */
+ const normalizeVehicleList = useCallback((response: any): NearbyVehicleItem[] => {
+ const rawList = Array.isArray(response)
+ ? response
+ : Array.isArray(response?.list)
+ ? response.list
+ : Array.isArray(response?.vehicles)
+ ? response.vehicles
+ : [];
+ return rawList
+ .map((item: any, index: number) => {
+ const latitude = Number(item?.latitude);
+ const longitude = Number(item?.longitude);
+ if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) return null;
+ const rawId = item?.id ?? `vehicle-${index}-${latitude}-${longitude}`;
+ return {
+ id: String(rawId),
+ latitude,
+ longitude,
+ direction: normalizeDirection(Number(item?.direction))
+ };
+ })
+ .filter(Boolean) as NearbyVehicleItem[];
+ }, []);
+
+ /**
+ * 请求车辆并衔接平滑位移动画。
+ */
+ const fetchAndAnimateVehicles = useCallback(async (center: NearbyCenter) => {
+ if (requestLockRef.current) return;
+ requestLockRef.current = true;
+ const seq = latestSeqRef.current + 1;
+ latestSeqRef.current = seq;
+ try {
+ const response = await getNearbyVehicles({ lat: center.lat, lon: center.lon });
+ if (seq !== latestSeqRef.current) return;
+ const normalizedVehicles = normalizeVehicleList(response);
+ if (normalizedVehicles.length === 0) {
+ clearVehicles();
+ return;
+ }
+ const now = Date.now();
+ const currentVehicleMap = renderedVehicleMapRef.current;
+ const tracks: VehicleTrackItem[] = normalizedVehicles.map((vehicle) => {
+ const currentVehicle = currentVehicleMap.get(vehicle.id);
+ return {
+ id: vehicle.id,
+ fromLat: currentVehicle?.latitude ?? vehicle.latitude,
+ fromLon: currentVehicle?.longitude ?? vehicle.longitude,
+ toLat: vehicle.latitude,
+ toLon: vehicle.longitude,
+ fromDirection: currentVehicle?.direction ?? vehicle.direction,
+ toDirection: vehicle.direction
+ };
+ });
+ clearAnimationTimer();
+ animationStateRef.current = {
+ startAt: now,
+ endAt: now + ANIMATION_DURATION_MS,
+ tracks
+ };
+ playAnimationFrame();
+ } catch (error) {
+ // 请求异常时保持当前车辆状态,避免闪断。
+ } finally {
+ requestLockRef.current = false;
+ }
+ }, [clearAnimationTimer, clearVehicles, normalizeVehicleList, playAnimationFrame]);
+
+ /**
+ * 启动 5 秒轮询,轮询中心遵循 activeSearchCenterRef。
+ */
+ const ensurePolling = useCallback(() => {
+ if (pollTimerRef.current) return;
+ pollTimerRef.current = setInterval(() => {
+ const center = activeSearchCenterRef.current;
+ if (!center || !isOperatingArea) {
+ disposeVehicles();
+ return;
+ }
+ fetchAndAnimateVehicles(center);
+ }, POLL_INTERVAL_MS);
+ }, [disposeVehicles, fetchAndAnimateVehicles, isOperatingArea]);
+
+ /**
+ * 激活指定中心点并立即拉起一次车辆查询。
+ */
+ const activateCenterAndFetch = useCallback((center: NearbyCenter) => {
+ activeSearchCenterRef.current = center;
+ lastSearchCenterRef.current = center;
+ fetchAndAnimateVehicles(center);
+ ensurePolling();
+ }, [ensurePolling, fetchAndAnimateVehicles]);
+
+ /**
+ * 首页初始化触发:围栏加载完成且中心点在 ODD 内时立即开始轮询。
+ */
+ const tryStartPollingOnInit = useCallback((payload: { lat: number; lon: number; isInside: boolean }) => {
+ if (!payload?.isInside || !isOperatingArea) return;
+ const lat = Number(payload.lat);
+ const lon = Number(payload.lon);
+ if (!Number.isFinite(lat) || !Number.isFinite(lon)) return;
+ if (lastSearchCenterRef.current) return;
+ activateCenterAndFetch({ lat, lon });
+ }, [activateCenterAndFetch, isOperatingArea]);
+
+ /**
+ * 地图拖动开始:仅清理停稳判定,不发起请求。
+ */
+ const handleRegionBegin = useCallback(() => {
+ clearSettleTimer();
+ }, [clearSettleTimer]);
+
+ /**
+ * 地图拖动结束:静止 500ms 后按 500 米阈值判断是否更新搜索中心。
+ */
+ const handleRegionSettled = useCallback((payload: { lat: number; lon: number; isInside: boolean }) => {
+ clearSettleTimer();
+ settleTimerRef.current = setTimeout(() => {
+ if (!payload?.isInside || !isOperatingArea) {
+ disposeVehicles();
+ return;
+ }
+ const currentCenter = { lat: payload.lat, lon: payload.lon };
+ const lastSearchCenter = lastSearchCenterRef.current;
+ if (!lastSearchCenter) {
+ activateCenterAndFetch(currentCenter);
+ return;
+ }
+ const movedDistance = getDistance(lastSearchCenter.lat, lastSearchCenter.lon, currentCenter.lat, currentCenter.lon);
+ if (movedDistance > MOVE_THRESHOLD_METERS) {
+ activateCenterAndFetch(currentCenter);
+ } else {
+ ensurePolling();
+ }
+ }, SETTLE_DELAY_MS);
+ }, [activateCenterAndFetch, clearSettleTimer, disposeVehicles, ensurePolling, isOperatingArea]);
+
+ useEffect(() => {
+ if (!isOperatingArea) {
+ disposeVehicles();
+ }
+ }, [disposeVehicles, isOperatingArea]);
+
+ useEffect(() => {
+ return () => {
+ disposeVehicles();
+ };
+ }, [disposeVehicles]);
+
+ return {
+ vehicleMarkers,
+ handleRegionBegin,
+ handleRegionSettled,
+ tryStartPollingOnInit,
+ disposeVehicles
+ };
+}
diff --git a/src/pages/robotaxi/home/hooks/use-region-change.ts b/src/pages/robotaxi/home/hooks/use-region-change.ts
index 7ad3a18..ab1cfa0 100644
--- a/src/pages/robotaxi/home/hooks/use-region-change.ts
+++ b/src/pages/robotaxi/home/hooks/use-region-change.ts
@@ -5,12 +5,14 @@
mapState: any;
siteState: any;
toOrigin: () => void;
+ onRegionBegin?: () => void;
+ onRegionSettled?: (payload: { lat: number; lon: number; isInside: boolean }) => void;
};
/**
* 地图 region-change 编排:处理地图拖拽/缩放后的吸附、回填和自动归位。
*/
-export default function useRegionChange({ mapState, siteState, toOrigin }: Deps) {
+export default function useRegionChange({ mapState, siteState, toOrigin, onRegionBegin, onRegionSettled }: Deps) {
/**
* 通过中心点主动触发吸附判断。
*/
@@ -63,7 +65,10 @@
return;
}
try {
- const { newMarkers, sites } = await mapState.updateMapData(lat, lng);
+ const { isInside, newMarkers, sites } = await mapState.updateMapData(lat, lng);
+ if (typeof onRegionSettled === 'function') {
+ onRegionSettled({ lat, lon: lng, isInside: !!isInside });
+ }
const nearestCandidates = sites.length > 0 ? sites : newMarkers;
const { adsorbed, marker } = siteState.tryAdsorb(lat, lng, nearestCandidates);
if ((causedBy === 'gesture' || causedBy === 'drag' || wasReturningToOrigin) && marker) {
@@ -106,6 +111,9 @@
}
if (type === 'begin') {
siteState.setShowToolTip(false);
+ if (typeof onRegionBegin === 'function') {
+ onRegionBegin();
+ }
return;
}
if (type !== 'end') return;
diff --git a/src/pages/robotaxi/home/index.tsx b/src/pages/robotaxi/home/index.tsx
index 97e39cc..6af729c 100644
--- a/src/pages/robotaxi/home/index.tsx
+++ b/src/pages/robotaxi/home/index.tsx
@@ -1,4 +1,4 @@
-import Taro, { useDidShow } from "@tarojs/taro"
+import Taro, { useDidHide, useDidShow } from "@tarojs/taro"
import { useEffect, useState, useRef, useMemo } from 'react';
import { View, Map, Text, Image, Button } from '@tarojs/components';
import './index.less';
@@ -46,6 +46,8 @@
import useHomeBootstrap from './hooks/use-home-bootstrap';
// 快捷胶囊编排
import useQuickCapsule from './hooks/use-quick-capsule';
+// 附近车辆编排
+import useNearbyVehicles from './hooks/use-nearby-vehicles';
import { DEFAULT_LATITUDE, DEFAULT_LONGITUDE, FIRST_ENTER_KEY } from '../../../robotaxi/constants/map';
import { TripStatus, toTripStatus } from '../../../robotaxi/pages/begin-route/constants/trip-status';
import {
@@ -86,7 +88,7 @@
const {
longitude,
latitude,
- markers,
+ markers: siteMarkers,
polygons,
mapScale,
isOperatingArea,
@@ -380,12 +382,49 @@
useDidShow(() => {
bootstrap.onDidShow();
refreshQuickCapsule();
+ if (typeof latitude === 'number' && typeof longitude === 'number') {
+ tryStartPollingOnInit({
+ lat: latitude,
+ lon: longitude,
+ isInside: isOperatingArea
+ });
+ }
});
+
+ useDidHide(() => {
+ disposeVehicles();
+ });
+
+ const {
+ vehicleMarkers,
+ handleRegionBegin: handleVehicleRegionBegin,
+ handleRegionSettled: handleVehicleRegionSettled,
+ tryStartPollingOnInit,
+ disposeVehicles
+ } = useNearbyVehicles({
+ isOperatingArea
+ }); // 附近车辆编排
+
+ useEffect(() => {
+ if (nearbyLoading || isLocationAuthDenied) return;
+ if (typeof latitude !== 'number' || typeof longitude !== 'number') return;
+ tryStartPollingOnInit({
+ lat: latitude,
+ lon: longitude,
+ isInside: isOperatingArea
+ });
+ }, [nearbyLoading, isLocationAuthDenied, latitude, longitude, isOperatingArea, tryStartPollingOnInit]);
+
+ const mergedMapMarkers = useMemo(() => {
+ return [...siteMarkers, ...vehicleMarkers];
+ }, [siteMarkers, vehicleMarkers]); // 地图合并 marker 列表
const { handleRegionChange, queryPointerByCenter } = useRegionChange({
mapState,
siteState,
- toOrigin
+ toOrigin,
+ onRegionBegin: handleVehicleRegionBegin,
+ onRegionSettled: handleVehicleRegionSettled
}); // 地图 region-change 编排
const {
ensureNetwork,
@@ -426,8 +465,11 @@
const markerId = e?.detail?.markerId;
if (markerId === undefined) return;
- const targetMarker = markers.find(m => m.id === markerId);
+ const targetMarker = mergedMapMarkers.find(m => m.id === markerId);
if (targetMarker) {
+ if (targetMarker.markerType === 'vehicle') {
+ return;
+ }
// 忽略因调用 moveToLocation 引起的 region change,避免重复吸附计算
shouldIgnoreRegionChange.current = true;
@@ -457,7 +499,7 @@
longitude={longitude ?? DEFAULT_LONGITUDE}
latitude={latitude ?? DEFAULT_LATITUDE}
scale={mapScale}
- markers={isLocationAuthDenied ? [] : markers.map(m => ({ ...m, title: '', name: '' }))}
+ markers={isLocationAuthDenied ? [] : mergedMapMarkers.map(m => ({ ...m, title: '', name: '' }))}
polygons={polygons}
showLocation={!!longitude}
onRegionChange={handleRegionChange}
diff --git a/src/robotaxi/api/home.ts b/src/robotaxi/api/home.ts
index d1403ac..51a79a1 100644
--- a/src/robotaxi/api/home.ts
+++ b/src/robotaxi/api/home.ts
@@ -101,3 +101,12 @@
payload
}, true);
}
+
+// 获取附近车辆
+export const getNearbyVehicles = async (payload: { lat: number; lon: number }) => {
+ return request({
+ url: _ROBOTAXI_HOST + '/vehicleConvoy/api/v1/mini-program/vehicle/nearby',
+ method: 'GET',
+ payload
+ }, true);
+}
diff --git a/src/robotaxi/constants/subscribe.ts b/src/robotaxi/constants/subscribe.ts
new file mode 100644
index 0000000..cedb0d7
--- /dev/null
+++ b/src/robotaxi/constants/subscribe.ts
@@ -0,0 +1,36 @@
+// 订阅场景枚举
+export enum SubscribeSceneKey {
+ OrderSuccess = 'ORDER_CREATE_SUCCESS',
+ OperationCouponGroup = 'OPERATION_COUPON_GROUP',
+}
+
+// 订阅场景配置类型
+export type SubscribeSceneConfig = {
+ sceneKey: SubscribeSceneKey;
+ tmplIds: string[];
+ limit: number;
+ needLogin: boolean;
+};
+
+// 下单成功订阅模板配置
+export const ORDER_SUCCESS_SUBSCRIBE_CONFIG: SubscribeSceneConfig = {
+ sceneKey: SubscribeSceneKey.OrderSuccess,
+ tmplIds: [
+ 'NOi0ff4rpuTxcRnnZl0SJxg9SMbvrc8xB8VJgrwu3oo',
+ 'elyV30kO0BZAD2OtihJ4Wdu7fwR3FdvCZYCM1BEbqWs'
+ ],
+ limit: 1,
+ needLogin: true
+};
+
+// 运营活动 + 券效期 + 券到账订阅模板配置
+export const OPERATION_COUPON_SUBSCRIBE_CONFIG: SubscribeSceneConfig = {
+ sceneKey: SubscribeSceneKey.OperationCouponGroup,
+ tmplIds: [
+ 'S6gGP8jyuDTkW03h8LhOm2KVDdUtHr0OhL-Mt_PyRPU',
+ 'ANtToR63JPCV_ujV69gmO15xCa8O3elnLWB52HLdcHM',
+ 'P7z_ksdyzpiiOBO07EJ1AZ6WeHlWs4XveQYjmt9JRE8'
+ ],
+ limit: 1,
+ needLogin: true
+};
diff --git a/src/robotaxi/pages/begin-route/components/calling-car-modules/fare-detail-card.tsx b/src/robotaxi/pages/begin-route/components/calling-car-modules/fare-detail-card.tsx
index dfaf358..5149013 100644
--- a/src/robotaxi/pages/begin-route/components/calling-car-modules/fare-detail-card.tsx
+++ b/src/robotaxi/pages/begin-route/components/calling-car-modules/fare-detail-card.tsx
@@ -68,7 +68,7 @@
行程费用
</View>
{
- fareBreakdown.operateFree == 1 ?
+ fareBreakdown?.operateFree == 1 ?
<View className="car-card-end-particulars-free">
载客测试免费
</View>
@@ -163,15 +163,15 @@
</View>
}
</View>
- <View className="car-card-end-cost">
+ <View className="car-card-end-cost" onClick={toMoneyDetail}>
<View className="car-card-end-cost-money">
全程{formatTripDistance(fareBreakdown?.kilometer)}|{formatTripDurationFromSeconds(fareBreakdown?.time)}
</View>
<Image src={require('../../../../assets/ccc-icon-right.png')} />
</View>
{
- (fareBreakdown?.redPacketMoney + fareBreakdown?.couponMoney > 0 && fareBreakdown.operateFree != 1) &&
- <View className="car-card-end-through" onClick={toMoneyDetail}>
+ (fareBreakdown?.redPacketMoney + fareBreakdown?.couponMoney > 0 && fareBreakdown?.operateFree != 1) &&
+ <View className="car-card-end-through">
<View className="car-card-end-through-label">优惠活动</View>
<View className="car-card-end-through-content">
<View className="car-card-end-through-content-title">最佳优惠</View>
@@ -185,7 +185,7 @@
tripStatus === TripStatus.TripFinished &&
<View className="pay-box">
{
- fareBreakdown.operateFree != 1 ?
+ fareBreakdown?.operateFree != 1 ?
<View className="pay-box-money"><View>0.00</View><View className="unit">元</View></View>
:
<View className="pay-box-free">载客测试免费</View>
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 979defd..3f44898 100644
--- a/src/robotaxi/pages/begin-route/hooks/use-order-actions.ts
+++ b/src/robotaxi/pages/begin-route/hooks/use-order-actions.ts
@@ -3,6 +3,7 @@
import { saveOrder, getOrderPayment, cancelOrder, endOrder, routeStaleCheck } from '../../../api/begin-route';
import { checkOtp } from '../../../../utils/tools';
import { jumpTo } from '../../../../utils/jump';
+import { subscribeAfterOrderSuccess } from '../../../../utils/subscribe';
import { TripStatus } from '../constants/trip-status';
type Deps = {
@@ -211,6 +212,7 @@
deps.setOrderId(nextOrderId);
deps.setTripStatus(TripStatus.Publishing);
deps.bindSocketListeners();
+ void subscribeAfterOrderSuccess(nextOrderId);
} catch (error) {
stopCallSubmitting();
}
diff --git a/src/robotaxi/pages/select-map-site/index.tsx b/src/robotaxi/pages/select-map-site/index.tsx
index ffa9384..8c54ae9 100644
--- a/src/robotaxi/pages/select-map-site/index.tsx
+++ b/src/robotaxi/pages/select-map-site/index.tsx
@@ -16,6 +16,7 @@
import noStar from '../../../robotaxi/assets/no-star.png'
import {
findNearestMarker,
+ getAutoScaleBySites,
getDistance,
formatGeofencePolygons,
fetchMapData,
@@ -32,10 +33,13 @@
import { jumpTo } from '../../../utils/jump';
export default function SelectMapSite() {
+ const AUTO_SCALE_MAX = 17; // 自动缩放最大等级
+ const AUTO_SCALE_MIN = 11; // 自动缩放最小等级
const [showCustomerService, setShowCustomerService] = useState(false); // 客服弹窗是否显示
const [longitude, setLongitude] = useState<number | null>(null); // 经度
const [latitude, setLatitude] = useState<number | null>(null); // 纬度
+ const [mapScale, setMapScale] = useState(14); // 地图缩放等级
const [showToolTip, setShowToolTip] = useState(true); // 提示是否显示
const [bounce, setBounce] = useState(false); // 是否弹跳
const [markers, setMarkers] = useState<any[]>([]); // 标记点列表
@@ -60,6 +64,7 @@
const waitingRadarSrc = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/waiting-radar.webp';
const [nearbyLoading, setNearbyLoading] = useState(false);
const pendingNearbyRequestCountRef = useRef(0);
+ const autoScaleLockedRef = useRef(false); // 自动缩放锁:仅初始化计算一次
const router = Taro.useRouter();
@@ -191,6 +196,17 @@
setIsOperatingArea(isInside);
setMarkers(newMarkers);
setNearbySites(sites);
+ if (!autoScaleLockedRef.current) {
+ const autoScale = getAutoScaleBySites(sites?.length > 0 ? sites : newMarkers);
+ if (typeof autoScale === 'number') {
+ setMapScale((prev) => {
+ const nextScale = Math.max(AUTO_SCALE_MIN, Math.min(AUTO_SCALE_MAX, autoScale));
+ if (Math.abs(prev - nextScale) < 0.01) return prev;
+ return nextScale;
+ });
+ }
+ autoScaleLockedRef.current = true;
+ }
return { isInside, newMarkers, sites };
} finally {
@@ -540,7 +556,7 @@
id="map"
longitude={longitude}
latitude={latitude}
- scale={14}
+ scale={mapScale}
markers={markers.map(m => ({ ...m, title: '', name: '' }))}
polyline={polyline}
polygons={polygons}
diff --git a/src/utils/Robotaxi-public-method.ts b/src/utils/Robotaxi-public-method.ts
index bd46806..1caf11b 100644
--- a/src/utils/Robotaxi-public-method.ts
+++ b/src/utils/Robotaxi-public-method.ts
@@ -36,6 +36,38 @@
};
/**
+ * 根据站点空间分布估算推荐缩放等级(站点越分散,缩放越小)。
+ */
+export const getAutoScaleBySites = (sites: any[]) => {
+ if (!Array.isArray(sites) || sites.length === 0) return null;
+ if (sites.length === 1) return 16;
+ let minLat = Number.POSITIVE_INFINITY;
+ let maxLat = Number.NEGATIVE_INFINITY;
+ let minLng = Number.POSITIVE_INFINITY;
+ let maxLng = Number.NEGATIVE_INFINITY;
+ sites.forEach((site) => {
+ const lat = Number(site?.latitude);
+ const lng = Number(site?.longitude);
+ if (!Number.isFinite(lat) || !Number.isFinite(lng)) return;
+ if (lat < minLat) minLat = lat;
+ if (lat > maxLat) maxLat = lat;
+ if (lng < minLng) minLng = lng;
+ if (lng > maxLng) maxLng = lng;
+ });
+ if (!Number.isFinite(minLat) || !Number.isFinite(maxLat) || !Number.isFinite(minLng) || !Number.isFinite(maxLng)) {
+ return null;
+ }
+ const diagonalMeters = getDistance(minLat, minLng, maxLat, maxLng);
+ if (diagonalMeters > 12000) return 11;
+ if (diagonalMeters > 8000) return 12;
+ if (diagonalMeters > 5000) return 13;
+ if (diagonalMeters > 3000) return 14;
+ if (diagonalMeters > 1500) return 15;
+ if (diagonalMeters > 800) return 16;
+ return 17;
+};
+
+/**
* 计算点 P 在线段 AB 上的投影点(吸附算法)
* @param p 真实点 {latitude, longitude}
* @param a 线段起点 {latitude, longitude}
diff --git a/src/utils/subscribe.ts b/src/utils/subscribe.ts
index 5d6cda2..15d1d92 100644
--- a/src/utils/subscribe.ts
+++ b/src/utils/subscribe.ts
@@ -6,6 +6,11 @@
import Taro from "@tarojs/taro";
import { checkOtp } from "./tools";
+import {
+ OPERATION_COUPON_SUBSCRIBE_CONFIG,
+ ORDER_SUCCESS_SUBSCRIBE_CONFIG,
+ SubscribeSceneConfig
+} from "../robotaxi/constants/subscribe";
// 需要频控的订阅字段,用于清空订阅次数
const subscribeFields = [
@@ -26,9 +31,8 @@
export async function addSubscribeCount(type, tmplIds, limit = 3, needlogin = true) {
const isLogin = await checkOtp();
if (!isLogin && needlogin) return false;
- const userInfo = Taro.getStorageSync(type);
- console.log(userInfo, "userInfo");
- Taro.setStorageSync(type, userInfo + 1 || 1);
+ const userInfo = Number(Taro.getStorageSync(type) || 0);
+ Taro.setStorageSync(type, userInfo + 1);
return new Promise((resolve, reject) => {
Taro.getSetting({
withSubscriptions: true,
@@ -83,4 +87,48 @@
return true;
}
})
-}
\ No newline at end of file
+}
+
+// 生成订阅频控 key
+export function buildSubscribeType(sceneKey: string, bizId?: string | number) {
+ if (bizId === undefined || bizId === null || bizId === '') {
+ return sceneKey;
+ }
+ return `${sceneKey}_${bizId}`;
+}
+
+// 执行通用订阅流程(失败/拒绝不影响主流程)
+export async function runSubscribeScene(config: SubscribeSceneConfig, bizId?: string | number) {
+ try {
+ const type = buildSubscribeType(config.sceneKey, bizId);
+ const canSubscribe = await addSubscribeCount(type, config.tmplIds, config.limit, config.needLogin);
+ if (!canSubscribe) return;
+ await new Promise<void>((resolve) => {
+ Taro.requestSubscribeMessage({
+ tmplIds: config.tmplIds,
+ success: (result) => {
+ checkSubscribeStatus(type, config.tmplIds, result, config.limit);
+ resolve();
+ },
+ fail: () => {
+ resolve();
+ },
+ complete: () => {
+ resolve();
+ }
+ });
+ });
+ } catch (error) {
+ }
+}
+
+// 下单成功后订阅消息
+export async function subscribeAfterOrderSuccess(orderId: string) {
+ if (!orderId) return;
+ await runSubscribeScene(ORDER_SUCCESS_SUBSCRIBE_CONFIG, orderId);
+}
+
+// 运营活动 + 券效期 + 券到账三合一模板订阅
+export async function subscribeOperationCouponGroup(bizId?: string | number) {
+ await runSubscribeScene(OPERATION_COUPON_SUBSCRIBE_CONFIG, bizId);
+}
--
Gitblit v1.7.1