import React, { useEffect, useMemo, useRef, useState } from 'react'; import { flushSync } from 'react-dom'; import { IcClock, IcClose, IcLocation, IcMic, IcNavBack, } from '../../res/icons'; import { useLocale } from '../../locale'; import { googleLangCode } from '../../utils/placeUtils'; import { useMapBackHandler } from '../../hooks/useMapBackHandler'; import { useMapGestures } from '../../hooks/useMapGestures'; import { useMapStrings } from '../../hooks/useMapStrings'; import { useMapStore, selectCurrentLocation, selectGoogle, type MapSearchHistoryEntry, } from '../../state'; import { formatDistanceLabelMeters } from '../../utils/placeUtils'; import { getAutocompleteOffline } from '../../utils/offlinePlaceStore'; import { hasGoogleMapsApiKey } from '../../utils/googleMapsConfig'; import { KeyboardService } from '@/os/keyboard'; type RouteSide = 'origin' | 'destination'; type PlacePrediction = { placeId: string; mainText: string; secondaryText: string; distanceMeters?: number; }; const AUTOCOMPLETE_DEBOUNCE_MS = 280; export const RoutePointPickerOverlay: React.FC<{ mode: RouteSide; initialQuery?: string; searchHistory: MapSearchHistoryEntry[]; counterpart: { name: string } | null; continuationDuration?: string | null; viewportCenter?: { lat: number; lng: number } | null; onClose: () => void; onChooseOnMap: (side: RouteSide) => void; onSelectPlace: (placeId: string, title: string, subtitle?: string) => void; }> = ({ mode, initialQuery = '', searchHistory, counterpart, continuationDuration, viewportCenter, onClose, onChooseOnMap, onSelectPlace, }) => { const { bindTap } = useMapGestures(); const google = useMapStore(selectGoogle); const currentLocation = useMapStore(selectCurrentLocation); const locale = useLocale(); const s = useMapStrings(); const [query, setQuery] = useState(initialQuery); const [debouncedQuery, setDebouncedQuery] = useState(initialQuery); const [predictions, setPredictions] = useState([]); const [suggestionsLoading, setSuggestionsLoading] = useState(false); const inputRef = useRef(null); useMapBackHandler( () => { onClose(); return true; }, { priority: 700 }, ); useEffect(() => { const t = window.setTimeout(() => setDebouncedQuery(query), AUTOCOMPLETE_DEBOUNCE_MS); return () => window.clearTimeout(t); }, [query]); useEffect(() => { if (!google) return; void google.maps.importLibrary('places'); }, [google]); useEffect(() => { const trimmedQuery = debouncedQuery.trim(); if (!trimmedQuery) { setPredictions([]); setSuggestionsLoading(false); return; } let cancelled = false; setSuggestionsLoading(true); const run = async () => { const offline = await getAutocompleteOffline({ input: trimmedQuery, locale, placesOnly: true, }); if (cancelled) return; if (offline.source === 'autocomplete_index') { console.log(`[Map][离线] 路线选点提示: "${trimmedQuery}" 命中 autocomplete_index -> ${offline.predictions.length} 个地点`); setPredictions( offline.predictions.map((item) => ({ placeId: item.place_id, mainText: item.main_text, secondaryText: item.secondary_text, distanceMeters: item.distance_meters, })), ); setSuggestionsLoading(false); return; } if (!google || !hasGoogleMapsApiKey()) { const fallback = await getAutocompleteOffline({ input: trimmedQuery, locale, placesOnly: true, allowSearchIndexFallback: true, }); if (cancelled) return; if (fallback.predictions.length > 0) { console.log(`[Map][离线未命中] 路线选点提示: "${trimmedQuery}" 没有 autocomplete_index,Google key 不可用,使用 search_index 兜底 -> ${fallback.predictions.length} 个地点`); setPredictions( fallback.predictions.map((item) => ({ placeId: item.place_id, mainText: item.main_text, secondaryText: item.secondary_text, distanceMeters: item.distance_meters, })), ); setSuggestionsLoading(false); return; } console.log(`[Map][离线未命中] 路线选点提示: "${trimmedQuery}" 未命中 autocomplete_index/search_index,Google key 不可用`); setPredictions([]); setSuggestionsLoading(false); return; } console.log(`[Map][在线] 路线选点提示: "${trimmedQuery}" 离线未命中,调用 Google Place Autocomplete`); const service = new google.maps.places.AutocompleteService(); const biasLat = viewportCenter?.lat ?? currentLocation.latitude; const biasLng = viewportCenter?.lng ?? currentLocation.longitude; const locationForBias = typeof biasLat === 'number' && typeof biasLng === 'number' ? new google.maps.LatLng(biasLat, biasLng) : undefined; const placeReq: google.maps.places.AutocompletionRequest = { input: debouncedQuery, language: googleLangCode(locale), ...(locationForBias ? { locationBias: { center: locationForBias, radius: 5000 }, origin: locationForBias } : {}), }; service.getPlacePredictions(placeReq, (results, status) => { if (cancelled) return; if (status !== google.maps.places.PlacesServiceStatus.OK || !results) { console.log(`[Map][离线未命中] 路线选点提示: "${trimmedQuery}" 在线提示失败或无结果 (${status})`); setPredictions([]); setSuggestionsLoading(false); return; } console.log(`[Map][在线] 路线选点提示: "${trimmedQuery}" -> ${results.length} 个地点`); setPredictions( results.map((item) => ({ placeId: item.place_id, mainText: item.structured_formatting?.main_text ?? item.description ?? '', secondaryText: item.structured_formatting?.secondary_text ?? '', distanceMeters: item.distance_meters ?? undefined, })), ); setSuggestionsLoading(false); }); }; void run(); return () => { cancelled = true; }; }, [debouncedQuery, google, currentLocation.latitude, currentLocation.longitude, viewportCenter?.lat, viewportCenter?.lng, locale]); const placeHistory = useMemo( () => searchHistory.filter( (item): item is MapSearchHistoryEntry & { kind: 'place'; placeId: string } => item.kind === 'place' && typeof item.placeId === 'string' && item.placeId.length > 0, ), [searchHistory], ); const placeholder = mode === 'origin' ? s.route_setup_select_start : s.route_setup_select_dest; const handleSelect = (placeId: string, title: string, subtitle?: string) => { KeyboardService.hide(); inputRef.current?.blur(); onSelectPlace(placeId, title, subtitle); }; const showHistory = query.trim().length === 0; return (
setQuery(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === 'NumpadEnter') { e.preventDefault(); if (predictions[0]) { handleSelect( predictions[0].placeId, predictions[0].mainText, predictions[0].secondaryText || undefined, ); } } }} /> {query ? ( ) : ( )}
{showHistory ? ( <> {placeHistory.map((item) => ( ))} ) : (
{suggestionsLoading ? (
{s.search_searching}
) : predictions.length > 0 ? ( predictions.map((item) => ( )) ) : (
{s.search_no_results}
)}
)}
{showHistory && counterpart ? (
{s.route_continue_to}{counterpart.name}
{continuationDuration ? (
{continuationDuration}
) : null}
) : null}
); };