zjk
2026-04-17 1871a12b89e4e423fc4d5bd9f5aafd5bae88b3ef
feat(share-trip): 新增分享行程页面地图相关钩子函数
5个文件已添加
3个文件已修改
1987 ■■■■■ 已修改文件
src/robotaxi/api/begin-route.ts 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/begin-route/components/calling-car.tsx 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/share-trip/hooks/use-map-markers.ts 260 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/share-trip/hooks/use-map-viewport.ts 289 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/share-trip/hooks/use-route-manager.ts 149 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/share-trip/hooks/use-vehicle-tracking.ts 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/share-trip/hooks/use-view-delegates.tsx 182 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/pages/share-trip/index.tsx 987 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/robotaxi/api/begin-route.ts
@@ -194,3 +194,20 @@
        payload
    }, true);
}
// 分享订单查询实时轨迹路线
export const shareQueryOrderRoute = async (orderId) => {
    return request({
        url: _ROBOTAXI_HOST + `/order/base/v1/mini-program/orderActive/share/queryOrderRoute/${orderId}`,
        method: 'GET'
    }, true);
}
// 分享订单查询订单详情
export const shareQueryOrderInfo = async (payload) => {
    return request({
        url: _ROBOTAXI_HOST + '/order/base/v1/mini-program/orderActive/share/orderInfo',
        method: 'GET',
        payload
    }, true);
}
src/robotaxi/pages/begin-route/components/calling-car.tsx
@@ -134,13 +134,6 @@
                    <TemperatureControl tripStatus={tripStatus} orderId={orderId} />
                    <TripActionBar
                        tripStatus={tripStatus}
                        handleCancelTrip={handleCancelTrip}
                        handleModifyDestination={handleModifyDestination}
                        handleShareTrip={handleShareTrip}
                    />
                    <DoorTrunkControl
                        tripStatus={tripStatus}
                        orderId={orderId}
@@ -157,6 +150,13 @@
                        onToggleDetailFade={onToggleDetailFade}
                        handlePay={handlePay}
                    />
                    <TripActionBar
                        tripStatus={tripStatus}
                        handleCancelTrip={handleCancelTrip}
                        handleModifyDestination={handleModifyDestination}
                        handleShareTrip={handleShareTrip}
                    />
                </View>
            }
src/robotaxi/pages/share-trip/hooks/use-map-markers.ts
New file
@@ -0,0 +1,260 @@
import Taro from "@tarojs/taro";
import { useEffect, useRef, useState } from "react";
type Deps = {
    tripStatus: number;
    statusRule: any;
    isAndroid: boolean;
    startSite: any;
    endSite: any;
    vehicleLocation: { latitude: number; longitude: number } | null;
    vehicleDirection: number | null;
    vehicleMarkerId: number;
};
const defaultDeps: Deps = {
    tripStatus: 1,
    statusRule: { showPickup: true, showDropoff: true, showCar: false },
    isAndroid: false,
    startSite: null,
    endSite: null,
    vehicleLocation: null,
    vehicleDirection: null,
    vehicleMarkerId: 3
};
export default function useMapMarkers(rawDeps?: Partial<Deps>) {
    const deps = { ...defaultDeps, ...(rawDeps || {}) } as Deps;
    const [markers, setMarkers] = useState<any[]>([]);
    const markersRef = useRef<any[]>([]);
    const markersSignatureRef = useRef<string>("");
    const carMarkerInitialized = useRef(false);
    const tripStatus6MarkerRefreshedRef = useRef(false);
    const createPickupMarker = (
        pickupSite: any,
        useAndroidNativeTrip1Callout: boolean
    ) => {
        if (!(pickupSite?.latitude && pickupSite?.longitude)) return null;
        return {
            id: 1,
            latitude: pickupSite.latitude,
            longitude: pickupSite.longitude,
            iconPath: "https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/maker-up.png",
            width: 22,
            height: 40,
            anchor: { x: 0.5, y: 1 },
            zIndex: 1,
            name: pickupSite?.name || "上车点",
            ...(useAndroidNativeTrip1Callout
                ? {
                      callout: {
                          content:
                              deps.tripStatus === 1
                                  ? `${pickupSite?.name || "上车点"} >`
                                  : pickupSite?.name || "上车点",
                          color: "#000000",
                          fontSize: 12,
                          bgColor: "#FFFFFF",
                          borderRadius: 8,
                          padding: 8,
                          display: "ALWAYS",
                          textAlign: "center"
                      }
                  }
                : {
                      customCallout: {
                          anchorY: [2, 3].includes(deps.tripStatus) ? 150 : -5,
                          anchorX: 0,
                          display: "ALWAYS"
                      }
                  })
        };
    };
    const createDropoffMarker = (
        dropoffSite: any,
        useAndroidNativeTrip1Callout: boolean
    ) => {
        if (!(dropoffSite?.latitude && dropoffSite?.longitude)) return null;
        const dropoffName =
            [7].includes(deps.tripStatus) ? "已到达" : dropoffSite.name || "目的地";
        return {
            id: 2,
            latitude: dropoffSite.latitude,
            longitude: dropoffSite.longitude,
            iconPath:
                "https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/maker-down.png",
            width: 22,
            height: 40,
            anchor: { x: 0.5, y: 1 },
            zIndex: 1,
            name: dropoffName,
            ...(useAndroidNativeTrip1Callout || deps.tripStatus == 6
                ? {
                      callout: {
                          content:
                              deps.tripStatus === 1
                                  ? `${dropoffName} >`
                                  : dropoffName,
                          color: "#000000",
                          fontSize: 12,
                          bgColor: "#FFFFFF",
                          borderRadius: 8,
                          padding: 8,
                          display: "ALWAYS",
                          textAlign: "center"
                      }
                  }
                : {
                      customCallout: {
                          anchorY: -5,
                          anchorX: 0,
                          display: "ALWAYS"
                      }
                  })
        };
    };
    const createCarMarker = () => {
        if (!deps.vehicleLocation) return null;
        return {
            id: deps.vehicleMarkerId,
            latitude: deps.vehicleLocation.latitude,
            longitude: deps.vehicleLocation.longitude,
            iconPath: "https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/marker-car.png",
            width: 20,
            height: 40,
            anchor: { x: 0.5, y: 0.5 },
            zIndex: 2,
            rotate:
                typeof deps.vehicleDirection === "number"
                    ? deps.vehicleDirection
                    : 0,
            name: "车辆位置",
            customCallout: { anchorY: 70, anchorX: 0, display: "ALWAYS" }
        };
    };
    const applyMarkerState = (
        nextMarkers: any[],
        nextMarkersSignature: string,
        shouldSkipUpdate: boolean,
        carMarker: any
    ) => {
        if (!shouldSkipUpdate && markersSignatureRef.current !== nextMarkersSignature) {
            markersSignatureRef.current = nextMarkersSignature;
            setMarkers(nextMarkers);
        }
        if (
            !deps.isAndroid &&
            [4, 6].includes(deps.tripStatus) &&
            deps.statusRule.showCar &&
            !!carMarker &&
            !tripStatus6MarkerRefreshedRef.current
        ) {
            tripStatus6MarkerRefreshedRef.current = true;
        }
        carMarkerInitialized.current = deps.statusRule.showCar && !!carMarker;
    };
    useEffect(() => {
        markersRef.current = markers;
    }, [markers]);
    useEffect(() => {
        const pickupSite =
            deps.startSite?.latitude && deps.startSite?.longitude
                ? deps.startSite
                : Taro.getStorageSync("startSite");
        const dropoffSite =
            deps.endSite?.latitude && deps.endSite?.longitude
                ? deps.endSite
                : Taro.getStorageSync("endSite");
        const useAndroidNativeTrip1Callout =
            deps.isAndroid && [1, 5, 8, 9, 10].includes(deps.tripStatus);
        const pickupMarker = createPickupMarker(
            pickupSite,
            useAndroidNativeTrip1Callout
        );
        const dropoffMarker = createDropoffMarker(
            dropoffSite,
            useAndroidNativeTrip1Callout
        );
        const carMarker = createCarMarker();
        const currentCarMarker = markersRef.current.find(
            (item: any) => item.id === deps.vehicleMarkerId
        );
        const nextMarkers: any[] = [];
        if (deps.statusRule.showPickup && pickupMarker) nextMarkers.push(pickupMarker);
        if (deps.statusRule.showCar && carMarker) nextMarkers.push(carMarker);
        if (deps.statusRule.showDropoff && dropoffMarker) nextMarkers.push(dropoffMarker);
        const nextMarkersSignature = nextMarkers
            .map((item: any) =>
                [
                    item.id,
                    Number(item.latitude).toFixed(6),
                    Number(item.longitude).toFixed(6),
                    item.name || "",
                    typeof item.rotate === "number" ? item.rotate : "",
                    item?.customCallout?.anchorY ?? ""
                ].join("|")
            )
            .join(";");
        const hasDropoffMarker = markersRef.current.some((item: any) => item.id === 2);
        const hasCarRotateChanged = Boolean(
            carMarker &&
                currentCarMarker &&
                currentCarMarker.rotate !== carMarker.rotate
        );
        const shouldHoldMarkerByTranslate =
            [4, 6].includes(deps.tripStatus) &&
            carMarkerInitialized.current &&
            deps.statusRule.showCar &&
            (!deps.statusRule.showDropoff || hasDropoffMarker);
        const shouldHoldMarkerWhenArrived =
            deps.tripStatus === 7 &&
            carMarkerInitialized.current &&
            deps.statusRule.showCar &&
            (!deps.statusRule.showDropoff || hasDropoffMarker);
        // 车辆位置不变但方向变化时,也需要刷新 marker 朝向。
        const shouldSkipUpdate =
            !hasCarRotateChanged &&
            ((shouldHoldMarkerByTranslate &&
                (deps.isAndroid || tripStatus6MarkerRefreshedRef.current)) ||
                shouldHoldMarkerWhenArrived);
        applyMarkerState(
            nextMarkers,
            nextMarkersSignature,
            shouldSkipUpdate,
            carMarker
        );
    }, [
        deps.tripStatus,
        deps.startSite?.name,
        deps.startSite?.latitude,
        deps.startSite?.longitude,
        deps.endSite?.name,
        deps.endSite?.latitude,
        deps.endSite?.longitude,
        deps.vehicleLocation,
        deps.vehicleDirection,
        deps.vehicleMarkerId,
        deps.isAndroid,
        deps.statusRule.showPickup,
        deps.statusRule.showCar,
        deps.statusRule.showDropoff
    ]);
    return {
        markers,
        setMarkers,
        markersRef,
        markersSignatureRef,
        carMarkerInitialized,
        tripStatus6MarkerRefreshedRef
    };
}
src/robotaxi/pages/share-trip/hooks/use-map-viewport.ts
New file
@@ -0,0 +1,289 @@
import Taro from "@tarojs/taro";
import { useCallback, useEffect, useRef, useState } from "react";
import { computeAdaptiveBottomPadding } from "../../../../utils/Robotaxi-public-method";
type Deps = {
    tripStatus: number;
    statusRule: any;
    routePoints: any[];
    walkingPoints: any[];
    startSite: any;
    endSite: any;
    vehicleLocation: { latitude: number; longitude: number } | null;
    longitude: number | null;
    latitude: number | null;
};
const defaultDeps: Deps = {
    tripStatus: 1,
    statusRule: { viewport: "pickup" },
    routePoints: [],
    walkingPoints: [],
    startSite: null,
    endSite: null,
    vehicleLocation: null,
    longitude: null,
    latitude: null
};
const DROPOFF_MAX_SCALE = 20;
export default function useMapViewport(rawDeps?: Partial<Deps>) {
    const deps = { ...defaultDeps, ...(rawDeps || {}) } as Deps;
    const [mapBottomPadding, setMapBottomPadding] = useState(280);
    const [mapScale, setMapScale] = useState(14);
    const viewportKeyRef = useRef<string>("");
    const tripStatus6ViewportLockedRef = useRef(false);
    const resolveSafeBottomInset = () => {
        try {
            const info = Taro.getSystemInfoSync();
            const screenHeight = info?.screenHeight;
            const safeBottom = info?.safeArea?.bottom;
            if (Number.isFinite(screenHeight) && Number.isFinite(safeBottom)) {
                return Math.max(0, screenHeight - safeBottom);
            }
        } catch (error) {
            return 0;
        }
        return 0;
    };
    const resolveSafeTopInset = () => {
        try {
            const info = Taro.getSystemInfoSync();
            const safeTop = info?.safeArea?.top;
            if (Number.isFinite(safeTop)) {
                return Math.max(0, safeTop);
            }
        } catch (error) {
            return 0;
        }
        return 0;
    };
    const computeBottomPadding = useCallback(
        (points: any[], viewportType: string) => {
            const basePadding =
                viewportType === "order_detail"
                    ? Math.max(200, mapBottomPadding)
                    : mapBottomPadding;
            return computeAdaptiveBottomPadding(points, basePadding);
        },
        [mapBottomPadding]
    );
    const updateMapViewportPadding = useCallback(() => {
        Taro.nextTick(() => {
            const query = Taro.createSelectorQuery();
            query.select(".container").boundingClientRect();
            query.exec((res) => {
                const rect = res?.[0];
                const safeInset = resolveSafeBottomInset();
                const measured = rect?.height ? rect.height + safeInset + 16 : 0;
                const nextPadding = Math.max(280, Math.round(measured));
                setMapBottomPadding(nextPadding);
            });
        });
    }, []);
    const resolveViewportSites = () => {
        const pickupSite =
            deps.startSite?.latitude && deps.startSite?.longitude
                ? deps.startSite
                : Taro.getStorageSync("startSite");
        const dropoffSite =
            deps.endSite?.latitude && deps.endSite?.longitude
                ? deps.endSite
                : Taro.getStorageSync("endSite");
        return { pickupSite, dropoffSite };
    };
    const buildViewportPayload = (
        viewportType: string,
        pickupSite: any,
        dropoffSite: any
    ) => {
        const points: any[] = [];
        const keyParts: any[] = [
            viewportType === "order_detail" ? 8 : deps.tripStatus,
            viewportType
        ];
        if (viewportType === "route_walking") {
            if (deps.routePoints.length > 0) points.push(...deps.routePoints);
            if (deps.walkingPoints.length > 0) points.push(...deps.walkingPoints);
            keyParts.push(deps.routePoints.length, deps.walkingPoints.length);
            return { points, keyParts };
        }
        if (viewportType === "pickup") {
            if (pickupSite?.latitude && pickupSite?.longitude) {
                points.push({
                    latitude: pickupSite.latitude,
                    longitude: pickupSite.longitude
                });
            }
            keyParts.push(pickupSite?.latitude, pickupSite?.longitude);
            return { points, keyParts };
        }
        if (viewportType === "route") {
            if (deps.routePoints.length > 0) points.push(...deps.routePoints);
            keyParts.push(deps.routePoints.length);
            return { points, keyParts };
        }
        if (viewportType === "car_user") {
            if (Number.isFinite(deps.latitude) && Number.isFinite(deps.longitude)) {
                points.push({
                    latitude: deps.latitude,
                    longitude: deps.longitude
                });
            }
            if (deps.vehicleLocation?.latitude && deps.vehicleLocation?.longitude) {
                points.push({
                    latitude: deps.vehicleLocation.latitude,
                    longitude: deps.vehicleLocation.longitude
                });
            }
            keyParts.push(
                deps.latitude,
                deps.longitude,
                deps.vehicleLocation?.latitude,
                deps.vehicleLocation?.longitude
            );
            return { points, keyParts };
        }
        if (viewportType === "dropoff") {
            if (dropoffSite?.latitude && dropoffSite?.longitude) {
                points.push({
                    latitude: dropoffSite.latitude,
                    longitude: dropoffSite.longitude
                });
            }
            keyParts.push(dropoffSite?.latitude, dropoffSite?.longitude);
            return { points, keyParts };
        }
        if (viewportType === "order_detail") {
            if (deps.routePoints.length > 0) points.push(...deps.routePoints);
            if (pickupSite?.latitude && pickupSite?.longitude) {
                points.push({
                    latitude: pickupSite.latitude,
                    longitude: pickupSite.longitude
                });
            }
            if (dropoffSite?.latitude && dropoffSite?.longitude) {
                points.push({
                    latitude: dropoffSite.latitude,
                    longitude: dropoffSite.longitude
                });
            }
            keyParts.push(
                deps.routePoints.length,
                pickupSite?.latitude,
                pickupSite?.longitude,
                dropoffSite?.latitude,
                dropoffSite?.longitude
            );
        }
        return { points, keyParts };
    };
    const updateViewportByPayload = useCallback(
        (viewportType: string, points: any[]) => {
            const mapCtx = Taro.createMapContext("map");
            const bottomPadding = computeBottomPadding(points, viewportType);
            const baseTopPadding = viewportType === "order_detail" ? 60 : 24;
            const topPadding = Math.max(baseTopPadding, resolveSafeTopInset() + 32);
            const padding =
                viewportType === "order_detail"
                    ? [topPadding, 50, bottomPadding, 50]
                    : [topPadding, 100, bottomPadding, 100];
            Taro.nextTick(() => {
                if (viewportType === "dropoff") {
                    mapCtx.moveToLocation({
                        latitude: points[0].latitude,
                        longitude: points[0].longitude
                    });
                    setTimeout(() => {
                        setMapScale(DROPOFF_MAX_SCALE);
                    }, 50);
                    return;
                }
                mapCtx.includePoints({ points, padding });
            });
        },
        [computeBottomPadding]
    );
    useEffect(() => {
        const viewportType = deps.statusRule.viewport;
        if (viewportType !== "dropoff") return;
        const { pickupSite, dropoffSite } = resolveViewportSites();
        const { points, keyParts } = buildViewportPayload(
            viewportType,
            pickupSite,
            dropoffSite
        );
        if (points.length === 0) return;
        const nextKey = keyParts.join("|");
        if (nextKey === viewportKeyRef.current) return;
        viewportKeyRef.current = nextKey;
        updateViewportByPayload(viewportType, points);
    }, [deps.tripStatus, deps.statusRule?.viewport, deps.endSite?.latitude, deps.endSite?.longitude, updateViewportByPayload]);
    useEffect(() => {
        const { pickupSite, dropoffSite } = resolveViewportSites();
        const viewportType = deps.statusRule.viewport;
        if (viewportType === "dropoff") return;
        const { points, keyParts } = buildViewportPayload(
            viewportType,
            pickupSite,
            dropoffSite
        );
        if (points.length === 0) return;
        if (
            deps.tripStatus === 6 &&
            viewportType === "route" &&
            tripStatus6ViewportLockedRef.current
        ) {
            return;
        }
        const nextKey = keyParts.join("|");
        if (nextKey === viewportKeyRef.current) return;
        viewportKeyRef.current = nextKey;
        updateViewportByPayload(viewportType, points);
        if (deps.tripStatus === 6 && viewportType === "route") {
            tripStatus6ViewportLockedRef.current = true;
        }
    }, [
        deps.tripStatus,
        deps.statusRule,
        deps.routePoints,
        deps.walkingPoints,
        deps.startSite?.latitude,
        deps.startSite?.longitude,
        deps.endSite?.latitude,
        deps.endSite?.longitude,
        deps.vehicleLocation,
        deps.longitude,
        deps.latitude,
        mapBottomPadding,
        updateViewportByPayload
    ]);
    return {
        mapScale,
        mapBottomPadding,
        updateMapViewportPadding,
        resolveSafeTopInset,
        computeBottomPadding
    };
}
src/robotaxi/pages/share-trip/hooks/use-route-manager.ts
New file
@@ -0,0 +1,149 @@
import { useEffect, useRef, useState } from "react";
type Deps = {
    isAndroid: boolean;
    tripStatus: number;
    statusRule: any;
    shouldShowRoute: boolean;
};
const defaultDeps: Deps = {
    isAndroid: false,
    tripStatus: 1,
    statusRule: { showWalking: false },
    shouldShowRoute: false
};
export default function useRouteManager(rawDeps?: Partial<Deps>) {
    const deps = { ...defaultDeps, ...(rawDeps || {}) } as Deps;
    const [polyline, setPolyline] = useState<any[]>([]);
    const [routePoints, setRoutePoints] = useState<any[]>([]);
    const [walkingPoints, setWalkingPoints] = useState<any[]>([]);
    const [walkingTime, setWalkingTime] = useState<number>(0);
    const [routeDistance, setRouteDistance] = useState<{
        value: number;
        unit: "km" | "m";
    } | null>(null);
    const [routeDuration, setRouteDuration] = useState<number | null>(null);
    const polylineKeyRef = useRef<string>("");
    const applyRoutePoints = (points: any[]) => {
        setRoutePoints(Array.isArray(points) ? points : []);
        return true;
    };
    const reducePoints = (points: any[], maxCount: number) => {
        if (!Array.isArray(points) || points.length <= maxCount) {
            return points;
        }
        const step = Math.ceil(points.length / maxCount);
        const reduced = [];
        for (let i = 0; i < points.length; i += step) {
            reduced.push(points[i]);
        }
        const lastPoint = points[points.length - 1];
        if (reduced[reduced.length - 1] !== lastPoint) {
            reduced.push(lastPoint);
        }
        return reduced;
    };
    const getDisplayRoutePoints = () => {
        const isTranslateMode = [4, 6].includes(deps.tripStatus);
        return isTranslateMode
            ? routePoints
            : reducePoints(routePoints, deps.isAndroid ? 120 : 200);
    };
    const buildPolylineKey = (displayRoutePoints: any[]) => {
        const lastRoutePoint = displayRoutePoints[displayRoutePoints.length - 1];
        const lastWalkingPoint = walkingPoints[walkingPoints.length - 1];
        return [
            deps.shouldShowRoute ? 1 : 0,
            deps.statusRule.showWalking ? 1 : 0,
            displayRoutePoints.length,
            walkingPoints.length,
            lastRoutePoint?.latitude ?? "",
            lastRoutePoint?.longitude ?? "",
            lastWalkingPoint?.latitude ?? "",
            lastWalkingPoint?.longitude ?? ""
        ].join("|");
    };
    const buildPolylineLines = (displayRoutePoints: any[]) => {
        const lines: any[] = [];
        if (displayRoutePoints.length > 0) {
            lines.push({
                id: 2001,
                points: displayRoutePoints,
                color: "#7B91FF",
                width: 10,
                clickable: true,
                dottedLine: false,
                arrowLine: true
            });
        }
        if (deps.statusRule.showWalking && walkingPoints.length > 0) {
            lines.push({
                id: 2002,
                points: walkingPoints,
                color: "#7B91FF",
                width: 6,
                dottedLine: true,
                arrowLine: false
            });
        }
        return lines;
    };
    useEffect(() => {
        const displayRoutePoints = getDisplayRoutePoints();
        const nextKey = buildPolylineKey(displayRoutePoints);
        if (nextKey === polylineKeyRef.current) return;
        polylineKeyRef.current = nextKey;
        const shouldDraw =
            deps.shouldShowRoute &&
            (displayRoutePoints.length > 0 ||
                (deps.statusRule.showWalking && walkingPoints.length > 0));
        if (shouldDraw) {
            setPolyline(buildPolylineLines(displayRoutePoints));
            return;
        }
        if (![8, 9].includes(deps.tripStatus)) {
            setPolyline([]);
        }
    }, [
        deps.tripStatus,
        deps.shouldShowRoute,
        deps.statusRule.showWalking,
        deps.isAndroid,
        routePoints,
        walkingPoints
    ]);
    return {
        polyline,
        setPolyline,
        routePoints,
        setRoutePoints,
        walkingPoints,
        setWalkingPoints,
        walkingTime,
        setWalkingTime,
        routeDistance,
        setRouteDistance,
        routeDuration,
        setRouteDuration,
        applyRoutePoints,
        reducePoints,
        buildPolylineKey,
        buildPolylineLines
    };
}
src/robotaxi/pages/share-trip/hooks/use-vehicle-tracking.ts
New file
@@ -0,0 +1,89 @@
import Taro from "@tarojs/taro";
import { useEffect, useRef, useState } from "react";
type Deps = {
    statusRule: any;
    vehicleMarkerId: number;
};
export default function useVehicleTracking({
    statusRule,
    vehicleMarkerId
}: Deps) {
    // 车辆当前位置
    const [vehicleLocation, setVehicleLocation] = useState<{
        latitude: number;
        longitude: number;
    } | null>(null);
    // 车辆沿线移动轨迹
    const [vehicleMoveAlongPoints, setVehicleMoveAlongPoints] = useState<any[]>(
        []
    );
    // 车辆朝向
    const [vehicleDirection, setVehicleDirection] = useState<number | null>(
        null
    );
    // 车辆信息
    const [vehicleInfo, setVehicleInfo] = useState<any>({});
    // 后备箱状态
    const [trunkStatus, setTrunkStatus] = useState(false);
    const lastVehiclePointRef = useRef<{
        latitude: number;
        longitude: number;
    } | null>(null);
    const lastVehicleUpdateAtRef = useRef<number>(0);
    const TRANSLATE_DURATION_MS = 100;
    // 更新车辆位置并维护最近位置引用
    const updateVehicleLocationThrottled = (
        lat: number,
        lng: number,
        force = false
    ) => {
        const now = Date.now();
        if (!force && !Number.isFinite(lat) && !Number.isFinite(lng)) {
            return;
        }
        if (Number.isFinite(lat) && Number.isFinite(lng)) {
            lastVehiclePointRef.current = { latitude: lat, longitude: lng };
            lastVehicleUpdateAtRef.current = now;
            setVehicleLocation({ latitude: lat, longitude: lng });
        }
    };
    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,
            rotate:
                typeof vehicleDirection === "number"
                    ? ((vehicleDirection % 360) + 360) % 360
                    : 0,
            duration: TRANSLATE_DURATION_MS
        });
    }, [statusRule.translateCar, vehicleLocation, vehicleDirection, vehicleMarkerId]);
    return {
        vehicleLocation,
        setVehicleLocation,
        vehicleMoveAlongPoints,
        setVehicleMoveAlongPoints,
        vehicleDirection,
        setVehicleDirection,
        vehicleInfo,
        setVehicleInfo,
        trunkStatus,
        setTrunkStatus,
        lastVehiclePointRef,
        lastVehicleUpdateAtRef,
        updateVehicleLocationThrottled
    };
}
src/robotaxi/pages/share-trip/hooks/use-view-delegates.tsx
New file
@@ -0,0 +1,182 @@
import { CoverImage, CoverView, Image, View } from "@tarojs/components";
import { formatDurationDisplay } from "../../../../utils/Robotaxi-public-method";
type Deps = {
    isAndroid: boolean;
    tripStatus: number;
    mapScale: number;
    markers: any[];
    shouldShowWaitingRadar?: boolean;
    waitingRadarVisible?: boolean;
    waitingRadarStyle?: any;
    waitingRadarSrc?: string;
    carCalloutVisible?: boolean;
    carCalloutStyle?: any;
    arriveCalloutVisible?: boolean;
    arriveCalloutStyle?: any;
    routeDistance: { value: number; unit: "km" | "m" } | null;
    routeDuration: number | null;
    customCalloutIconSrc: string;
};
export default function useViewDelegates(deps: Deps) {
    const durationDisplay = formatDurationDisplay(deps.routeDuration);
    const routeSummaryText =
        deps.routeDistance && deps.routeDuration !== null
            ? `${durationDisplay.value}${durationDisplay.unit} ${deps.routeDistance.value}${
                  deps.routeDistance.unit === "m" ? "米" : "公里"
              }`
            : "";
    const renderWaitingCallout = (item: any, calloutKey: string) => (
        <CoverView
            slot="callout"
            marker-id={item.id}
            key={calloutKey}
            className="waiting-callout"
        >
            <CoverView className="waiting-callout-content">
                <CoverView
                    className={`waiting-callout-txt ${!deps.isAndroid && "pr-6"}`}
                >
                    {item.name}
                    {!deps.isAndroid && (
                        <CoverView style={{ color: "transparent" }}>占</CoverView>
                    )}
                </CoverView>
            </CoverView>
            <CoverImage className="waiting-radar" src={deps.waitingRadarSrc || ""} />
        </CoverView>
    );
    const renderDefaultCallout = (item: any, calloutKey: string) => (
        <CoverView
            slot="callout"
            marker-id={item.id}
            key={calloutKey}
            className="custom-callout"
        >
            <>
                {item.name !== "车辆位置" &&
                    !(item.id === 2 && [7].includes(deps.tripStatus)) && (
                        <CoverView
                            className={`custom-callout-content ${
                                deps.isAndroid && "txt-center "
                            }`}
                            style={{ zIndex: 1 }}
                        >
                            <CoverView
                                style={{
                                    lineHeight: "22px",
                                    display: "flex",
                                    alignItems: "center"
                                }}
                            >
                                {item.name}
                            </CoverView>
                            {deps.isAndroid && <CoverView style={{ width: "2px" }}></CoverView>}
                            {deps.tripStatus == 1 && (
                                <CoverImage
                                    className="custom-callout-icon"
                                    src={deps.customCalloutIconSrc}
                                />
                            )}
                        </CoverView>
                    )}
                {item.name === "车辆位置" &&
                    [4, 6].includes(deps.tripStatus) &&
                    !deps.isAndroid && (
                        <CoverView
                            className="custom-callout-content"
                            style={{
                                padding: "6px 10px",
                                minWidth: "40px",
                                justifyContent: "center",
                                zIndex: 2
                            }}
                        >
                            <CoverView className="car-time">
                                <CoverView className="car-time-num">
                                    {deps.routeDistance?.value ?? 0}
                                </CoverView>
                                <CoverView>
                                    {deps.routeDistance?.unit === "m" ? "米" : "公里"}
                                </CoverView>
                                <CoverView
                                    style={{ marginLeft: "2.5px" }}
                                    className="car-time-num"
                                >
                                    {durationDisplay.value}
                                </CoverView>
                                <CoverView>{durationDisplay.unit}</CoverView>
                            </CoverView>
                        </CoverView>
                    )}
                {item.id === 2 &&
                    [7].includes(deps.tripStatus) &&
                    !deps.isAndroid &&
                    deps.mapScale >= 20 && (
                        <CoverView className="custom-callout-content">
                            已到达
                        </CoverView>
                    )}
            </>
        </CoverView>
    );
    const renderMarkerCallout = (item: any, index: number) => {
        const calloutKey = `callout-${item.id}-${item.name || ""}`;
        const isWaiting = [2, 3].includes(deps.tripStatus) && index === 0;
        if (isWaiting) return renderWaitingCallout(item, calloutKey);
        if (item.name === "车辆位置" && deps.isAndroid && [4, 6].includes(deps.tripStatus)) {
            return null;
        }
        return renderDefaultCallout(item, calloutKey);
    };
    const renderMapOverlays = () => (
        <>
            {!(deps.isAndroid && [1, 5, 8, 9, 10].includes(deps.tripStatus)) &&
                deps.markers.map(renderMarkerCallout)}
            {deps.shouldShowWaitingRadar && deps.waitingRadarVisible && (
                <Image
                    className="waiting-radar-img"
                    src={deps.waitingRadarSrc || ""}
                    webp={true}
                    style={deps.waitingRadarStyle || {}}
                />
            )}
            {deps.isAndroid && deps.carCalloutVisible && (
                <View className="car-callout-android" style={deps.carCalloutStyle || {}}>
                    <View className="custom-callout-content" style={{ padding: "6px 10px" }}>
                        <View className="car-time" style={{ alignItems: "flex-end" }}>
                            <View className="car-time-num">
                                {deps.routeDistance?.value ?? 0}
                            </View>
                            <View style={{ flexShrink: 0 }}>
                                {deps.routeDistance?.unit === "m" ? "米" : "公里"}
                            </View>
                            <View style={{ marginLeft: "2.5px" }} className="car-time-num">
                                {durationDisplay.value}
                            </View>
                            <View style={{ flexShrink: 0 }}>{durationDisplay.unit}</View>
                        </View>
                    </View>
                </View>
            )}
            {deps.isAndroid && deps.arriveCalloutVisible && (
                <View
                    className="car-callout-android"
                    style={deps.arriveCalloutStyle || {}}
                >
                    <View className="custom-callout-content">已到达</View>
                </View>
            )}
        </>
    );
    return {
        durationDisplay,
        renderMapOverlays
    };
}
src/robotaxi/pages/share-trip/index.tsx
@@ -1,6 +1,11 @@
import Taro from "@tarojs/taro";
import { View, Image, Map } from "@tarojs/components";
import Taro, { useRouter } from "@tarojs/taro";
import { View, Image, Map, CoverView, CoverImage } from "@tarojs/components";
import { useState, useEffect, useMemo, useRef } from "react";
import {
  formatDurationDisplay,
  mapOrderStatusToTripStatus,
  safeParse
} from "../../../utils/Robotaxi-public-method";
import "./index.less";
import homeIcon from "../../assets/back-home.png"; // 占位,建议换成首页小房子图标
@@ -8,44 +13,141 @@
import contactServiceIcon from "../../assets/contact-service.png"; // 客服占位图
import shareLine from "../../assets/share-line.png"; // 分享线占位图
// 地图标记状态管理
import useMapMarkers from '../begin-route/hooks/use-map-markers';
// 车辆追踪状态管理
import useVehicleTracking from '../begin-route/hooks/use-vehicle-tracking';
// 路线渲染状态管理
import useRouteManager from "../begin-route/hooks/use-route-manager";
// 地图覆盖物渲染委托
import useViewDelegates from "../begin-route/hooks/use-view-delegates";
// 分享页本地 hooks,避免依赖 begin-route 页面实现
import useMapMarkers from "./hooks/use-map-markers";
import useVehicleTracking from "./hooks/use-vehicle-tracking";
import useRouteManager from "./hooks/use-route-manager";
import useMapViewport from "./hooks/use-map-viewport";
import useViewDelegates from "./hooks/use-view-delegates";
import {
  shareQueryOrderInfo,
  shareQueryOrderRoute
} from "../../api/begin-route";
import {
  normalizeDistanceKm,
  normalizeDurationSeconds
} from "../../utils/format";
export default function ShareTrip() {
  const router = useRouter();
  const { orderId = "" } = router.params || {};
  const [tripData, setTripData] = useState<any>(null);
  const [longitude, setLongitude] = useState(113.32452);
  const [latitude, setLatitude] = useState(23.10229);
  const [startSite, setStartSite] = useState<any>(null); // 上车点
  const [endSite, setEndSite] = useState<any>({}); // 下车点
  const [isExpired, setIsExpired] = useState(false); // 是否已过期(大于24小时)
  const [showReminder, setShowReminder] = useState(true); // 模拟是否显示提醒
  const [tripStatus, setTripStatus] = useState<number>(1); // 行程状态 1:发布行程 2:发布中 3:呼叫车中 4:车辆即将到达 5:车辆到达上车点 6:车辆行驶中 7:到达目的地 8:行程已结束 9:支付成功 10:取消订单时页面只展示一个上车点
  const [showReminder, setShowReminder] = useState(false);
  const [tripStatus, setTripStatus] = useState<number>(1);
  const tripStatusRef = useRef(tripStatus);
  const startSiteRef = useRef<any>(null);
  const endSiteRef = useRef<any>({});
  const infoStatusRef = useRef<number | null>(null);
  const errorToastShownRef = useRef(false);
  // 行程状态显示规则配置:showPickup-显示上车点,showDropoff-显示下车点,showCar-显示车辆,showRoute-显示路线,showWalking-显示步行路线,viewport-视口类型,translateCar-车辆是否平移
  const STATUS_RULES: Record<number, any> = {
    1: { showPickup: true, showDropoff: true, showCar: false, showRoute: true, showWalking: true, viewport: 'route_walking', translateCar: false },
    2: { showPickup: true, showDropoff: false, showCar: false, showRoute: false, showWalking: false, viewport: 'pickup', translateCar: false },
    3: { showPickup: true, showDropoff: false, showCar: false, showRoute: false, showWalking: false, viewport: 'pickup', translateCar: false },
    4: { showPickup: true, showDropoff: false, showCar: true, showRoute: true, showWalking: false, viewport: 'route', translateCar: true },
    5: { showPickup: true, showDropoff: false, showCar: true, showRoute: false, showWalking: false, viewport: 'car_user', translateCar: true },
    6: { showPickup: false, showDropoff: true, showCar: true, showRoute: true, showWalking: false, viewport: 'route', translateCar: true },
    7: { showPickup: false, showDropoff: true, showCar: false, showRoute: false, showWalking: false, viewport: 'dropoff', translateCar: false },
    8: { showPickup: true, showDropoff: true, showCar: false, showRoute: true, showWalking: false, viewport: 'order_detail', translateCar: false },
    9: { showPickup: true, showDropoff: true, showCar: false, showRoute: true, showWalking: false, viewport: 'order_detail', translateCar: false },
    10: { showPickup: true, showDropoff: false, showCar: false, showRoute: false, showWalking: false, viewport: 'pickup', translateCar: false },
    1: {
      showPickup: true,
      showDropoff: true,
      showCar: false,
      showRoute: true,
      showWalking: true,
      viewport: "route_walking",
      translateCar: false
    },
    2: {
      showPickup: true,
      showDropoff: false,
      showCar: false,
      showRoute: false,
      showWalking: false,
      viewport: "pickup",
      translateCar: false
    },
    3: {
      showPickup: true,
      showDropoff: false,
      showCar: false,
      showRoute: false,
      showWalking: false,
      viewport: "pickup",
      translateCar: false
    },
    4: {
      showPickup: true,
      showDropoff: false,
      showCar: true,
      showRoute: true,
      showWalking: false,
      viewport: "route",
      translateCar: true
    },
    5: {
      showPickup: true,
      showDropoff: false,
      showCar: true,
      showRoute: false,
      showWalking: false,
      viewport: "car_user",
      translateCar: true
    },
    6: {
      showPickup: false,
      showDropoff: true,
      showCar: true,
      showRoute: true,
      showWalking: false,
      viewport: "route",
      translateCar: true
    },
    7: {
      showPickup: false,
      showDropoff: true,
      showCar: false,
      showRoute: false,
      showWalking: false,
      viewport: "dropoff",
      translateCar: false
    },
    8: {
      showPickup: true,
      showDropoff: true,
      showCar: false,
      showRoute: true,
      showWalking: false,
      viewport: "order_detail",
      translateCar: false
    },
    9: {
      showPickup: true,
      showDropoff: true,
      showCar: false,
      showRoute: true,
      showWalking: false,
      viewport: "order_detail",
      translateCar: false
    },
    10: {
      showPickup: true,
      showDropoff: false,
      showCar: false,
      showRoute: false,
      showWalking: false,
      viewport: "pickup",
      translateCar: false
    }
  };
  const statusRule = useMemo(() => STATUS_RULES[tripStatus] || STATUS_RULES[1], [tripStatus]); // 当前行程状态对应的显示规则
  const statusRule = useMemo(
    () => STATUS_RULES[tripStatus] || STATUS_RULES[1],
    [tripStatus]
  ); // 当前行程状态对应的显示规则
  const shouldShowRoute = statusRule.showRoute;
  const isAndroid = useMemo(() => {
    try {
      const info = Taro.getSystemInfoSync();
      return info?.platform === 'android' || /android/i.test(info?.system || '');
      return (
        info?.platform === "android" || /android/i.test(info?.system || "")
      );
    } catch (error) {
      return false;
    }
@@ -54,43 +156,39 @@
  const mapLatitude = latitude ?? 39.916527;
  const vehicleMarkerId = !isAndroid && tripStatus === 6 ? 30 : 3;
  const vehicleState = useVehicleTracking({
      statusRule,
      vehicleMarkerId
    }); // 车辆追踪状态容器
    const {
      vehicleLocation,
      vehicleDirection,
      setVehicleDirection,
      updateVehicleLocationThrottled,
    } = vehicleState; // 车辆状态解构
    statusRule,
    vehicleMarkerId
  }); // 车辆追踪状态容器
  const {
    vehicleLocation,
    vehicleDirection,
    setVehicleDirection,
    updateVehicleLocationThrottled
  } = vehicleState; // 车辆状态解构
   const markerState = useMapMarkers({
      tripStatus,
      statusRule,
      isAndroid,
      startSite,
      endSite,
      vehicleLocation,
      vehicleDirection,
      vehicleMarkerId
    }); // 地图标记状态容器
    const {
      markers
    } = markerState; // 标记状态解构
  const markerState = useMapMarkers({
    tripStatus,
    statusRule,
    isAndroid,
    startSite,
    endSite,
    vehicleLocation,
    vehicleDirection,
    vehicleMarkerId
  }); // 地图标记状态容器
  const { markers } = markerState; // 标记状态解构
  const routeState = useRouteManager({
    isAndroid,
    tripStatus,
    tripStatusRef,
    statusRule,
    shouldShowRoute,
    startSite,
    updateVehicleLocationThrottled,
    setVehicleDirection
    shouldShowRoute
  });
  const {
    polyline,
    applyRoutePoints,
    routePoints,
    walkingPoints,
    routeDistance,
    routeDuration,
    setRouteDistance,
@@ -100,67 +198,10 @@
  const updateVehicleLocationRef = useRef(updateVehicleLocationThrottled);
  const setVehicleDirectionRef = useRef(setVehicleDirection);
  const mockScene = useMemo(() => {
    const drivingTrack = [
      { longitude: 104.055501, latitude: 30.586747 }, { longitude: 104.05551, latitude: 30.586678 }, { longitude: 104.055537, latitude: 30.58657 }, { longitude: 104.055548, latitude: 30.58656 }, { longitude: 104.055821, latitude: 30.586565 }, { longitude: 104.056191, latitude: 30.58657 },
      { longitude: 104.056191, latitude: 30.58657 }, { longitude: 104.056202, latitude: 30.586426 }, { longitude: 104.056218, latitude: 30.586189 }, { longitude: 104.056234, latitude: 30.585938 }, { longitude: 104.056256, latitude: 30.58568 }, { longitude: 104.056261, latitude: 30.585562 },
      { longitude: 104.056261, latitude: 30.585562 }, { longitude: 104.055741, latitude: 30.58554 }, { longitude: 104.055467, latitude: 30.585524 }, { longitude: 104.05477, latitude: 30.585492 }, { longitude: 104.054501, latitude: 30.585476 }, { longitude: 104.054405, latitude: 30.585476 },
      { longitude: 104.053869, latitude: 30.585438 }, { longitude: 104.053488, latitude: 30.585417 }, { longitude: 104.053359, latitude: 30.585412 }, { longitude: 104.053074, latitude: 30.585396 }, { longitude: 104.05117, latitude: 30.585288 }, { longitude: 104.050693, latitude: 30.585267 },
      { longitude: 104.050328, latitude: 30.585245 }, { longitude: 104.050221, latitude: 30.58524 }, { longitude: 104.049604, latitude: 30.585208 }, { longitude: 104.049046, latitude: 30.585176 }, { longitude: 104.048853, latitude: 30.585165 }, { longitude: 104.04859, latitude: 30.585154 },
      { longitude: 104.048354, latitude: 30.585133 }, { longitude: 104.048273, latitude: 30.585122 }, { longitude: 104.048134, latitude: 30.585117 }, { longitude: 104.048005, latitude: 30.585133 }, { longitude: 104.047565, latitude: 30.585111 }, { longitude: 104.046616, latitude: 30.585068 },
      { longitude: 104.046616, latitude: 30.585068 }, { longitude: 104.046637, latitude: 30.585938 }, { longitude: 104.046643, latitude: 30.586061 }, { longitude: 104.046669, latitude: 30.586366 }, { longitude: 104.046723, latitude: 30.586753 }, { longitude: 104.046798, latitude: 30.587128 },
      { longitude: 104.046911, latitude: 30.587777 }, { longitude: 104.046954, latitude: 30.587965 }, { longitude: 104.046986, latitude: 30.58811 }, { longitude: 104.046653, latitude: 30.588319 }, { longitude: 104.046653, latitude: 30.588319 }, { longitude: 104.045741, latitude: 30.588276 },
      { longitude: 104.044942, latitude: 30.588255 }, { longitude: 104.043156, latitude: 30.588212 }, { longitude: 104.042185, latitude: 30.588191 }, { longitude: 104.041493, latitude: 30.588169 }, { longitude: 104.040527, latitude: 30.588137 }, { longitude: 104.039921, latitude: 30.588121 },
      { longitude: 104.038483, latitude: 30.588089 }, { longitude: 104.037518, latitude: 30.588089 }, { longitude: 104.037298, latitude: 30.588083 }, { longitude: 104.037121, latitude: 30.588072 }, { longitude: 104.035951, latitude: 30.588051 }, { longitude: 104.035565, latitude: 30.588072 },
      { longitude: 104.035511, latitude: 30.588078 }, { longitude: 104.0352, latitude: 30.58811 }, { longitude: 104.034599, latitude: 30.588164 }, { longitude: 104.034213, latitude: 30.588212 }, { longitude: 104.033843, latitude: 30.588276 }, { longitude: 104.033382, latitude: 30.5884 },
      { longitude: 104.033382, latitude: 30.5884 }, { longitude: 104.033452, latitude: 30.588587 }, { longitude: 104.033602, latitude: 30.58892 }, { longitude: 104.033763, latitude: 30.589279 }, { longitude: 104.034015, latitude: 30.590776 }, { longitude: 104.034026, latitude: 30.591709 },
      { longitude: 104.034031, latitude: 30.592219 }, { longitude: 104.033966, latitude: 30.593786 }, { longitude: 104.033961, latitude: 30.59459 }, { longitude: 104.033977, latitude: 30.595604 }, { longitude: 104.033961, latitude: 30.595931 }, { longitude: 104.033972, latitude: 30.596924 },
      { longitude: 104.033972, latitude: 30.596951 }, { longitude: 104.033972, latitude: 30.596951 }, { longitude: 104.034423, latitude: 30.596972 }, { longitude: 104.034423, latitude: 30.596972 }, { longitude: 104.034423, latitude: 30.596924 }, { longitude: 104.034433, latitude: 30.596768 },
      { longitude: 104.034444, latitude: 30.595443 }, { longitude: 104.034449, latitude: 30.594295 }, { longitude: 104.034481, latitude: 30.594172 }, { longitude: 104.034594, latitude: 30.594022 }, { longitude: 104.034755, latitude: 30.593871 }, { longitude: 104.034755, latitude: 30.593871 },
      { longitude: 104.033339, latitude: 30.592729 }, { longitude: 104.032915, latitude: 30.592417 }, { longitude: 104.032792, latitude: 30.592326 }, { longitude: 104.03262, latitude: 30.592187 }, { longitude: 104.032084, latitude: 30.591758 }, { longitude: 104.031402, latitude: 30.591211 },
      { longitude: 104.031343, latitude: 30.591168 }, { longitude: 104.030957, latitude: 30.590857 }, { longitude: 104.030769, latitude: 30.590706 }, { longitude: 104.030281, latitude: 30.590309 }, { longitude: 104.030169, latitude: 30.590218 }, { longitude: 104.030104, latitude: 30.590164 },
      { longitude: 104.02975, latitude: 30.58988 }, { longitude: 104.028828, latitude: 30.58914 }, { longitude: 104.027964, latitude: 30.588448 }, { longitude: 104.027175, latitude: 30.587809 }, { longitude: 104.026794, latitude: 30.587509 }, { longitude: 104.026639, latitude: 30.587386 },
      { longitude: 104.026639, latitude: 30.587386 }, { longitude: 104.026623, latitude: 30.587327 }, { longitude: 104.026558, latitude: 30.587219 }, { longitude: 104.026172, latitude: 30.586806 }, { longitude: 104.02614, latitude: 30.586764 }, { longitude: 104.026124, latitude: 30.586704 },
      { longitude: 104.02518, latitude: 30.585938 }, { longitude: 104.020545, latitude: 30.582214 }, { longitude: 104.019338, latitude: 30.581244 }, { longitude: 104.018721, latitude: 30.580745 }, { longitude: 104.018554, latitude: 30.580611 }, { longitude: 104.017111, latitude: 30.579468 },
      { longitude: 104.016731, latitude: 30.579194 }, { longitude: 104.016307, latitude: 30.578921 }, { longitude: 104.015937, latitude: 30.578701 }, { longitude: 104.015556, latitude: 30.578508 }, { longitude: 104.015175, latitude: 30.57832 }, { longitude: 104.014628, latitude: 30.578073 },
      { longitude: 104.014, latitude: 30.577848 }, { longitude: 104.013217, latitude: 30.577617 }, { longitude: 104.012664, latitude: 30.577483 }, { longitude: 104.011731, latitude: 30.577306 }, { longitude: 104.003899, latitude: 30.575938 }, { longitude: 104.002306, latitude: 30.575659 },
      { longitude: 104.001474, latitude: 30.575509 }, { longitude: 103.998926, latitude: 30.575085 }, { longitude: 103.998213, latitude: 30.574951 }, { longitude: 103.996582, latitude: 30.574661 }, { longitude: 103.995933, latitude: 30.574549 }, { longitude: 103.982232, latitude: 30.572146 },
      { longitude: 103.979244, latitude: 30.57162 }, { longitude: 103.978327, latitude: 30.571464 }, { longitude: 103.977592, latitude: 30.571351 }, { longitude: 103.977093, latitude: 30.571292 }, { longitude: 103.975339, latitude: 30.570965 }, { longitude: 103.974609, latitude: 30.570831 },
      { longitude: 103.973644, latitude: 30.570665 }, { longitude: 103.972834, latitude: 30.570536 }, { longitude: 103.972024, latitude: 30.570413 }, { longitude: 103.972024, latitude: 30.570413 }, { longitude: 103.971949, latitude: 30.570391 }, { longitude: 103.971788, latitude: 30.570364 },
      { longitude: 103.971353, latitude: 30.570327 }, { longitude: 103.970994, latitude: 30.570322 }, { longitude: 103.970725, latitude: 30.570327 }, { longitude: 103.970436, latitude: 30.570343 }, { longitude: 103.970151, latitude: 30.570375 }, { longitude: 103.969513, latitude: 30.570483 },
      { longitude: 103.968515, latitude: 30.57067 }, { longitude: 103.967807, latitude: 30.570826 }, { longitude: 103.967807, latitude: 30.570826 }, { longitude: 103.966986, latitude: 30.571024 }, { longitude: 103.966707, latitude: 30.571105 }, { longitude: 103.966053, latitude: 30.57133 },
      { longitude: 103.965243, latitude: 30.571609 }, { longitude: 103.964986, latitude: 30.571706 }, { longitude: 103.964691, latitude: 30.571824 }, { longitude: 103.964309, latitude: 30.571952 }, { longitude: 103.963966, latitude: 30.572076 }, { longitude: 103.963891, latitude: 30.572102 },
      { longitude: 103.963687, latitude: 30.572162 }, { longitude: 103.963548, latitude: 30.572215 }, { longitude: 103.963146, latitude: 30.572355 }, { longitude: 103.962518, latitude: 30.572575 }, { longitude: 103.961981, latitude: 30.572784 }, { longitude: 103.961418, latitude: 30.572998 },
      { longitude: 103.961418, latitude: 30.572998 }, { longitude: 103.961225, latitude: 30.5731 }, { longitude: 103.96108, latitude: 30.57317 }, { longitude: 103.960157, latitude: 30.573481 }, { longitude: 103.959401, latitude: 30.573728 }, { longitude: 103.959031, latitude: 30.573808 },
      { longitude: 103.958956, latitude: 30.573824 }, { longitude: 103.958382, latitude: 30.573894 }, { longitude: 103.95828, latitude: 30.573766 }, { longitude: 103.95828, latitude: 30.573766 }, { longitude: 103.958135, latitude: 30.573261 }, { longitude: 103.95791, latitude: 30.572816 },
      { longitude: 103.957422, latitude: 30.571749 }, { longitude: 103.957229, latitude: 30.571319 }, { longitude: 103.956955, latitude: 30.570729 }, { longitude: 103.95681, latitude: 30.570418 }, { longitude: 103.956751, latitude: 30.570284 }, { longitude: 103.956569, latitude: 30.569914 },
      { longitude: 103.956435, latitude: 30.569619 }, { longitude: 103.956309, latitude: 30.569347 }
    ];
    const pickupSite = {
      name: "上车点",
      latitude: drivingTrack[0].latitude,
      longitude: drivingTrack[0].longitude
    };
    const dropoffSite = {
      name: "目的地",
      latitude: drivingTrack[drivingTrack.length - 1].latitude,
      longitude: drivingTrack[drivingTrack.length - 1].longitude
    };
    // 使用路线起点附近的一小段倒放,模拟车辆靠近上车点
    const pickupTrack = drivingTrack.slice(0, 10).reverse().map(item => ({ ...item }));
    const centerPoint = drivingTrack[Math.floor(drivingTrack.length / 2)];
    return {
      pickupSite,
      dropoffSite,
      pickupTrack,
      drivingTrack,
      centerPoint
    };
  }, []);
  const getDirectionByPoints = (from: { latitude: number; longitude: number }, to?: { latitude: number; longitude: number }) => {
  const getDirectionByPoints = (
    from: { latitude: number; longitude: number },
    to?: { latitude: number; longitude: number }
  ) => {
    if (!from || !to) return 0;
    const deltaLng = to.longitude - from.longitude;
    const deltaLat = to.latitude - from.latitude;
@@ -168,9 +209,334 @@
    return ((angle % 360) + 360) % 360;
  };
  const normalizeDirection = (value: any) => {
    const direction = Number(value);
    if (!Number.isFinite(direction)) {
      return null;
    }
    return ((direction % 360) + 360) % 360;
  };
  const toFiniteNumber = (value: any) => {
    const num = Number(value);
    return Number.isFinite(num) ? num : null;
  };
  const normalizePoint = (point: any) => {
    if (!point) {
      return null;
    }
    if (typeof point === "string") {
      const [longitudeValue, latitudeValue] = point.split(",");
      const longitudeNum = toFiniteNumber(longitudeValue);
      const latitudeNum = toFiniteNumber(latitudeValue);
      if (longitudeNum === null || latitudeNum === null) {
        return null;
      }
      return { longitude: longitudeNum, latitude: latitudeNum };
    }
    const longitudeNum = toFiniteNumber(
      point.longitude ?? point.lon ?? point.lng
    );
    const latitudeNum = toFiniteNumber(point.latitude ?? point.lat);
    if (longitudeNum === null || latitudeNum === null) {
      return null;
    }
    return { longitude: longitudeNum, latitude: latitudeNum };
  };
  const getFieldValue = (source: any, keys: string[]) => {
    for (const key of keys) {
      const value = source?.[key];
      if (value !== undefined && value !== null && value !== "") {
        return value;
      }
    }
    return null;
  };
  const buildSite = (source: any, type: "start" | "end") => {
    const isStart = type === "start";
    const latitudeValue = getFieldValue(
      source,
      isStart
        ? ["startLat", "startLatitude", "upLat", "originLat", "originLatitude"]
        : [
            "endLat",
            "endLatitude",
            "downLat",
            "destinationLat",
            "destinationLatitude"
          ]
    );
    const longitudeValue = getFieldValue(
      source,
      isStart
        ? [
            "startLng",
            "startLon",
            "startLongitude",
            "upLng",
            "upLon",
            "originLng",
            "originLon",
            "originLongitude"
          ]
        : [
            "endLng",
            "endLon",
            "endLongitude",
            "downLng",
            "downLon",
            "destinationLng",
            "destinationLon",
            "destinationLongitude"
          ]
    );
    const latitudeNum = toFiniteNumber(latitudeValue);
    const longitudeNum = toFiniteNumber(longitudeValue);
    if (latitudeNum === null || longitudeNum === null) {
      return null;
    }
    return {
      latitude: latitudeNum,
      longitude: longitudeNum,
      name:
        getFieldValue(
          source,
          isStart
            ? ["startName", "startSiteName", "upName", "originName"]
            : ["endName", "endSiteName", "downName", "destinationName"]
        ) || (isStart ? "上车点" : "终点"),
      address:
        getFieldValue(
          source,
          isStart
            ? ["startAddress", "startSiteAddress", "upAddress", "originAddress"]
            : [
                "endAddress",
                "endSiteAddress",
                "downAddress",
                "destinationAddress"
              ]
        ) || ""
    };
  };
  const formatTimeText = (value: any) => {
    if (!value) {
      return null;
    }
    const text = String(value);
    if (/^\d{2}:\d{2}/.test(text)) {
      return text.slice(0, 5);
    }
    const date = new Date(text);
    if (Number.isNaN(date.getTime())) {
      return text;
    }
    const hours = `${date.getHours()}`.padStart(2, "0");
    const minutes = `${date.getMinutes()}`.padStart(2, "0");
    return `${hours}:${minutes}`;
  };
  const parseTimeToTimestamp = (value: any) => {
    if (value === undefined || value === null || value === "") {
      return null;
    }
    if (typeof value === "number") {
      if (!Number.isFinite(value) || value <= 0) {
        return null;
      }
      // 兼容秒级时间戳,统一转换成毫秒。
      return value < 1e12 ? value * 1000 : value;
    }
    const normalizedValue = String(value).trim();
    if (!normalizedValue) {
      return null;
    }
    if (/^\d+$/.test(normalizedValue)) {
      const numericValue = Number(normalizedValue);
      if (!Number.isFinite(numericValue) || numericValue <= 0) {
        return null;
      }
      return numericValue < 1e12 ? numericValue * 1000 : numericValue;
    }
    // 兼容 "2026-04-17 10:00:00" 这类在部分环境下解析不稳定的时间格式。
    const parsedTime = new Date(normalizedValue.replace(/-/g, "/")).getTime();
    return Number.isFinite(parsedTime) && parsedTime > 0 ? parsedTime : null;
  };
  const resolveExpiredState = (info: any, mappedStatus: number) => {
    if (info?.expired === true || info?.isExpired === true) {
      return true;
    }
    if (mappedStatus < 7) {
      return false;
    }
    const getoffTimestamp = parseTimeToTimestamp(info?.getoffTime);
    if (typeof getoffTimestamp !== "number") {
      return false;
    }
    // 仅以下车时间作为分享页过期依据。
    return Date.now() - getoffTimestamp > 24 * 60 * 60 * 1000;
  };
  const getCenterPoint = (
    points: Array<{ longitude: number; latitude: number }>
  ) => {
    if (!points.length) {
      return null;
    }
    const init = {
      minLng: points[0].longitude,
      maxLng: points[0].longitude,
      minLat: points[0].latitude,
      maxLat: points[0].latitude
    };
    const bounds = points.reduce(
      (acc, point) => ({
        minLng: Math.min(acc.minLng, point.longitude),
        maxLng: Math.max(acc.maxLng, point.longitude),
        minLat: Math.min(acc.minLat, point.latitude),
        maxLat: Math.max(acc.maxLat, point.latitude)
      }),
      init
    );
    return {
      longitude: (bounds.minLng + bounds.maxLng) / 2,
      latitude: (bounds.minLat + bounds.maxLat) / 2
    };
  };
  const extractRoutePoints = (routeResponse: any) => {
    const candidates = [
      routeResponse?.points,
      routeResponse?.route?.polyline,
      routeResponse?.polyline,
      routeResponse?.content?.points
    ];
    for (const candidate of candidates) {
      if (Array.isArray(candidate) && candidate.length > 0) {
        return candidate.map(normalizePoint).filter(Boolean) as Array<{
          longitude: number;
          latitude: number;
        }>;
      }
    }
    return [];
  };
  const extractServedPoints = (routeResponse: any) => {
    if (!Array.isArray(routeResponse?.servedPoints)) {
      return [];
    }
    return routeResponse.servedPoints
      .map(normalizePoint)
      .filter(Boolean) as Array<{ longitude: number; latitude: number }>;
  };
  const resolveVehiclePoint = (
    points: Array<{ longitude: number; latitude: number }>,
    servedPoints: Array<{ longitude: number; latitude: number }> = []
  ) => {
    const servedLastPoint =
      Array.isArray(servedPoints) && servedPoints.length > 0
        ? servedPoints[servedPoints.length - 1]
        : null;
    if (servedLastPoint) {
      return servedLastPoint;
    }
    return points[0] || null;
  };
  const syncShareRoute = (
    routeResponse: any,
    mappedStatus: number,
    nextStartSite: any,
    nextEndSite: any
  ) => {
    const routePoints = extractRoutePoints(routeResponse);
    const servedPoints = extractServedPoints(routeResponse);
    const routeMeta =
      routeResponse?.content?.route ||
      routeResponse?.route ||
      routeResponse ||
      {};
    const distance = normalizeDistanceKm(Number(routeMeta?.distance));
    const duration = normalizeDurationSeconds(Number(routeMeta?.duration));
    setRouteDistance(distance);
    setRouteDuration(duration);
    applyRoutePointsRef.current(routePoints);
    const direction = normalizeDirection(
      routeResponse?.direction ??
        routeResponse?.content?.direction ??
        routeMeta?.direction ??
        routeResponse?.vehicleDirection
    );
    const vehiclePoint = resolveVehiclePoint(routePoints, servedPoints);
    if (vehiclePoint && [4, 5, 6].includes(mappedStatus)) {
      updateVehicleLocationRef.current(
        vehiclePoint.latitude,
        vehiclePoint.longitude,
        true
      );
      if (typeof direction === "number") {
        setVehicleDirectionRef.current(direction);
      } else {
        setVehicleDirectionRef.current(
          getDirectionByPoints(vehiclePoint, routePoints[1])
        );
      }
    }
    const center = getCenterPoint([
      ...routePoints,
      ...(nextStartSite ? [nextStartSite] : []),
      ...(nextEndSite ? [nextEndSite] : [])
    ]);
    if (center) {
      setLongitude(center.longitude);
      setLatitude(center.latitude);
    }
  };
  useEffect(() => {
    tripStatusRef.current = tripStatus;
  }, [tripStatus]);
  useEffect(() => {
    startSiteRef.current = startSite;
  }, [startSite]);
  useEffect(() => {
    endSiteRef.current = endSite;
  }, [endSite]);
  const viewportState = useMapViewport({
    tripStatus,
    statusRule,
    routePoints,
    walkingPoints,
    startSite,
    endSite,
    vehicleLocation,
    longitude,
    latitude
  });
  const { mapScale, updateMapViewportPadding } = viewportState;
  useEffect(() => {
    applyRoutePointsRef.current = applyRoutePoints;
@@ -179,107 +545,171 @@
  }, [applyRoutePoints, updateVehicleLocationThrottled, setVehicleDirection]);
  useEffect(() => {
    setShowReminder(false);
    setStartSite(mockScene.pickupSite);
    setEndSite(mockScene.dropoffSite);
    setLongitude(mockScene.centerPoint.longitude);
    setLatitude(mockScene.centerPoint.latitude);
    updateMapViewportPadding();
  }, [tripStatus, updateMapViewportPadding]);
    let stage = "pickup";
    let pointIndex = 0;
  useEffect(() => {
    const timer = setTimeout(() => {
      updateMapViewportPadding();
    }, 50);
    return () => clearTimeout(timer);
  }, [
    tripStatus,
    showReminder,
    tripData?.startPointName,
    tripData?.endPointName,
    tripData?.plateNumber,
    updateMapViewportPadding
  ]);
    const syncMockTripData = (nextStatus: number, etaMinutes: number) => {
      const remainDistance =
        nextStatus === 4
          ? { value: Math.max(0.5, Number((etaMinutes * 0.8).toFixed(1))), unit: "km" as const }
          : nextStatus === 6
            ? { value: Math.max(0.5, Number(((mockScene.drivingTrack.length - pointIndex) * 0.6).toFixed(1))), unit: "km" as const }
            : { value: 0, unit: "km" as const };
      const remainDuration = nextStatus === 7 ? 0 : Math.max(60, etaMinutes * 60);
  useEffect(() => {
    if (!orderId) {
      Taro.showToast({ title: "分享订单不存在", icon: "none" });
      return;
    }
      setTripStatus(nextStatus);
      setRouteDistance(remainDistance);
      setRouteDuration(remainDuration);
      setTripData({
        etaMinutes,
        distanceText: `${remainDistance.value}${remainDistance.unit}`,
        plateNumber: "粤PXV7029",
        vehicleColor: "白色",
        vehicleCode: "03",
        vehicleImage: "https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/car-g9.png",
        startName: mockScene.pickupSite.name,
        endName: mockScene.dropoffSite.name,
        tripStatus: nextStatus,
        boardTime: nextStatus >= 6 ? "14:56" : null,
        arriveTime: nextStatus === 7 ? "15:10" : null
      });
    let cancelled = false;
    const showFetchError = (error: unknown) => {
      console.error("获取分享行程失败", error);
      if (!errorToastShownRef.current) {
        errorToastShownRef.current = true;
        Taro.showToast({ title: "获取分享行程失败", icon: "none" });
      }
    };
    // 初始化一段路线,先验证 hooks 产出的 polyline 是否可用
    applyRoutePointsRef.current(mockScene.pickupTrack);
    updateVehicleLocationRef.current(
      mockScene.pickupTrack[0].latitude,
      mockScene.pickupTrack[0].longitude,
      true
    );
    syncMockTripData(4, 4);
    const applyShareInfo = (info: any) => {
      const mappedStatus = mapOrderStatusToTripStatus(info?.status);
      const nextStartSite = buildSite(info, "start");
      const nextEndSite = buildSite(info, "end");
      const etaMinutes = Math.max(
        0,
        Number(
          info?.etaMinutes ??
            info?.estimatedWaitTime ??
            info?.arrivalMinutes ??
            info?.routeDurationMinutes ??
            0
        ) || 0
      );
      const nextTripData = {
        ...info,
        tripStatus: mappedStatus,
        etaMinutes,
        plateNumber: info?.plateNumber || info?.vehiclePlateNumber || "",
        vehicleColor: info?.vehicleColor || "",
        vehicleCode: info?.vehicleCode || "",
        vehiclePictures: info?.vehiclePictures || info?.vehicleImage || "",
        startPointName: info?.startPointName || nextStartSite?.name || "上车点",
        endPointName: info?.endPointName || nextEndSite?.name || "终点",
        boardingTime: formatTimeText(info?.boardingTime || info?.pickupTime),
        getoffTime: formatTimeText(
          info?.getoffTime || info?.finishTime || info?.orderFinishTime
        ),
        customerServicePhone:
          info?.customerServicePhone || info?.servicePhone || "400-000-0000"
      };
    const intervalId = setInterval(() => {
      if (stage === "pickup") {
        const currentPoint = mockScene.pickupTrack[pointIndex];
        const nextPoint = mockScene.pickupTrack[pointIndex + 1];
        const remainPoints = mockScene.pickupTrack.slice(pointIndex);
      infoStatusRef.current = mappedStatus;
      tripStatusRef.current = mappedStatus;
      startSiteRef.current = nextStartSite;
      endSiteRef.current = nextEndSite || {};
        if (currentPoint) {
          applyRoutePointsRef.current(remainPoints.length ? remainPoints : [currentPoint]);
          updateVehicleLocationRef.current(currentPoint.latitude, currentPoint.longitude, true);
          setVehicleDirectionRef.current(getDirectionByPoints(currentPoint, nextPoint));
          syncMockTripData(4, Math.max(1, mockScene.pickupTrack.length - pointIndex - 1));
        }
      setTripStatus(mappedStatus);
      setTripData(nextTripData);
      setStartSite(nextStartSite);
      setEndSite(nextEndSite || {});
      setIsExpired(resolveExpiredState(info, mappedStatus));
      setShowReminder(Boolean(info?.showReminder));
        if (pointIndex >= mockScene.pickupTrack.length - 1) {
          stage = "driving";
          pointIndex = 0;
          applyRoutePointsRef.current(mockScene.drivingTrack);
          syncMockTripData(6, 0);
      return {
        mappedStatus,
        nextStartSite,
        nextEndSite
      };
    };
    const resolveRouteOrderStatus = (routeResponse: any) => {
      const statusValue =
        getFieldValue(routeResponse?.content, [
          "tripStatus",
          "orderStatus",
          "status"
        ]) ?? getFieldValue(routeResponse, ["tripStatus", "orderStatus"]);
      const parsedStatus = Number(statusValue);
      return Number.isFinite(parsedStatus)
        ? mapOrderStatusToTripStatus(parsedStatus)
        : null;
    };
    const fetchInitialShareInfo = async () => {
      const info = await shareQueryOrderInfo({ orderId });
      if (cancelled) {
        return null;
      }
      return applyShareInfo(info || {});
    };
    const fetchShareTripData = async () => {
      try {
        const routeResponse = await shareQueryOrderRoute(orderId);
        if (cancelled) {
          return;
        }
        pointIndex += 1;
        return;
      }
        const routeStatus = resolveRouteOrderStatus(routeResponse);
        const shouldFetchInfo =
          routeStatus !== null && routeStatus !== infoStatusRef.current;
      if (stage === "driving") {
        const currentPoint = mockScene.drivingTrack[pointIndex];
        const nextPoint = mockScene.drivingTrack[pointIndex + 1];
        const remainPoints = mockScene.drivingTrack.slice(pointIndex);
        let mappedStatus = tripStatusRef.current;
        let nextStartSite = startSiteRef.current;
        let nextEndSite = endSiteRef.current;
        if (currentPoint) {
          applyRoutePointsRef.current(remainPoints.length ? remainPoints : [currentPoint]);
          updateVehicleLocationRef.current(currentPoint.latitude, currentPoint.longitude, true);
          setVehicleDirectionRef.current(getDirectionByPoints(currentPoint, nextPoint));
          syncMockTripData(6, 0);
        if (shouldFetchInfo) {
          const info = await shareQueryOrderInfo({ orderId });
          if (cancelled) {
            return;
          }
          const infoState = applyShareInfo(info || {});
          mappedStatus = infoState.mappedStatus;
          nextStartSite = infoState.nextStartSite;
          nextEndSite = infoState.nextEndSite;
        }
        if (pointIndex >= mockScene.drivingTrack.length - 1) {
          stage = "arrived";
          syncMockTripData(7, 0);
        syncShareRoute(routeResponse, mappedStatus, nextStartSite, nextEndSite);
        errorToastShownRef.current = false;
      } catch (error) {
        showFetchError(error);
      }
    };
    fetchInitialShareInfo()
      .then(infoState => {
        if (!infoState || cancelled) {
          return;
        }
        fetchShareTripData();
      })
      .catch(error => {
        showFetchError(error);
      });
    const timer = setInterval(fetchShareTripData, 10000);
        pointIndex += 1;
      }
    }, 2000);
    return () => clearInterval(intervalId);
  }, [mockScene, setRouteDistance, setRouteDuration]);
    return () => {
      cancelled = true;
      clearInterval(timer);
    };
  }, [orderId, setRouteDistance, setRouteDuration]);
  const goHome = () => {
    Taro.reLaunch({ url: "/pages/robotaxi/home/index" });
  };
  const durationDisplay = formatDurationDisplay(routeDuration);
  const { renderMapOverlays } = useViewDelegates({
    isAndroid,
    tripStatus,
    mapScale,
    markers,
    shouldShowWaitingRadar: false,
    waitingRadarVisible: false,
@@ -291,107 +721,17 @@
    arriveCalloutStyle: {},
    routeDistance,
    routeDuration,
    customCalloutIconSrc: "https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/maker-tooltip-right.png",
    startSite,
    endSite,
    callImmediately: () => {},
    fareEstimate: {},
    handleOpenPassengerNotice: () => {},
    walkingTime: 0,
    isCallDisabled: false,
    handleOpenCancelPopupByTripStatus: () => {},
    handleOpenCancelPopupWithType: () => {},
    setTripStatus,
    finishCountdownEnabled: false,
    orderId: "",
    vehicleInfo: {},
    handleFinishCountdownTimeout: () => {},
    resetDetailVersion: 0,
    priceUpdateVersion: 0,
    evaluateSubmitVersion: 0,
    evaluateCloseVersion: 0,
    handleOpenAnnouncementsPopup: () => {},
    handleOpenOverRoutePopup: () => {},
    handlePay: () => {},
    handleOpenEvaluatePopup: () => {},
    handleToggleDetailFade: () => {},
    isRouteSelectExpanded: false,
    onToggleRouteSelect: () => {},
    trunkStatus: false
    customCalloutIconSrc:
      "https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/maker-tooltip-right.png"
  });
  // const renderMarkers = () => {
  //   if (!tripData) return [];
  //   const markersList: any[] = [];
  //   // 上车点 (等待接驾、行程中显示)
  //   if (tripData.tripStatus === 1 || tripData.tripStatus === 2) {
  //     markersList.push({
  //       id: 1,
  //       latitude: tripData.startLat,
  //       longitude: tripData.startLng,
  //       iconPath: "https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/maker-up.png",
  //       width: 22,
  //       height: 40,
  //       anchor: { x: 0.5, y: 1 },
  //       zIndex: 1,
  //       name: tripData.startName
  //     });
  //   }
  //   // 下车点 (始终显示)
  //   markersList.push({
  //     id: 2,
  //     latitude: tripData.endLat,
  //     longitude: tripData.endLng,
  //     iconPath: "https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/maker-down.png",
  //     width: 22,
  //     height: 40,
  //     anchor: { x: 0.5, y: 1 },
  //     zIndex: 1,
  //     name: tripData.endName
  //   });
  //   // 车辆 (等待接驾、行程中显示)
  //   if (
  //     vehicleLocation &&
  //     (tripData.tripStatus === 1 || tripData.tripStatus === 2)
  //   ) {
  //     markersList.push({
  //       id: 3,
  //       latitude: vehicleLocation.latitude,
  //       longitude: vehicleLocation.longitude,
  //       iconPath: carIcon,
  //       width: 46,
  //       height: 71,
  //       anchor: { x: 0.5, y: 0.5 },
  //       rotate: vehicleDirection,
  //       zIndex: 2,
  //       callout:
  //         tripData.tripStatus === 1
  //           ? {
  //               content: `${tripData.distanceText} ${tripData.etaMinutes}分钟`,
  //               color: "#6D7FD9",
  //               fontSize: 12,
  //               borderRadius: 12,
  //               bgColor: "#ffffff",
  //               padding: 6,
  //               display: "ALWAYS"
  //             }
  //           : undefined
  //     });
  //   }
  //   return markersList;
  // };
  // const markers = renderMarkers();
  const handleContactService = () => {
    // 唤起客服操作
    Taro.makePhoneCall({
      phoneNumber: "400-000-0000" // TODO: 替换为实际客服电话
    }).catch(() => {});
    const phoneNumber = Taro.getStorageSync("customerServicePhone");
    if (phoneNumber) {
      Taro.makePhoneCall({ phoneNumber });
    } else {
      Taro.showToast({ title: "暂无客服电话", icon: "none" });
    }
  };
  if (isExpired) {
@@ -411,20 +751,11 @@
  return (
    <View className="home-container">
      {/* <Map
        className="map-container"
        longitude={longitude}
        latitude={latitude}
        scale={14}
        markers={markers}
        polyline={tripData?.tripStatus === 1 || tripData?.tripStatus === 2 ? polyline : []}
        style={{ width: "100%", height: "100%" }}
      > */}
      <Map
        id="map"
        longitude={mapLongitude}
        latitude={mapLatitude}
        scale={14}
        scale={mapScale}
        markers={markers}
        polyline={polyline}
        style={{ width: "100%", height: "87vh" }}
@@ -436,9 +767,15 @@
        </View>
        <View className="container">
          {/* 联系客服悬浮按钮 */}
          <View className={`share-contact ${showReminder ? "has-tip" : ""}`}>
            <Image className="share-contact-icon" src={contactServiceIcon} />
          </View>
          <CoverView
            className={`share-contact ${showReminder ? "has-tip" : ""}`}
            onClick={handleContactService}
          >
            <CoverImage
              className="share-contact-icon"
              src={contactServiceIcon}
            />
          </CoverView>
          {/* 提醒 */}
          {showReminder && (
            <View className="tip-box">
@@ -466,33 +803,51 @@
                  {tripData?.tripStatus === 4 && (
                    <View className="calling-car-head-title">
                      车辆还有
                      <View className="calling-car-head-title-eta">{`${tripData.etaMinutes}`}</View>
                      分钟到达上车点
                      <View className="calling-car-head-title-eta">
                        {durationDisplay.value}
                      </View>
                      {durationDisplay.unit}到达上车点
                    </View>
                  )}
                  {tripData?.tripStatus === 5 && (
                    <View className="calling-car-head-title">
                      车辆已到达上车点
                    </View>
                  )}
                  {tripData?.tripStatus === 6 && (
                    <View className="calling-car-head-title">
                      乘客已上车,正在前往目的地
                      预计
                      <View className="calling-car-head-title-eta">
                        {durationDisplay.value}
                      </View>
                      {durationDisplay.unit}抵达目的地
                    </View>
                  )}
                  {tripData?.tripStatus === 7 && (
                  {tripData?.tripStatus > 7 && (
                    <View className="calling-car-head-title">已到达目的地</View>
                  )}
                </View>
                <View className="car-card">
                  <View className="car-card-detail">
                    <View>
                      <View className="car-card-detail-num">粤PXV7029</View>
                      <View className="car-card-detail-num">
                        {tripData?.plateNumber || "--"}
                      </View>
                      <View className="car-card-detail-color">
                        白色
                        {tripData?.vehicleColor || "--"}
                        <View className="car-card-detail-color-line"></View>
                        车身编码: 03
                        车身编码: {tripData?.vehicleCode || "--"}
                      </View>
                    </View>
                    <Image
                      className="car-card-detail-img"
                      src="https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/vehicle-default.png"
                    />
                    {tripData?.vehiclePictures && (
                      <Image
                        className="car-card-detail-img"
                        src={
                          safeParse(tripData?.vehiclePictures, {})?.url ||
                          "https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/vehicle-default.png"
                        }
                      />
                    )}
                  </View>
                  <View className="car-card-line"></View>
                  <View className="car-card-temp">
@@ -510,14 +865,14 @@
                                : ""
                            }`}
                          >
                            起点:{tripData?.startName}
                            起点:{tripData?.startPointName}
                          </View>
                        </View>
                        {(tripData?.tripStatus === 6 ||
                          tripData?.tripStatus === 7) &&
                          tripData?.boardTime && (
                          tripData?.boardingTime && (
                            <View className="car-card-temp-time disabled-text">
                              {tripData.boardTime} 上车
                              {tripData.boardingTime} 上车
                            </View>
                          )}
                      </View>
@@ -534,12 +889,12 @@
                              tripData?.tripStatus === 7 ? "disabled-text" : ""
                            }`}
                          >
                            终点:{tripData?.endName}
                            终点:{tripData?.endPointName}
                          </View>
                        </View>
                        {tripData?.tripStatus === 7 && tripData?.arriveTime && (
                        {tripData?.tripStatus === 7 && tripData?.getoffTime && (
                          <View className="car-card-temp-time disabled-text">
                            {tripData.arriveTime} 到达
                            {tripData.getoffTime} 到达
                          </View>
                        )}
                      </View>