fix: tìm kiếm địa điểm với cách gõ tên trên bản đồ
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||||
import { format, parseISO } from 'date-fns';
|
import { format, parseISO } from 'date-fns';
|
||||||
import { X, MapPin, Loader2, Clock, Map as MapIcon, Navigation, Search } from 'lucide-react';
|
import { X, MapPin, Loader2, Clock, Map as MapIcon, Navigation, Search } from 'lucide-react';
|
||||||
import { useTourStore } from '@/store/useTourStore.js';
|
import { useTourStore } from '@/store/useTourStore';
|
||||||
import { MapContainer, TileLayer, Marker, useMapEvents, useMap } from 'react-leaflet';
|
import { MapContainer, TileLayer, Marker, useMapEvents, useMap } from 'react-leaflet';
|
||||||
import L from 'leaflet';
|
import L from 'leaflet';
|
||||||
|
|
||||||
@@ -79,6 +79,7 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
|
|||||||
});
|
});
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [searchResults, setSearchResults] = useState<any[]>([]);
|
const [searchResults, setSearchResults] = useState<any[]>([]);
|
||||||
|
const [hasNoResults, setHasNoResults] = useState(false);
|
||||||
const [isSearching, setIsSearching] = useState(false);
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
const searchTimeout = useRef<any>(null);
|
const searchTimeout = useRef<any>(null);
|
||||||
|
|
||||||
@@ -121,6 +122,12 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
|
|||||||
}
|
}
|
||||||
}, [initialLegId, editingLocation, isOpen]);
|
}, [initialLegId, editingLocation, isOpen]);
|
||||||
|
|
||||||
|
// Memoize tọa độ để tránh việc bản đồ tự động reset tâm khi re-render (ví dụ khi gõ tìm kiếm)
|
||||||
|
const currentCoords = useMemo<[number, number]>(
|
||||||
|
() => [formData.latitude, formData.longitude],
|
||||||
|
[formData.latitude, formData.longitude]
|
||||||
|
);
|
||||||
|
|
||||||
// Tự động cập nhật vị trí bản đồ theo chặng được chọn (Lấy điểm cuối của chặng đó làm tham chiếu)
|
// Tự động cập nhật vị trí bản đồ theo chặng được chọn (Lấy điểm cuối của chặng đó làm tham chiếu)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen && !editingLocation && formData.legId && !formData.name) {
|
if (isOpen && !editingLocation && formData.legId && !formData.name) {
|
||||||
@@ -155,15 +162,23 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
|
|||||||
if (query.trim().length < 2) {
|
if (query.trim().length < 2) {
|
||||||
setSearchResults([]);
|
setSearchResults([]);
|
||||||
setIsSearching(false);
|
setIsSearching(false);
|
||||||
|
setHasNoResults(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsSearching(true);
|
setIsSearching(true);
|
||||||
|
setHasNoResults(false);
|
||||||
searchTimeout.current = setTimeout(async () => {
|
searchTimeout.current = setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}&limit=8&addressdetails=1&accept-language=vi`);
|
// Loại bỏ countrycodes=vn để tìm kiếm rộng hơn, thêm namedetails=1 để lấy tên chính xác
|
||||||
|
const res = await fetch(`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(query)}&limit=15&addressdetails=1&namedetails=1&accept-language=vi`, {
|
||||||
|
headers: {
|
||||||
|
'Accept-Language': 'vi'
|
||||||
|
}
|
||||||
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setSearchResults(data);
|
setSearchResults(data);
|
||||||
|
setHasNoResults(data.length === 0);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Lỗi tìm kiếm địa điểm:", e);
|
console.error("Lỗi tìm kiếm địa điểm:", e);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -175,9 +190,12 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
|
|||||||
const selectSearchResult = (result: any) => {
|
const selectSearchResult = (result: any) => {
|
||||||
const lat = parseFloat(result.lat);
|
const lat = parseFloat(result.lat);
|
||||||
const lon = parseFloat(result.lon);
|
const lon = parseFloat(result.lon);
|
||||||
|
// Ưu tiên lấy tên từ namedetails nếu có, nếu không lấy phần đầu của display_name
|
||||||
|
const locationName = result.namedetails?.name || result.display_name.split(',')[0];
|
||||||
|
|
||||||
setFormData(prev => ({
|
setFormData(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
name: result.display_name.split(',')[0],
|
name: locationName,
|
||||||
address: result.display_name,
|
address: result.display_name,
|
||||||
latitude: lat,
|
latitude: lat,
|
||||||
longitude: lon
|
longitude: lon
|
||||||
@@ -297,10 +315,10 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
|
|||||||
|
|
||||||
{/* Mini Map Picker */}
|
{/* Mini Map Picker */}
|
||||||
<div className="h-48 w-full rounded-2xl overflow-hidden mb-6 border border-gray-100 relative shadow-inner group">
|
<div className="h-48 w-full rounded-2xl overflow-hidden mb-6 border border-gray-100 relative shadow-inner group">
|
||||||
<MapContainer center={[formData.latitude, formData.longitude]} zoom={13} className="h-full w-full">
|
<MapContainer center={currentCoords} zoom={13} className="h-full w-full">
|
||||||
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
|
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
|
||||||
<Marker position={[formData.latitude, formData.longitude]} />
|
<Marker position={currentCoords} />
|
||||||
<MapPicker center={[formData.latitude, formData.longitude]} onPick={handlePickLocation} />
|
<MapPicker center={currentCoords} onPick={handlePickLocation} />
|
||||||
</MapContainer>
|
</MapContainer>
|
||||||
<div className="absolute bottom-2 left-2 z-[1000] bg-white/90 backdrop-blur-sm px-2 py-1 rounded-lg text-[10px] font-black text-gray-500 shadow-sm border border-gray-100">
|
<div className="absolute bottom-2 left-2 z-[1000] bg-white/90 backdrop-blur-sm px-2 py-1 rounded-lg text-[10px] font-black text-gray-500 shadow-sm border border-gray-100">
|
||||||
CHUỘT PHẢI ĐỂ CHỌN VỊ TRÍ
|
CHUỘT PHẢI ĐỂ CHỌN VỊ TRÍ
|
||||||
@@ -318,7 +336,7 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4" onClick={() => setSearchResults([])}>
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<label className="block text-sm font-bold text-gray-700 mb-1">Tên địa điểm</label>
|
<label className="block text-sm font-bold text-gray-700 mb-1">Tên địa điểm</label>
|
||||||
<div className="relative group">
|
<div className="relative group">
|
||||||
@@ -333,7 +351,7 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
|
|||||||
{isSearching ? (
|
{isSearching ? (
|
||||||
<Loader2 className="w-4 h-4 animate-spin text-blue-500" />
|
<Loader2 className="w-4 h-4 animate-spin text-blue-500" />
|
||||||
) : formData.name ? (
|
) : formData.name ? (
|
||||||
<button type="button" onClick={() => { setFormData({...formData, name: ''}); setSearchResults([]); }} className="hover:text-red-500 transition-colors">
|
<button type="button" onClick={() => { setFormData({...formData, name: ''}); setSearchResults([]); setHasNoResults(false); }} className="hover:text-red-500 transition-colors">
|
||||||
<X className="w-4 h-4 text-gray-400" />
|
<X className="w-4 h-4 text-gray-400" />
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
@@ -342,19 +360,32 @@ export const AddLocationModal = ({ isOpen, onClose, tourId, initialLegId, editin
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{searchResults.length > 0 && (
|
{(searchResults.length > 0 || hasNoResults) && (
|
||||||
<div className="absolute z-[3100] left-0 right-0 mt-2 bg-white border border-gray-100 rounded-2xl shadow-2xl overflow-hidden max-h-64 overflow-y-auto animate-in slide-in-from-top-2 duration-200">
|
<div className="absolute z-[5000] left-0 right-0 mt-2 bg-white border border-gray-200 rounded-2xl shadow-2xl overflow-hidden max-h-64 overflow-y-auto animate-in slide-in-from-top-2 duration-200">
|
||||||
{searchResults.map((result, idx) => (
|
{hasNoResults ? (
|
||||||
<button
|
<div className="px-4 py-6 text-center text-gray-400 text-sm italic">
|
||||||
key={idx}
|
Không tìm thấy địa điểm nào phù hợp...
|
||||||
type="button"
|
</div>
|
||||||
onClick={(e) => { e.preventDefault(); e.stopPropagation(); selectSearchResult(result); }}
|
) : (
|
||||||
className="w-full text-left px-4 py-3 hover:bg-blue-50 border-b border-gray-50 last:border-0 transition-all flex flex-col gap-0.5"
|
searchResults.map((result, idx) => (
|
||||||
>
|
<button
|
||||||
<div className="font-bold text-sm text-gray-900 line-clamp-1">{result.display_name.split(',')[0]}</div>
|
key={idx}
|
||||||
<div className="text-[10px] text-gray-500 line-clamp-2 leading-tight">{result.display_name}</div>
|
type="button"
|
||||||
</button>
|
onClick={(e) => { e.preventDefault(); e.stopPropagation(); selectSearchResult(result); }}
|
||||||
))}
|
className="w-full text-left px-4 py-3 hover:bg-blue-50 border-b border-gray-50 last:border-0 transition-all flex flex-col gap-0.5"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<div className="font-bold text-sm text-gray-900 line-clamp-1">
|
||||||
|
{result.namedetails?.name || result.display_name.split(',')[0]}
|
||||||
|
</div>
|
||||||
|
{result.type && (
|
||||||
|
<span className="text-[9px] font-black uppercase text-blue-400 bg-blue-50 px-1.5 py-0.5 rounded border border-blue-100 shrink-0">{result.type}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-[10px] text-gray-500 line-clamp-2 leading-tight">{result.display_name}</div>
|
||||||
|
</button>
|
||||||
|
))
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user