import Taro, { useDidShow } from "@tarojs/taro";
|
import { showToast } from '../../../components/toast';
|
import { useEffect, useMemo } from 'react';
|
import {
|
buildStationMetaMap,
|
findNearestMarker,
|
formatGeofenceByType,
|
getAutoScaleBySites,
|
getDistance,
|
fetchMapData,
|
filterRenderableGeofence,
|
extractPointsFromPolygons,
|
preloadImageToTemp,
|
reportEvent,
|
safeMakePhoneCall,
|
} from '../../../../utils/Robotaxi-public-method';
|
import { getGeofence } from '../../../api/home';
|
import { getOrderDistance } from '../../../api/select-map-site';
|
import {
|
ensureOrderDistanceConfig,
|
isWithinPickupMaxDistance,
|
isWithinStartEndMinDistance,
|
normalizeOrderDistanceConfig
|
} from '../../../utils/order-distance';
|
import { collectPoint } from '../../../api/site-search';
|
import { checkOtp } from '../../../../utils/tools';
|
import { jumpTo } from '../../../../utils/jump';
|
import useSelectMapSiteState from './use-select-map-site-state';
|
import { SELECT_SITE_RANGE_STATUS } from './use-select-map-site-state';
|
import useSelectMapSiteWalkingRoute from './use-select-map-site-walking-route';
|
import {
|
MODIFY_DESTINATION_SESSION_KEY,
|
MODIFY_DESTINATION_CANDIDATE_KEY,
|
MODIFY_DESTINATION_SKIP_AC_QUERY_ON_RETURN_KEY,
|
MODIFY_DESTINATION_PENDING_SAME_SITE_TOAST_KEY,
|
isSameModifyDestinationSite
|
} from '../../../constants/modify-destination';
|
import { hasActiveTripOrderInStorage } from '../../../utils/order';
|
import {
|
CONFIRM_MAP_DROPOFF_BUTTON_EVENT_ID,
|
CONFIRM_MAP_PICKUP_BUTTON_EVENT_ID
|
} from '../../../constants/report-events';
|
import { createReportStationPayload } from '../../../constants/report-structures';
|
import {
|
HOME_START_SITE_SOURCE_MANUAL_SELECTED,
|
writeHomeStartSiteDisplayState
|
} from '../../../hooks/use-home-start-site-display';
|
import {
|
MAP_CONFIRM_RIDE_SOURCE_TYPE,
|
completeConfirmRideSelectionSession,
|
writeConfirmRideSiteSourceType
|
} from '../../../constants/confirm-ride-report';
|
import { GeofenceType } from '../../../constants/geofence';
|
import {
|
ORDER_DISTANCE_TIP_TYPE_MAX_DISTANCE,
|
ORDER_DISTANCE_TIP_TYPE_MIN_DISTANCE
|
} from '../../../constants/order-distance';
|
|
const AUTO_SCALE_MAX = 17; // 自动缩放最大等级
|
const AUTO_SCALE_MIN = 11; // 自动缩放最小等级
|
const INCLUDE_POINTS_PADDING = [100, 100, 100, 100] as [number, number, number, number]; // 地图回视图内边距
|
const CROSS_OPERATING_AREA_FILL_COLOR = '#0000001a'; // 非可选运营区围栏填充色
|
const CROSS_OPERATING_AREA_STROKE_COLOR = '#DFB369'; // 非可选运营区围栏描边色
|
const CROSS_OPERATING_AREA_STROKE_WIDTH = 1.5; // 非可选运营区围栏描边宽度
|
const CROSS_OPERATING_AREA_DASH_ARRAY = [10, 10] as [number, number]; // 非可选运营区 polygon 虚线边框
|
const SELECT_MAP_POINT_ON_OFF = {
|
start: 1,
|
end: 2
|
} as const; // 选点地图站点开关:1=上车点,2=下车点
|
|
// 站点 marker 原始(紫色)图标,来自 formatSiteToMarker
|
const OLD_SITE_MARKER_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/site-poi.png';
|
// 选点页站点金色图标(与首页保持一致)
|
const SITE_MARKER_ICON = 'https://s.xiaopeng.com/xp-fe/robot-taxi/imgs/v2/golden-site-20260708.png';
|
|
/**
|
* 将站点 marker 的紫色图标替换为金色图标(与首页一致)。
|
*/
|
const applySiteMarkerIcon = (markerList: any[]) => {
|
if (!Array.isArray(markerList)) {
|
return markerList;
|
}
|
return markerList.map((marker) => (
|
marker?.iconPath === OLD_SITE_MARKER_ICON
|
? { ...marker, iconPath: SITE_MARKER_ICON, width: 18, height: 18 }
|
: marker
|
));
|
};
|
|
/**
|
* 选点页业务编排:聚合状态容器与副作用 hooks,并对外提供稳定页面接口。
|
*/
|
export default function useSelectMapSite(props?: { onScaleChange?: (scale: number) => void }) {
|
const state = useSelectMapSiteState(); // 选点页状态容器
|
|
/**
|
* 同步当前指针确认位置。
|
*/
|
const syncPinLocation = (lat: number, lng: number) => {
|
state.pinLocationRef.current = {
|
lat: Number(lat || 0),
|
lng: Number(lng || 0)
|
};
|
};
|
|
/**
|
* 构造确认上车点埋点中的站点字段。
|
*/
|
const createConfirmedPickupStationPayload = () => {
|
const selectedCity = Taro.getStorageSync('selectedCity');
|
return createReportStationPayload(state.selectedSite, String(selectedCity?.name || ''));
|
};
|
|
/**
|
* 上报地图确认站点埋点。
|
*/
|
const reportConfirmMapSiteEvent = () => {
|
const eventId = state.pageType === 'start'
|
? CONFIRM_MAP_PICKUP_BUTTON_EVENT_ID
|
: state.pageType === 'end'
|
? CONFIRM_MAP_DROPOFF_BUTTON_EVENT_ID
|
: '';
|
if (!eventId) {
|
return;
|
}
|
reportEvent(eventId, {
|
has_nearby_stations_shown: state.nearbySites.length > 0 ? true : false,
|
nearby_station_count: Number(state.nearbySites.length || 0),
|
confirmed_pickup: {
|
station: createConfirmedPickupStationPayload(),
|
type: state.pickupConfirmTypeRef.current,
|
lat: Number(state.selectedSite?.latitude || state.selectedSite?.lat || 0),
|
lng: Number(state.selectedSite?.longitude || state.selectedSite?.lng || 0)
|
},
|
map_zoom_level: Number(state.mapScale || 0),
|
pin_loc: {
|
lat: Number(state.pinLocationRef.current?.lat || 0),
|
lng: Number(state.pinLocationRef.current?.lng || 0)
|
}
|
});
|
};
|
|
/**
|
* 重置场站选择模式,回到普通站点模式。
|
*/
|
const resetStationSelection = () => {
|
state.setSelectionMode('point');
|
state.setSelectedStation(null);
|
state.setStationSiteList([]);
|
};
|
|
/**
|
* 根据当前选中站点判断是否进入场站模式,并设置默认站点与场站信息。
|
*/
|
const applySelectionBySite = (site: any, candidateSites?: any[]) => {
|
const stationId = site?.stationId ? String(site.stationId) : '';
|
if (!stationId) {
|
resetStationSelection();
|
state.setSelectedSite(site || {});
|
syncPinLocation(site?.latitude, site?.longitude);
|
return site || {};
|
}
|
const stationMeta = state.stationMetaMapRef.current[stationId];
|
const sourceSites = Array.isArray(candidateSites) ? candidateSites : state.nearbySites;
|
const stationSites = sourceSites.filter((item: any) => String(item?.stationId || '') === stationId);
|
if (!stationMeta || stationSites.length === 0) {
|
resetStationSelection();
|
state.setSelectedSite(site || {});
|
return site || {};
|
}
|
state.setSelectionMode('station');
|
state.setSelectedStation(stationMeta);
|
state.setStationSiteList(stationSites);
|
const defaultStationSite = stationSites[0]; // 进入场站模式默认选中首个站点
|
state.setSelectedSite(defaultStationSite);
|
syncPinLocation(defaultStationSite?.latitude, defaultStationSite?.longitude);
|
return defaultStationSite;
|
};
|
|
/**
|
* 获取当前页面用于约束同运营区选择的对端站点电子围栏 ID。
|
*/
|
const getReferenceElectronicAreaId = () => {
|
const cachedSite = state.pageType === 'start'
|
? Taro.getStorageSync('endSite')
|
: Taro.getStorageSync('startSite');
|
return String(cachedSite?.electronicAreaId || '');
|
};
|
|
/**
|
* 判断目标电子围栏是否与对端已选站点处于同一运营区。
|
*/
|
const isSameOperatingAreaById = (electronicAreaId?: string | number) => {
|
const referenceElectronicAreaId = getReferenceElectronicAreaId();
|
const targetElectronicAreaId = String(electronicAreaId || '');
|
if (!referenceElectronicAreaId) {
|
return true;
|
}
|
if (!targetElectronicAreaId) {
|
return false;
|
}
|
return referenceElectronicAreaId === targetElectronicAreaId;
|
};
|
|
/**
|
* 解析地图围栏所属运营区 ID,围栏数据优先使用 areaId,与缓存的 electronicAreaId 对齐比对。
|
*/
|
const getMapGeofenceAreaId = (geofence: any) => {
|
return String(geofence?.areaId || geofence?.electronicAreaId || '');
|
};
|
|
/**
|
* 判断目标站点是否属于当前允许的同运营区范围。
|
*/
|
const isSelectableInSameOperatingArea = (site: any) => {
|
return isSameOperatingAreaById(site?.electronicAreaId);
|
};
|
|
/**
|
* 过滤当前允许参与吸附和选择的同运营区候选站点。
|
*/
|
const filterSelectableSites = (sites: any[]) => {
|
if (!Array.isArray(sites)) {
|
return [];
|
}
|
return sites.filter((site) => isSelectableInSameOperatingArea(site));
|
};
|
|
/**
|
* 从 marker 中解析对应的业务站点对象,优先使用完整的站点数据。
|
*/
|
const findSiteByMarker = (marker: any, candidateSites?: any[]) => {
|
const sourceSites = Array.isArray(candidateSites) ? candidateSites : state.nearbySites;
|
return sourceSites.find((item: any) => {
|
const markerPointId = String(marker?.pointId || marker?.id || '');
|
const sitePointId = String(item?.pointId || item?.id || '');
|
if (markerPointId && sitePointId && markerPointId === sitePointId) {
|
return true;
|
}
|
return Number(item?.latitude) === Number(marker?.latitude) && Number(item?.longitude) === Number(marker?.longitude);
|
});
|
};
|
|
/**
|
* 根据允许选择的站点列表,构造可参与吸附的 marker 集合。
|
*/
|
const getSelectableMarkers = (markers: any[], candidateSites: any[]) => {
|
const selectableSites = filterSelectableSites(candidateSites);
|
if (selectableSites.length === 0) {
|
return [];
|
}
|
const selectablePointIds = new Set(
|
selectableSites.map((item: any) => String(item?.pointId || item?.id || '')).filter(Boolean)
|
);
|
return markers.filter((marker: any) => {
|
const markerPointId = String(marker?.pointId || marker?.id || '');
|
if (markerPointId && selectablePointIds.has(markerPointId)) {
|
return true;
|
}
|
return selectableSites.some((site: any) => (
|
Number(site?.latitude) === Number(marker?.latitude)
|
&& Number(site?.longitude) === Number(marker?.longitude)
|
));
|
});
|
};
|
|
/**
|
* 判断当前中心点附近是否存在跨运营区但距离满足吸附阈值的候选站点。
|
*/
|
const hasCrossOperatingAreaCandidateNearby = (lat: number, lng: number, markers: any[], candidateSites: any[]) => {
|
if (!getReferenceElectronicAreaId()) {
|
return false;
|
}
|
return markers.some((marker: any) => {
|
const site = findSiteByMarker(marker, candidateSites) || marker;
|
if (isSelectableInSameOperatingArea(site)) {
|
return false;
|
}
|
return getDistance(lat, lng, marker.latitude, marker.longitude) < state.distanceConfig.adsorptionDistance;
|
});
|
};
|
|
/**
|
* 计算是否超出可选范围(运营区外或起终点电子围栏不一致)。
|
*/
|
const getRangeStatus = (inside: boolean, site: any, fallbackElectronicAreaId?: string | number) => {
|
const effectiveElectronicAreaId = String(site?.electronicAreaId || fallbackElectronicAreaId || '');
|
const isCrossArea = !isSameOperatingAreaById(effectiveElectronicAreaId);
|
if (!inside) {
|
return SELECT_SITE_RANGE_STATUS.outOfOperatingArea;
|
}
|
if (isCrossArea) {
|
return SELECT_SITE_RANGE_STATUS.crossOperatingArea;
|
}
|
return SELECT_SITE_RANGE_STATUS.normal;
|
};
|
|
/**
|
* 当前是否存在对端已选站点,用于约束同运营区选择。
|
*/
|
const referenceElectronicAreaId = useMemo(() => getReferenceElectronicAreaId(), [state.pageType, state.selectedSite?.electronicAreaId]);
|
|
/**
|
* 跨运营区态下底部提示文案。
|
*/
|
const crossOperatingAreaHintText = useMemo(() => (
|
state.pageType === 'start'
|
? '与目的地不在同一个运营区,请重新选择'
|
: '与上车点不在同一个运营区,请重新选择'
|
), [state.pageType]);
|
|
/**
|
* 跨运营区态下的确认按钮文案。
|
*/
|
const crossOperatingAreaConfirmButtonText = useMemo(() => (
|
state.pageType === 'start' ? '确认上车点' : '确认目的地站点'
|
), [state.pageType]);
|
|
/**
|
* 当前可参与展示计算的原始围栏数据。
|
*/
|
const renderableGeofenceList = useMemo(() => {
|
return filterRenderableGeofence(state.geofenceListRef.current || []);
|
}, [state.polygons]);
|
|
/**
|
* 同运营区围栏集合,保持原始蓝色/场站色展示。
|
*/
|
const selectableGeofenceList = useMemo(() => {
|
if (!referenceElectronicAreaId) {
|
return renderableGeofenceList;
|
}
|
return renderableGeofenceList.filter((item: any) => isSameOperatingAreaById(getMapGeofenceAreaId(item)));
|
}, [referenceElectronicAreaId, renderableGeofenceList]);
|
|
/**
|
* 非同运营区围栏集合,统一灰显处理。
|
*/
|
const disabledGeofenceList = useMemo(() => {
|
if (!referenceElectronicAreaId) {
|
return [];
|
}
|
return renderableGeofenceList.filter((item: any) => !isSameOperatingAreaById(getMapGeofenceAreaId(item)));
|
}, [referenceElectronicAreaId, renderableGeofenceList]);
|
|
/**
|
* 同运营区围栏展示数据,保持原始配色。
|
*/
|
const selectableGeofenceDisplay = useMemo(() => {
|
if (selectableGeofenceList.length === 0) {
|
return { polygons: [] };
|
}
|
return formatGeofenceByType(selectableGeofenceList);
|
}, [selectableGeofenceList]);
|
|
/**
|
* 围栏灰显展示:仅保留同运营区围栏原色,其余围栏统一置灰。
|
*/
|
const displayPolygons = useMemo(() => {
|
if (!referenceElectronicAreaId || renderableGeofenceList.length === 0) {
|
return state.polygons;
|
}
|
const disabledPolygons = formatGeofenceByType(disabledGeofenceList).polygons.map((polygon: any) => ({
|
...polygon,
|
fillColor: CROSS_OPERATING_AREA_FILL_COLOR,
|
strokeColor: CROSS_OPERATING_AREA_STROKE_COLOR,
|
strokeWidth: CROSS_OPERATING_AREA_STROKE_WIDTH,
|
dashArray: CROSS_OPERATING_AREA_DASH_ARRAY
|
}));
|
return [...selectableGeofenceDisplay.polygons, ...disabledPolygons];
|
}, [
|
disabledGeofenceList,
|
referenceElectronicAreaId,
|
renderableGeofenceList.length,
|
selectableGeofenceDisplay.polygons,
|
state.polygons
|
]);
|
|
/**
|
* 当前可选运营区围栏集合,用于“查看运营区域”回视图。
|
*/
|
const selectablePolygons = useMemo(() => (
|
referenceElectronicAreaId && renderableGeofenceList.length > 0
|
? selectableGeofenceDisplay.polygons
|
: state.polygons
|
), [referenceElectronicAreaId, renderableGeofenceList.length, selectableGeofenceDisplay.polygons, state.polygons]);
|
|
/**
|
* 是否展示“查看运营区域”按钮。
|
*/
|
const showViewOperatingAreaButton = useMemo(() => (
|
(
|
state.rangeStatus === SELECT_SITE_RANGE_STATUS.crossOperatingArea
|
|| state.rangeStatus === SELECT_SITE_RANGE_STATUS.outOfOperatingArea
|
)
|
&& !!referenceElectronicAreaId
|
&& selectablePolygons.length > 0
|
), [referenceElectronicAreaId, selectablePolygons.length, state.rangeStatus]);
|
|
/**
|
* 更新地图业务数据:围栏内判断、站点列表、自动缩放。
|
* @param seq 可选的请求序号,传入时仅当其为最新一次拖动请求才会回写页面状态,
|
* 用于丢弃连续/快速拖动时乱序返回的过期结果,避免旧站点数据回填。
|
*/
|
const updateMapData = async (lat: number, lng: number, seq?: number) => {
|
const selectedCity = Taro.getStorageSync('selectedCity');
|
const pointOnOff = state.pageType === 'start'
|
? SELECT_MAP_POINT_ON_OFF.start
|
: SELECT_MAP_POINT_ON_OFF.end;
|
state.pendingNearbyRequestCountRef.current += 1;
|
state.setNearbyLoading(true);
|
try {
|
const { isInside, newMarkers: rawMarkers, sites, electronicAreaId } = await fetchMapData(
|
lat,
|
lng,
|
selectedCity?.code,
|
pointOnOff,
|
{ allowedElectronicAreaId: referenceElectronicAreaId }
|
);
|
// 站点紫色图标替换为金色(与首页一致)
|
const newMarkers = applySiteMarkerIcon(rawMarkers);
|
// 仅最新一次拖动请求才回写状态;过期请求只返回数据、不污染 markers/nearbySites 等状态,
|
// 防止过期 nearbySites 触发场站副作用回填 selectedSite,导致大头针离开点位后旧信息滞留。
|
const isStaleRequest = typeof seq === 'number' && seq !== state.regionSeqRef.current;
|
if (!isStaleRequest) {
|
state.setIsOperatingArea(isInside);
|
state.setMarkers(newMarkers);
|
state.setNearbySites(sites);
|
if (state.selectionMode === 'station' && state.selectedStation?.stationId) {
|
const nextStationSites = filterSelectableSites(sites).filter((item: any) => String(item?.stationId || '') === String(state.selectedStation.stationId));
|
state.setStationSiteList(nextStationSites);
|
}
|
|
const isStationFocused = state.selectedSite && state.selectedSite.stationId;
|
|
if (!isStationFocused && !state.autoScaleLockedRef.current) {
|
const autoScale = getAutoScaleBySites(sites?.length > 0 ? sites : newMarkers, lat, lng);
|
if (typeof autoScale === 'number') {
|
state.setMapScale((prev) => {
|
const nextScale = Math.max(AUTO_SCALE_MIN, Math.min(AUTO_SCALE_MAX, autoScale));
|
if (Math.abs(prev - nextScale) < 0.01) return prev;
|
return nextScale;
|
});
|
}
|
state.autoScaleLockedRef.current = true;
|
}
|
}
|
return { isInside, newMarkers, sites, electronicAreaId };
|
} finally {
|
state.pendingNearbyRequestCountRef.current = Math.max(0, state.pendingNearbyRequestCountRef.current - 1);
|
if (state.pendingNearbyRequestCountRef.current === 0) {
|
state.setNearbyLoading(false);
|
}
|
}
|
};
|
|
/**
|
* 获取用户定位并作为地图中心点。
|
*/
|
const getLocationInfo = () => {
|
Taro.getLocation({ type: 'gcj02' }).then((res) => {
|
state.setLatitude(res.latitude);
|
state.setLongitude(res.longitude);
|
syncPinLocation(res.latitude, res.longitude);
|
updateMapData(res.latitude, res.longitude).then(({ isInside, electronicAreaId }) => {
|
state.setRangeStatus(getRangeStatus(isInside, state.selectedSite, electronicAreaId));
|
});
|
});
|
};
|
|
/**
|
* 获取用户定位,仅用于步行路线起点计算。
|
*/
|
const fetchUserLocation = () => {
|
Taro.getLocation({ type: 'gcj02' }).then((res) => {
|
state.setUserLatitude(res.latitude);
|
state.setUserLongitude(res.longitude);
|
});
|
};
|
|
/**
|
* 返回当前位置并重新吸附最近站点。
|
*/
|
const toOrigin = () => {
|
const mapCtx = Taro.createMapContext('select-site-map');
|
mapCtx.moveToLocation({
|
success: () => {
|
handleRegionChange({ type: 'end', causedBy: 'gesture' });
|
}
|
});
|
};
|
|
/**
|
* 将当前可选运营区围栏移动到视图内。
|
*/
|
const handleViewOperatingArea = () => {
|
const points = extractPointsFromPolygons(selectablePolygons);
|
if (points.length === 0) {
|
showToast({ title: '暂无可选运营区域', icon: 'none' });
|
return;
|
}
|
const mapCtx = Taro.createMapContext('select-site-map');
|
mapCtx.includePoints({
|
points,
|
padding: INCLUDE_POINTS_PADDING
|
});
|
};
|
|
/**
|
* 地图区域变化时触发,结束后按阈值吸附到最近站点。
|
*/
|
const handleRegionChange = (e: any) => {
|
const type = e?.type || e?.detail?.type;
|
const causedBy = e?.causedBy || e?.detail?.causedBy;
|
if (type === 'begin') {
|
state.setShowToolTip(false);
|
return;
|
}
|
if (type !== 'end') return;
|
state.setBounce(true);
|
|
const mapCtx = Taro.createMapContext('select-site-map');
|
// 获取真实的缩放等级并通过回调传递出去,彻底阻断与 mapScale 状态的绑定,避免触发原生组件抖动
|
mapCtx.getScale({
|
success: (res) => {
|
if (typeof props?.onScaleChange === 'function') {
|
props.onScaleChange(res.scale);
|
}
|
}
|
});
|
|
if (causedBy === 'gesture' || causedBy === 'drag' || causedBy === 'scale') {
|
state.pickupConfirmTypeRef.current = 'drag';
|
// 同步自增请求序号,标记本次拖动为「最新」。连续/快速拖动时每个 end 事件都会派生一条
|
// 独立异步链,借助该序号在异步返回后判定是否为最新一次,丢弃乱序返回的过期结果。
|
const currentRegionSeq = (state.regionSeqRef.current += 1);
|
mapCtx.getCenterLocation({
|
success: async (res) => {
|
const { latitude: lat, longitude: lng } = res;
|
const { newMarkers, sites, isInside, electronicAreaId } = await updateMapData(lat, lng, currentRegionSeq);
|
// 已有更新的拖动发生,当前为过期结果,直接丢弃,避免用旧中心点的吸附/清空结论覆盖最新状态。
|
if (currentRegionSeq !== state.regionSeqRef.current) return;
|
const selectableMarkers = getSelectableMarkers(newMarkers, sites);
|
const { targetMarker, minDistance } = findNearestMarker(lat, lng, selectableMarkers);
|
if (targetMarker && minDistance < state.distanceConfig.adsorptionDistance) {
|
mapCtx.moveToLocation({
|
latitude: targetMarker.latitude,
|
longitude: targetMarker.longitude,
|
});
|
const activeSite = applySelectionBySite(findSiteByMarker(targetMarker, sites) || targetMarker, filterSelectableSites(sites));
|
state.setRangeStatus(getRangeStatus(isInside, activeSite));
|
state.setLatitude(activeSite?.latitude || targetMarker.latitude);
|
state.setLongitude(activeSite?.longitude || targetMarker.longitude);
|
syncPinLocation(activeSite?.latitude || targetMarker.latitude, activeSite?.longitude || targetMarker.longitude);
|
state.setShowToolTip(true);
|
} else {
|
resetStationSelection();
|
state.setSelectedSite({});
|
state.setRangeStatus(
|
isInside && !isSameOperatingAreaById(electronicAreaId)
|
? SELECT_SITE_RANGE_STATUS.crossOperatingArea
|
: isInside && hasCrossOperatingAreaCandidateNearby(lat, lng, newMarkers, sites)
|
? SELECT_SITE_RANGE_STATUS.crossOperatingArea
|
: getRangeStatus(isInside, {}, electronicAreaId)
|
);
|
// 只有当坐标发生实质性变化才去更新 React 状态中的经纬度,避免频繁反推给原生组件导致抖动
|
if (Math.abs(state.latitude - lat) > 0.0001 || Math.abs(state.longitude - lng) > 0.0001) {
|
state.setLatitude(lat);
|
state.setLongitude(lng);
|
}
|
syncPinLocation(lat, lng);
|
state.setShowToolTip(true);
|
}
|
}
|
});
|
}
|
setTimeout(() => {
|
state.setBounce(false);
|
state.setShowToolTip(true);
|
}, 600);
|
};
|
|
/**
|
* 地图 marker 点击:直接聚焦并更新当前选中站点。
|
*/
|
const handleMarkerTap = (e: any) => {
|
const markerId = e?.detail?.markerId;
|
if (markerId === undefined) return;
|
const targetMarker = state.markers.find((m) => m.id === markerId);
|
if (!targetMarker) return;
|
const targetSite = findSiteByMarker(targetMarker, state.nearbySites) || targetMarker;
|
state.pickupConfirmTypeRef.current = 'click';
|
state.setShowToolTip(false);
|
state.setBounce(true);
|
state.setLatitude(targetMarker.latitude);
|
state.setLongitude(targetMarker.longitude);
|
setTimeout(() => {
|
state.setBounce(false);
|
if (!isSelectableInSameOperatingArea(targetSite)) {
|
resetStationSelection();
|
state.setSelectedSite({});
|
state.setRangeStatus(SELECT_SITE_RANGE_STATUS.crossOperatingArea);
|
syncPinLocation(targetMarker.latitude, targetMarker.longitude);
|
state.setShowToolTip(true);
|
return;
|
}
|
const activeSite = applySelectionBySite(targetSite, filterSelectableSites(state.nearbySites));
|
state.setRangeStatus(getRangeStatus(state.isOperatingArea, activeSite));
|
state.setLatitude(activeSite?.latitude || targetMarker.latitude);
|
state.setLongitude(activeSite?.longitude || targetMarker.longitude);
|
syncPinLocation(activeSite?.latitude || targetMarker.latitude, activeSite?.longitude || targetMarker.longitude);
|
state.setShowToolTip(true);
|
}, 600);
|
};
|
|
/**
|
* 场站模式切换子站点:仅更新选中站点,不切换场站。
|
*/
|
const changeStationSite = (site: any) => {
|
if (!site?.latitude || !site?.longitude) return;
|
state.pickupConfirmTypeRef.current = 'click';
|
state.setSelectedSite(site);
|
state.setLatitude(site.latitude);
|
state.setLongitude(site.longitude);
|
syncPinLocation(site.latitude, site.longitude);
|
const mapCtx = Taro.createMapContext('select-site-map');
|
mapCtx.moveToLocation({
|
latitude: site.latitude,
|
longitude: site.longitude,
|
});
|
};
|
|
/**
|
* 关闭客服弹窗。
|
*/
|
const handleCloseCustomerService = () => {
|
state.setShowCustomerService(false);
|
};
|
|
/**
|
* 打开客服电话拨打。
|
*/
|
const openService = () => {
|
safeMakePhoneCall();
|
};
|
|
/**
|
* 打开个人中心。
|
*/
|
const toPersonalCenter = async () => {
|
const otp = await checkOtp(false);
|
if (!otp) {
|
jumpTo({ url: '/pages/user/login?hideBack=1' });
|
return;
|
}
|
Taro.navigateTo({ url: '/robotaxi/pages/personal-center/index' });
|
};
|
|
/**
|
* 返回上一页。
|
*/
|
const toBack = () => {
|
Taro.navigateBack({ delta: 1 });
|
};
|
|
/**
|
* 获取距离配置,避免用户快速确认时使用到未初始化完成的配置。
|
*/
|
const ensureDistanceConfigReady = async () => ensureOrderDistanceConfig({
|
currentConfig: state.distanceConfig,
|
fetchConfig: getOrderDistance,
|
onConfigResolved: state.setDistanceConfig
|
});
|
|
/**
|
* 确认选择站点(包含起终点距离与电子围栏校验)。
|
*/
|
const confirmSite = async () => {
|
if (!state.selectedSite.name) {
|
showToast({ title: '请选择站点', icon: 'none' });
|
return;
|
}
|
if (state.rangeStatus === SELECT_SITE_RANGE_STATUS.crossOperatingArea) {
|
showToast({ title: '请在同一运营区内选择', icon: 'none' });
|
return;
|
}
|
if (state.pageType === 'start') {
|
const distanceConfig = await ensureDistanceConfigReady();
|
const endSiteCache = Taro.getStorageSync('endSite');
|
if (endSiteCache?.electronicAreaId && state.selectedSite?.electronicAreaId && endSiteCache.electronicAreaId !== state.selectedSite.electronicAreaId) {
|
showToast({ title: '请在同一运营区内选择', icon: 'none' });
|
return;
|
}
|
if (!isWithinPickupMaxDistance({
|
userLatitude: state.userLatitude,
|
userLongitude: state.userLongitude,
|
pickupSite: state.selectedSite,
|
maxDistance: distanceConfig?.maxDistance
|
})) {
|
state.setTipType(ORDER_DISTANCE_TIP_TYPE_MAX_DISTANCE);
|
state.setShowTipPopup(true);
|
return;
|
}
|
if (!isWithinStartEndMinDistance({
|
startSite: state.selectedSite,
|
endSite: endSiteCache,
|
minDistance: distanceConfig?.minDistance
|
})) {
|
state.setTipType(ORDER_DISTANCE_TIP_TYPE_MIN_DISTANCE);
|
state.setShowTipPopup(true);
|
return;
|
}
|
reportConfirmMapSiteEvent();
|
Taro.setStorageSync('startSite', state.selectedSite);
|
writeConfirmRideSiteSourceType('start', MAP_CONFIRM_RIDE_SOURCE_TYPE);
|
completeConfirmRideSelectionSession(state.selectedSite, endSiteCache);
|
writeHomeStartSiteDisplayState(state.selectedSite, HOME_START_SITE_SOURCE_MANUAL_SELECTED);
|
if (state.router.params.skipSource) {
|
Taro.navigateBack();
|
return;
|
}
|
Taro.navigateBack({ delta: 2 });
|
return;
|
}
|
const otp = await checkOtp(false);
|
if (!otp) {
|
jumpTo({ url: '/pages/user/login?hideBack=1' });
|
return;
|
}
|
const distanceConfig = await ensureDistanceConfigReady();
|
const startSiteCache = Taro.getStorageSync('startSite');
|
if (startSiteCache?.electronicAreaId && state.selectedSite?.electronicAreaId && startSiteCache.electronicAreaId !== state.selectedSite.electronicAreaId) {
|
showToast({ title: '请在同一运营区内选择', icon: 'none' });
|
return;
|
}
|
if (!isWithinStartEndMinDistance({
|
startSite: startSiteCache,
|
endSite: state.selectedSite,
|
minDistance: distanceConfig?.minDistance
|
})) {
|
state.setTipType(ORDER_DISTANCE_TIP_TYPE_MIN_DISTANCE);
|
state.setShowTipPopup(true);
|
return;
|
}
|
reportConfirmMapSiteEvent();
|
const modifySession = Taro.getStorageSync(MODIFY_DESTINATION_SESSION_KEY); // 修改目的地会话中将终点写入候选缓存,返回行程页进入预览态
|
// 增加 hasActiveTripOrderInStorage 联合判断,防止上一单异常终止导致 modifySession 缓存残留,污染新下单流程
|
const isModifyDestinationActive = modifySession?.active && hasActiveTripOrderInStorage();
|
|
// 【修改目的地拦截补充】接驾中选点相同或距离过近拦截
|
if (isModifyDestinationActive) {
|
const isPickupTripStatus = [4, 5].includes(modifySession.tripStatus); // 4: VehicleApproaching, 5: VehicleArrivedPickup
|
if (isPickupTripStatus) {
|
const isSameSite = isSameModifyDestinationSite(startSiteCache, state.selectedSite);
|
const isTooClose = !isWithinStartEndMinDistance({
|
startSite: startSiteCache,
|
endSite: state.selectedSite,
|
minDistance: distanceConfig?.minDistance
|
});
|
|
if (isSameSite || isTooClose) {
|
state.setTipType(ORDER_DISTANCE_TIP_TYPE_MIN_DISTANCE);
|
state.setShowTipPopup(true);
|
return;
|
}
|
}
|
}
|
|
if (isModifyDestinationActive && isSameModifyDestinationSite(modifySession?.previousEndSite, state.selectedSite)) {
|
Taro.setStorageSync(MODIFY_DESTINATION_PENDING_SAME_SITE_TOAST_KEY, 1);
|
}
|
if (isModifyDestinationActive) {
|
Taro.setStorageSync(MODIFY_DESTINATION_CANDIDATE_KEY, state.selectedSite);
|
Taro.setStorageSync(MODIFY_DESTINATION_SKIP_AC_QUERY_ON_RETURN_KEY, 1); // 修改目的地返回行程页时跳过一次空调/温度状态查询
|
} else {
|
Taro.setStorageSync('endSite', state.selectedSite);
|
}
|
writeConfirmRideSiteSourceType('end', MAP_CONFIRM_RIDE_SOURCE_TYPE);
|
completeConfirmRideSelectionSession(startSiteCache, state.selectedSite);
|
const returnToBeginRouteUrl = isModifyDestinationActive
|
? '/robotaxi/pages/begin-route/index?skipPageFlag=1&fromModifyDestination=1'
|
: (Taro.getStorageSync('orderId') ? '/robotaxi/pages/begin-route/index?skipPageFlag=1' : '/robotaxi/pages/begin-route/index');
|
Taro.reLaunch({
|
url: returnToBeginRouteUrl,
|
});
|
};
|
|
/**
|
* 关闭提示弹窗。
|
*/
|
const handleCloseTipPopup = () => {
|
state.setShowTipPopup(false);
|
setTimeout(() => {
|
state.setTipType('');
|
}, 600);
|
};
|
|
/**
|
* 跳转优惠券列表,未登录时先引导登录。
|
*/
|
const toCoupon = async () => {
|
const otp = await checkOtp(false);
|
if (!otp) {
|
jumpTo({ url: '/pages/user/login?hideBack=1' });
|
return;
|
}
|
Taro.navigateTo({ url: '/robotaxi/pages/coupon/index' });
|
};
|
|
/**
|
* 收藏/取消收藏当前选中站点。
|
*/
|
const handleCollect = async (e: any) => {
|
e.stopPropagation();
|
try {
|
const isStationMode = state.selectionMode === 'station' && state.selectedStation?.stationId;
|
const collectSource = isStationMode ? state.selectedStation : state.selectedSite;
|
const type = isStationMode
|
? (collectSource?.isCollect ? 2 : 1)
|
: (collectSource?.isCollect === 1 ? 2 : 1);
|
const payload: any = isStationMode
|
? { itemType: 2, stationId: String(state.selectedStation.stationId), type } // 场站收藏只传 stationId
|
: {
|
itemType: 1,
|
latitude: state.selectedSite.latitude,
|
longitude: state.selectedSite.longitude,
|
type
|
};
|
if (!isStationMode) {
|
if (state.selectedSite.pointId) {
|
payload.pointId = state.selectedSite.pointId;
|
} else if (state.selectedSite.id) {
|
payload.pointId = state.selectedSite.id;
|
}
|
}
|
await collectPoint(payload);
|
if (isStationMode) {
|
const nextStation = { ...state.selectedStation, isCollect: type === 1 };
|
state.setSelectedStation(nextStation);
|
state.stationMetaMapRef.current[String(nextStation.stationId)] = nextStation;
|
} else {
|
const nextSite = { ...state.selectedSite, isCollect: type };
|
state.setSelectedSite(nextSite);
|
if (state.pageType === 'start') {
|
Taro.setStorageSync('startSite', nextSite);
|
}
|
if (state.pageType === 'end') {
|
Taro.setStorageSync('endSite', nextSite);
|
}
|
}
|
showToast({
|
title: type === 2 ? '已取消收藏' : '收藏成功',
|
icon: 'none'
|
});
|
} catch (error) {
|
console.log('收藏操作失败', error);
|
}
|
};
|
|
useEffect(() => {
|
if (!state.selectedSite?.electronicAreaId) return; // 空 site 时无法判断当前中心点运营区,交由 handleRegionChange/handleMarkerTap 显式设值
|
state.setRangeStatus(getRangeStatus(state.isOperatingArea, state.selectedSite));
|
}, [referenceElectronicAreaId, state.isOperatingArea, state.pageType, state.selectedSite?.electronicAreaId]);
|
|
useEffect(() => {
|
const systemInfo = Taro.getSystemInfoSync();
|
state.setIsAndroid(systemInfo?.platform === 'android');
|
const pType = state.router.params.pageType || 'end';
|
state.setPageType(pType);
|
|
let cachedSite: any = null;
|
if (pType === 'start') {
|
cachedSite = Taro.getStorageSync('startSite');
|
} else if (pType === 'end') {
|
cachedSite = Taro.getStorageSync('endSite');
|
if (!cachedSite) {
|
cachedSite = Taro.getStorageSync('startSite');
|
}
|
}
|
|
const hasCachedSite = !!(cachedSite && cachedSite.latitude && cachedSite.longitude);
|
if (hasCachedSite) {
|
state.setLatitude(cachedSite.latitude);
|
state.setLongitude(cachedSite.longitude);
|
applySelectionBySite(cachedSite);
|
updateMapData(cachedSite.latitude, cachedSite.longitude).then(({ isInside, electronicAreaId }) => {
|
state.setRangeStatus(getRangeStatus(isInside, cachedSite, electronicAreaId));
|
});
|
}
|
fetchUserLocation();
|
|
getOrderDistance().then((res: any) => {
|
if (res) {
|
state.setDistanceConfig(normalizeOrderDistanceConfig(res));
|
}
|
}).catch((err) => {
|
console.error('获取距离配置失败', err);
|
});
|
|
const selectedCity = Taro.getStorageSync('selectedCity');
|
if (selectedCity && selectedCity.code) {
|
getGeofence({ cityCode: selectedCity.code }).then((res: any) => {
|
if (res && Array.isArray(res)) {
|
const renderableGeofenceList = filterRenderableGeofence(res);
|
const { polygons: newPolygons } = formatGeofenceByType(renderableGeofenceList);
|
state.geofenceListRef.current = renderableGeofenceList;
|
state.stationMetaMapRef.current = buildStationMetaMap(renderableGeofenceList);
|
state.setPolygons(newPolygons.map((polygon: any, index: number) => ({
|
...polygon,
|
electronicAreaId: String(renderableGeofenceList[index]?.electronicAreaId || ''),
|
geofenceType: Number(renderableGeofenceList[index]?.type || 0)
|
})));
|
if (!hasCachedSite && renderableGeofenceList.length > 0 && renderableGeofenceList[0]?.points?.length > 0) {
|
const firstPoint = renderableGeofenceList[0].points[0];
|
const lat = Number(firstPoint.lat);
|
const lng = Number(firstPoint.lng);
|
state.setLatitude(lat);
|
state.setLongitude(lng);
|
updateMapData(lat, lng).then(({ isInside, electronicAreaId }) => {
|
state.setRangeStatus(getRangeStatus(isInside, {}, electronicAreaId));
|
});
|
const mapCtx = Taro.createMapContext('select-site-map');
|
const points = extractPointsFromPolygons(newPolygons);
|
mapCtx.includePoints({
|
points,
|
padding: INCLUDE_POINTS_PADDING
|
});
|
} else if (!hasCachedSite) {
|
getLocationInfo();
|
}
|
} else {
|
state.geofenceListRef.current = [];
|
state.stationMetaMapRef.current = {};
|
state.setPolygons([]);
|
if (!hasCachedSite) {
|
getLocationInfo();
|
}
|
}
|
}).catch((err) => {
|
console.error('获取电子围栏失败', err);
|
state.geofenceListRef.current = [];
|
state.stationMetaMapRef.current = {};
|
state.setPolygons([]);
|
if (!hasCachedSite) {
|
getLocationInfo();
|
}
|
});
|
} else if (!hasCachedSite) {
|
getLocationInfo();
|
}
|
}, []);
|
|
useEffect(() => {
|
let cancelled = false;
|
const run = async () => {
|
const local = await preloadImageToTemp(state.tooltipBgUrl);
|
if (!cancelled) state.setTooltipBgSrc(local);
|
};
|
run();
|
return () => { cancelled = true; };
|
}, []);
|
|
// 从 site-search 返回时同步已选择的站点
|
useDidShow(() => {
|
const pType = state.router.params.pageType || state.pageType || 'end';
|
let cachedSite: any = null;
|
if (pType === 'start') {
|
cachedSite = Taro.getStorageSync('startSite');
|
} else if (pType === 'end') {
|
cachedSite = Taro.getStorageSync('endSite');
|
}
|
if (cachedSite && cachedSite.latitude && cachedSite.longitude) {
|
const currentId = String(state.selectedSite?.pointId || state.selectedSite?.id || '');
|
const cachedId = String(cachedSite?.pointId || cachedSite?.id || '');
|
if (currentId !== cachedId) {
|
state.setLatitude(cachedSite.latitude);
|
state.setLongitude(cachedSite.longitude);
|
applySelectionBySite(cachedSite);
|
updateMapData(cachedSite.latitude, cachedSite.longitude).then(({ isInside }) => {
|
state.setRangeStatus(getRangeStatus(isInside, cachedSite));
|
});
|
}
|
}
|
});
|
|
useSelectMapSiteWalkingRoute({
|
pageType: state.pageType,
|
userLatitude: state.userLatitude,
|
userLongitude: state.userLongitude,
|
latitude: state.latitude,
|
longitude: state.longitude,
|
selectedSite: state.selectedSite,
|
isAndroid: state.isAndroid,
|
setPolyline: state.setPolyline,
|
setWalkingDistance: state.setWalkingDistance,
|
setWalkingDuration: state.setWalkingDuration,
|
});
|
|
useEffect(() => {
|
if (state.selectionMode !== 'station') return;
|
if (!state.selectedStation?.stationId) return;
|
const stationId = String(state.selectedStation.stationId);
|
const nextStationSites = filterSelectableSites(state.nearbySites).filter((item: any) => String(item?.stationId || '') === stationId);
|
state.setStationSiteList(nextStationSites);
|
if (!state.selectedSite?.stationId || String(state.selectedSite.stationId) !== stationId) {
|
if (nextStationSites.length > 0) {
|
state.setSelectedSite(nextStationSites[0]);
|
}
|
}
|
}, [state.nearbySites, state.selectionMode, state.selectedStation?.stationId]);
|
|
/**
|
* 兜底校正场站模式,避免切到场站外点位后底部仍停留在旧场站卡片。
|
*/
|
useEffect(() => {
|
if (state.selectionMode !== 'station') return;
|
const selectedSiteStationId = state.selectedSite?.stationId ? String(state.selectedSite.stationId) : '';
|
const selectedStationId = state.selectedStation?.stationId ? String(state.selectedStation.stationId) : '';
|
if (!selectedSiteStationId || selectedSiteStationId !== selectedStationId) {
|
resetStationSelection();
|
}
|
}, [state.selectedSite?.stationId, state.selectedStation?.stationId, state.selectionMode]);
|
|
useEffect(() => {
|
const stationId = state.selectedSite?.stationId ? String(state.selectedSite.stationId) : '';
|
if (!stationId || state.selectionMode === 'station') return;
|
if (!state.stationMetaMapRef.current[stationId]) return;
|
if (!Array.isArray(state.nearbySites) || state.nearbySites.length === 0) return;
|
applySelectionBySite(state.selectedSite, state.nearbySites);
|
}, [state.nearbySites, state.selectedSite?.stationId, state.selectionMode]);
|
|
return {
|
longitude: state.longitude,
|
latitude: state.latitude,
|
mapScale: state.mapScale,
|
markers: state.markers,
|
mapPolylines: state.mapPolylines,
|
polygons: displayPolygons,
|
handleRegionChange,
|
handleMarkerTap,
|
toBack,
|
openService,
|
toCoupon,
|
nearbyLoading: state.nearbyLoading,
|
isAndroid: state.isAndroid,
|
waitingRadarSrc: state.waitingRadarSrc,
|
showToolTip: state.showToolTip,
|
selectedSite: state.selectedSite,
|
selectionMode: state.selectionMode,
|
selectedStation: state.selectedStation,
|
stationSiteList: state.stationSiteList,
|
rangeStatus: state.rangeStatus,
|
tooltipBgSrc: state.tooltipBgSrc,
|
pageType: state.pageType,
|
bounce: state.bounce,
|
toOrigin,
|
walkingDuration: state.walkingDuration,
|
walkingDistance: state.walkingDistance,
|
nearbySites: state.nearbySites,
|
handleCollect,
|
changeStationSite,
|
confirmSite,
|
confirmButtonText: state.rangeStatus === SELECT_SITE_RANGE_STATUS.crossOperatingArea
|
? crossOperatingAreaConfirmButtonText
|
: (state.pageType === 'start' ? '确认上车点' : '确认目的地站点'),
|
confirmButtonDisabled: state.rangeStatus === SELECT_SITE_RANGE_STATUS.crossOperatingArea || !state.selectedSite.name,
|
crossOperatingAreaHintText,
|
showViewOperatingAreaButton,
|
handleViewOperatingArea,
|
showCustomerService: state.showCustomerService,
|
handleCloseCustomerService,
|
showTipPopup: state.showTipPopup,
|
handleCloseTipPopup,
|
tipType: state.tipType,
|
toPersonalCenter,
|
};
|
}
|