feat(share-trip): 新增分享行程页面地图相关钩子函数
| | |
| | | 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); |
| | | } |
| | |
| | | |
| | | <TemperatureControl tripStatus={tripStatus} orderId={orderId} /> |
| | | |
| | | <TripActionBar |
| | | tripStatus={tripStatus} |
| | | handleCancelTrip={handleCancelTrip} |
| | | handleModifyDestination={handleModifyDestination} |
| | | handleShareTrip={handleShareTrip} |
| | | /> |
| | | |
| | | <DoorTrunkControl |
| | | tripStatus={tripStatus} |
| | | orderId={orderId} |
| | |
| | | onToggleDetailFade={onToggleDetailFade} |
| | | handlePay={handlePay} |
| | | /> |
| | | |
| | | <TripActionBar |
| | | tripStatus={tripStatus} |
| | | handleCancelTrip={handleCancelTrip} |
| | | handleModifyDestination={handleModifyDestination} |
| | | handleShareTrip={handleShareTrip} |
| | | /> |
| | | </View> |
| | | } |
| | | |
| New file |
| | |
| | | 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 |
| | | }; |
| | | } |
| New file |
| | |
| | | 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 |
| | | }; |
| | | } |
| New file |
| | |
| | | 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 |
| | | }; |
| | | } |
| New file |
| | |
| | | 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 |
| | | }; |
| | | } |
| New file |
| | |
| | | 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 |
| | | }; |
| | | } |
| | |
| | | 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"; // 占位,建议换成首页小房子图标 |
| | |
| | | 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; |
| | | } |
| | |
| | | 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, |
| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | }, [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, |
| | |
| | | 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) { |
| | |
| | | |
| | | 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" }} |
| | |
| | | </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"> |
| | |
| | | {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"> |
| | |
| | | : "" |
| | | }`} |
| | | > |
| | | 起点:{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> |
| | |
| | | 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> |