| | |
| | | import Taro, { useDidShow } from "@tarojs/taro"; |
| | | import { useEffect } from 'react'; |
| | | import { useEffect, useMemo } from 'react'; |
| | | import { |
| | | buildStationMetaMap, |
| | | findNearestMarker, |
| | |
| | | getAutoScaleBySites, |
| | | getDistance, |
| | | fetchMapData, |
| | | filterRenderableGeofence, |
| | | extractPointsFromPolygons, |
| | | preloadImageToTemp, |
| | | reportEvent, |
| | |
| | | completeConfirmRideSelectionSession, |
| | | writeConfirmRideSiteSourceType |
| | | } from '../../../constants/confirm-ride-report'; |
| | | import { GeofenceType } from '../../../constants/geofence'; |
| | | |
| | | 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 = 'rgba(0, 0, 0, 0.25)'; // 非可选运营区围栏填充色 |
| | | const CROSS_OPERATING_AREA_STROKE_COLOR = 'rgba(0, 0, 0, 0.4)'; // 非可选运营区围栏描边色 |
| | | const SELECT_MAP_POINT_ON_OFF = { |
| | | start: 1, |
| | | end: 2 |
| | |
| | | }; |
| | | |
| | | /** |
| | | * 判断目标站点是否属于当前允许的同运营区范围。 |
| | | * 判断目标电子围栏是否与对端已选站点处于同一运营区。 |
| | | */ |
| | | const isSelectableInSameOperatingArea = (site: any) => { |
| | | const isSameOperatingAreaById = (electronicAreaId?: string | number) => { |
| | | const referenceElectronicAreaId = getReferenceElectronicAreaId(); |
| | | const targetElectronicAreaId = String(electronicAreaId || ''); |
| | | if (!referenceElectronicAreaId) { |
| | | return true; |
| | | } |
| | | const siteElectronicAreaId = String(site?.electronicAreaId || ''); |
| | | if (!siteElectronicAreaId) { |
| | | if (!targetElectronicAreaId) { |
| | | return false; |
| | | } |
| | | return referenceElectronicAreaId === siteElectronicAreaId; |
| | | 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 getRangeStatus = (inside: boolean, site: any) => { |
| | | const cachedStartSite = Taro.getStorageSync('startSite'); |
| | | const cachedEndSite = Taro.getStorageSync('endSite'); |
| | | const isCrossArea = (state.pageType === 'start' |
| | | ? (cachedEndSite?.electronicAreaId && site?.electronicAreaId && cachedEndSite.electronicAreaId !== site.electronicAreaId) |
| | | : (cachedStartSite?.electronicAreaId && site?.electronicAreaId && cachedStartSite.electronicAreaId !== site.electronicAreaId)); |
| | | 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; |
| | | } |
| | |
| | | } |
| | | 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.fencePolylines, 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: [], fencePolylines: [] }; |
| | | } |
| | | 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: 'rgba(0, 0, 0, 0)' |
| | | })); |
| | | return [...selectableGeofenceDisplay.polygons, ...disabledPolygons]; |
| | | }, [ |
| | | disabledGeofenceList, |
| | | referenceElectronicAreaId, |
| | | renderableGeofenceList.length, |
| | | selectableGeofenceDisplay.polygons, |
| | | state.polygons |
| | | ]); |
| | | |
| | | /** |
| | | * 围栏边框灰显展示:非可选运营区统一输出灰色虚线边框。 |
| | | */ |
| | | const displayFencePolylines = useMemo(() => { |
| | | if (!referenceElectronicAreaId || renderableGeofenceList.length === 0) { |
| | | return state.fencePolylines; |
| | | } |
| | | const disabledFencePolylines = disabledGeofenceList.reduce((acc: any[], item: any, index: number) => { |
| | | if (!Array.isArray(item?.points) || item.points.length < 2) { |
| | | return acc; |
| | | } |
| | | const linePoints = item.points.map((point: any) => ({ |
| | | latitude: point.lat, |
| | | longitude: point.lng |
| | | })); |
| | | acc.push({ |
| | | id: `cross_operating_area_${index}_${getMapGeofenceAreaId(item)}`, |
| | | points: [...linePoints, linePoints[0]], |
| | | color: CROSS_OPERATING_AREA_STROKE_COLOR, |
| | | width: 2, |
| | | dottedLine: true, |
| | | arrowLine: false |
| | | }); |
| | | return acc; |
| | | }, []); |
| | | return [...selectableGeofenceDisplay.fencePolylines, ...disabledFencePolylines]; |
| | | }, [ |
| | | disabledGeofenceList, |
| | | referenceElectronicAreaId, |
| | | renderableGeofenceList.length, |
| | | selectableGeofenceDisplay.fencePolylines, |
| | | state.fencePolylines |
| | | ]); |
| | | |
| | | /** |
| | | * 地图最终折线集合:跨运营区灰显后的围栏边框 + 步行路线。 |
| | | */ |
| | | const displayMapPolylines = useMemo(() => { |
| | | return [...displayFencePolylines, ...state.polyline]; |
| | | }, [displayFencePolylines, state.polyline]); |
| | | |
| | | /** |
| | | * 当前可选运营区围栏集合,用于“查看运营区域”回视图。 |
| | | */ |
| | | 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]); |
| | | |
| | | /** |
| | | * 更新地图业务数据:围栏内判断、站点列表、自动缩放。 |
| | |
| | | state.pendingNearbyRequestCountRef.current += 1; |
| | | state.setNearbyLoading(true); |
| | | try { |
| | | const { isInside, newMarkers, sites } = await fetchMapData(lat, lng, selectedCity?.code, pointOnOff); |
| | | const { isInside, newMarkers, sites, electronicAreaId } = await fetchMapData( |
| | | lat, |
| | | lng, |
| | | selectedCity?.code, |
| | | pointOnOff, |
| | | { allowedElectronicAreaId: referenceElectronicAreaId } |
| | | ); |
| | | state.setIsOperatingArea(isInside); |
| | | state.setMarkers(newMarkers); |
| | | state.setNearbySites(sites); |
| | |
| | | } |
| | | state.autoScaleLockedRef.current = true; |
| | | } |
| | | return { isInside, newMarkers, sites }; |
| | | return { isInside, newMarkers, sites, electronicAreaId }; |
| | | } finally { |
| | | state.pendingNearbyRequestCountRef.current = Math.max(0, state.pendingNearbyRequestCountRef.current - 1); |
| | | if (state.pendingNearbyRequestCountRef.current === 0) { |
| | |
| | | state.setLatitude(res.latitude); |
| | | state.setLongitude(res.longitude); |
| | | syncPinLocation(res.latitude, res.longitude); |
| | | updateMapData(res.latitude, res.longitude).then(({ isInside }) => { |
| | | state.setRangeStatus(getRangeStatus(isInside, state.selectedSite)); |
| | | updateMapData(res.latitude, res.longitude).then(({ isInside, electronicAreaId }) => { |
| | | state.setRangeStatus(getRangeStatus(isInside, state.selectedSite, electronicAreaId)); |
| | | }); |
| | | }); |
| | | }; |
| | |
| | | success: () => { |
| | | handleRegionChange({ type: 'end', causedBy: 'gesture' }); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | /** |
| | | * 将当前可选运营区围栏移动到视图内。 |
| | | */ |
| | | const handleViewOperatingArea = () => { |
| | | const points = extractPointsFromPolygons(selectablePolygons); |
| | | if (points.length === 0) { |
| | | Taro.showToast({ title: '暂无可选运营区域', icon: 'none' }); |
| | | return; |
| | | } |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | mapCtx.includePoints({ |
| | | points, |
| | | padding: INCLUDE_POINTS_PADDING |
| | | }); |
| | | }; |
| | | |
| | |
| | | mapCtx.getCenterLocation({ |
| | | success: async (res) => { |
| | | const { latitude: lat, longitude: lng } = res; |
| | | const { newMarkers, sites, isInside } = await updateMapData(lat, lng); |
| | | const { newMarkers, sites, isInside, electronicAreaId } = await updateMapData(lat, lng); |
| | | const selectableMarkers = getSelectableMarkers(newMarkers, sites); |
| | | const { targetMarker, minDistance } = findNearestMarker(lat, lng, selectableMarkers); |
| | | if (targetMarker && minDistance < state.distanceConfig.adsorptionDistance) { |
| | |
| | | resetStationSelection(); |
| | | state.setSelectedSite({}); |
| | | state.setRangeStatus( |
| | | isInside && hasCrossOperatingAreaCandidateNearby(lat, lng, newMarkers, sites) |
| | | isInside && !isSameOperatingAreaById(electronicAreaId) |
| | | ? SELECT_SITE_RANGE_STATUS.crossOperatingArea |
| | | : getRangeStatus(isInside, {}) |
| | | : 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) { |
| | |
| | | |
| | | useEffect(() => { |
| | | state.setRangeStatus(getRangeStatus(state.isOperatingArea, state.selectedSite)); |
| | | }, [state.isOperatingArea, state.pageType, state.selectedSite?.electronicAreaId]); |
| | | }, [referenceElectronicAreaId, state.isOperatingArea, state.pageType, state.selectedSite?.electronicAreaId]); |
| | | |
| | | useEffect(() => { |
| | | const systemInfo = Taro.getSystemInfoSync(); |
| | |
| | | state.setLatitude(cachedSite.latitude); |
| | | state.setLongitude(cachedSite.longitude); |
| | | applySelectionBySite(cachedSite); |
| | | updateMapData(cachedSite.latitude, cachedSite.longitude).then(({ isInside }) => { |
| | | state.setRangeStatus(getRangeStatus(isInside, cachedSite)); |
| | | updateMapData(cachedSite.latitude, cachedSite.longitude).then(({ isInside, electronicAreaId }) => { |
| | | state.setRangeStatus(getRangeStatus(isInside, cachedSite, electronicAreaId)); |
| | | }); |
| | | } |
| | | fetchUserLocation(); |
| | |
| | | if (selectedCity && selectedCity.code) { |
| | | getGeofence({ cityCode: selectedCity.code }).then((res: any) => { |
| | | if (res && Array.isArray(res)) { |
| | | const { polygons: newPolygons, fencePolylines: nextFencePolylines } = formatGeofenceByType(res); |
| | | state.stationMetaMapRef.current = buildStationMetaMap(res); |
| | | state.setPolygons(newPolygons); |
| | | state.setFencePolylines(nextFencePolylines); |
| | | if (!hasCachedSite && res.length > 0 && res[0]?.points?.length > 0) { |
| | | const firstPoint = res[0].points[0]; |
| | | const renderableGeofenceList = filterRenderableGeofence(res); |
| | | const stationGeofenceList = renderableGeofenceList.filter((item) => Number(item?.type) === GeofenceType.STATION); |
| | | const { polygons: newPolygons, fencePolylines: nextFencePolylines } = 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) |
| | | }))); |
| | | state.setFencePolylines(nextFencePolylines.map((polyline: any, index: number) => ({ |
| | | ...polyline, |
| | | electronicAreaId: String(stationGeofenceList[index]?.electronicAreaId || ''), |
| | | geofenceType: Number(stationGeofenceList[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 }) => { |
| | | state.setRangeStatus(getRangeStatus(isInside, {})); |
| | | updateMapData(lat, lng).then(({ isInside, electronicAreaId }) => { |
| | | state.setRangeStatus(getRangeStatus(isInside, {}, electronicAreaId)); |
| | | }); |
| | | const mapCtx = Taro.createMapContext('map'); |
| | | const points = extractPointsFromPolygons(newPolygons); |
| | | mapCtx.includePoints({ |
| | | points, |
| | | padding: [100, 100, 100, 100] |
| | | padding: INCLUDE_POINTS_PADDING |
| | | }); |
| | | } else if (!hasCachedSite) { |
| | | getLocationInfo(); |
| | | } |
| | | } else { |
| | | state.geofenceListRef.current = []; |
| | | state.stationMetaMapRef.current = {}; |
| | | state.setPolygons([]); |
| | | state.setFencePolylines([]); |
| | |
| | | } |
| | | }).catch((err) => { |
| | | console.error('获取电子围栏失败', err); |
| | | state.geofenceListRef.current = []; |
| | | state.stationMetaMapRef.current = {}; |
| | | state.setPolygons([]); |
| | | state.setFencePolylines([]); |
| | |
| | | latitude: state.latitude, |
| | | mapScale: state.mapScale, |
| | | markers: state.markers, |
| | | mapPolylines: state.mapPolylines, |
| | | polygons: state.polygons, |
| | | mapPolylines: displayMapPolylines, |
| | | polygons: displayPolygons, |
| | | handleRegionChange, |
| | | handleMarkerTap, |
| | | toBack, |
| | |
| | | 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, |