fix: Ngăn bản đồ tự động reset về trung tâm khi người dùng đã tương tác

- TourDetailPage: MapTourBounds chỉ fitBounds 1 lần cho mỗi lần dữ liệu lộ trình thay đổi thực sự (theo locKey). Nếu người dùng đã pan/zoom rồi thì không refit khi data reload.
- TourNavigationPage: FitBounds chỉ chạy khi người dùng CHƯA tương tác (hasInteractedRef). Sau khi người dùng pan/zoom/drag, bản đồ giữ nguyên vị trí.
- Cả hai bản đồ đều tôn trọng vị trí người dùng đã chọn, không reset về mặc định khi có thay đổi dữ liệu phụ.
This commit is contained in:
2026-06-25 12:28:37 +07:00
parent 90475d4130
commit 33a5996bee
2 changed files with 38 additions and 25 deletions
+20 -11
View File
@@ -148,20 +148,29 @@ const combineSegmentRoutes = (segmentRoutes: OSRMRoute[][], selectedIndices: num
// Component Helper để tự động điều chỉnh khung nhìn bản đồ bao phủ toàn bộ lộ trình // Component Helper để tự động điều chỉnh khung nhìn bản đồ bao phủ toàn bộ lộ trình
const MapTourBounds = ({ locations }: { locations: any[] }) => { const MapTourBounds = ({ locations }: { locations: any[] }) => {
const map = useMap(); const map = useMap();
// Tạo một key dựa trên giá trị tọa độ để tránh chạy lại khi chỉ thay đổi tham chiếu mảng const shouldFitRef = useRef(true);
const locKey = useMemo(() => JSON.stringify(locations.map(l => [l.latitude, l.longitude])), [locations]); const prevLocKeyRef = useRef<string | null>(null);
const locKey = useMemo(() => JSON.stringify(locations.map((l: any) => [l.latitude, l.longitude])), [locations]);
useEffect(() => { useEffect(() => {
if (locations.length > 0) { if (locations.length === 0) return;
const bounds = L.latLngBounds(locations.map(l => [l.latitude, l.longitude]));
if (locations.length === 1) { if (prevLocKeyRef.current !== null && prevLocKeyRef.current !== locKey) {
// Chỉ thực hiện nếu bản đồ chưa ở đúng vị trí (tránh trigger moveend liên tục) shouldFitRef.current = true;
map.setView([locations[0].latitude, locations[0].longitude], 15, { animate: true });
} else {
map.fitBounds(bounds, { padding: [50, 50], maxZoom: 15 });
}
} }
}, [locKey, map]); prevLocKeyRef.current = locKey;
if (!shouldFitRef.current) return;
const bounds = L.latLngBounds(locations.map((l: any) => [l.latitude, l.longitude]));
if (locations.length === 1) {
map.setView([locations[0].latitude, locations[0].longitude], 15, { animate: true });
} else {
map.fitBounds(bounds, { padding: [50, 50], maxZoom: 15 });
}
shouldFitRef.current = false;
}, [locKey, map, locations.length]);
return null; return null;
}; };
+18 -14
View File
@@ -16,14 +16,13 @@ interface TourNavigationPageProps {
onBack: () => void; onBack: () => void;
} }
const FitBounds = ({ coords, canFitRef }: { coords: [number, number][], canFitRef: React.MutableRefObject<boolean> }) => { const FitBounds = ({ coords, destination, hasInteractedRef }: { coords: [number, number][], destination: { lat: number; lng: number; name: string } | null, hasInteractedRef: React.MutableRefObject<boolean> }) => {
const map = useMap(); const map = useMap();
useEffect(() => { useEffect(() => {
if (coords.length > 0 && canFitRef.current) { if (coords.length > 0 && !hasInteractedRef.current && destination) {
canFitRef.current = false;
map.fitBounds(coords, { padding: [60, 60], maxZoom: 15 }); map.fitBounds(coords, { padding: [60, 60], maxZoom: 15 });
} }
}, [map, coords, canFitRef]); }, [map, coords, destination, hasInteractedRef]);
return null; return null;
}; };
@@ -58,14 +57,16 @@ const MapRotationHandler = ({ rotation }: { rotation: number }) => {
}; };
const CompassInteractionDetector = ({ const CompassInteractionDetector = ({
onUserInteraction onUserInteraction,
hasInteractedRef
}: { }: {
onUserInteraction: () => void; onUserInteraction: () => void;
hasInteractedRef: React.MutableRefObject<boolean>;
}) => { }) => {
useMapEvents({ useMapEvents({
movestart: onUserInteraction, movestart: () => { hasInteractedRef.current = true; },
zoomstart: onUserInteraction, zoomstart: () => { hasInteractedRef.current = true; },
dragstart: onUserInteraction, dragstart: () => { hasInteractedRef.current = true; },
}); });
return null; return null;
}; };
@@ -78,14 +79,16 @@ const MapRefSetter = ({ mapRef }: { mapRef: React.MutableRefObject<L.Map | null>
return null; return null;
}; };
const MapInteractionWatcher = () => { const MapInteractionWatcher = ({ hasInteractedRef }: { hasInteractedRef: React.MutableRefObject<boolean> }) => {
const map = useMap(); const map = useMap();
useEffect(() => { useEffect(() => {
const onZoomEnd = () => { const onZoomEnd = () => {
map._userZoomLevel = map.getZoom(); map._userZoomLevel = map.getZoom();
hasInteractedRef.current = true;
}; };
const onMoveEnd = () => { const onMoveEnd = () => {
map._userCenter = map.getCenter(); map._userCenter = map.getCenter();
hasInteractedRef.current = true;
}; };
map.on('zoomend', onZoomEnd); map.on('zoomend', onZoomEnd);
map.on('moveend', onMoveEnd); map.on('moveend', onMoveEnd);
@@ -93,7 +96,7 @@ const MapInteractionWatcher = () => {
map.off('zoomend', onZoomEnd); map.off('zoomend', onZoomEnd);
map.off('moveend', onMoveEnd); map.off('moveend', onMoveEnd);
}; };
}, [map]); }, [map, hasInteractedRef]);
return null; return null;
}; };
@@ -141,7 +144,7 @@ export const TourNavigationPage: React.FC<TourNavigationPageProps> = ({ tourId,
const mapRef = useRef<L.Map | null>(null); const mapRef = useRef<L.Map | null>(null);
const watchIdRef = useRef<number | null>(null); const watchIdRef = useRef<number | null>(null);
const fetchLockRef = useRef(false); const fetchLockRef = useRef(false);
const canFitRef = useRef(true); const hasInteractedRef = useRef(false);
const originLat = routeData?.origin?.lat; const originLat = routeData?.origin?.lat;
const originLng = routeData?.origin?.lng; const originLng = routeData?.origin?.lng;
@@ -226,6 +229,7 @@ export const TourNavigationPage: React.FC<TourNavigationPageProps> = ({ tourId,
}, [routeData, originLat, originLng, destLat, destLng]); }, [routeData, originLat, originLng, destLat, destLng]);
const handleUserInteraction = useCallback(() => { const handleUserInteraction = useCallback(() => {
hasInteractedRef.current = true;
if (isCompassActive) { if (isCompassActive) {
setIsCompassActive(false); setIsCompassActive(false);
} }
@@ -346,14 +350,14 @@ export const TourNavigationPage: React.FC<TourNavigationPageProps> = ({ tourId,
> >
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" attribution="" /> <TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" attribution="" />
<MapRefSetter mapRef={mapRef} /> <MapRefSetter mapRef={mapRef} />
<MapInteractionWatcher /> <MapInteractionWatcher hasInteractedRef={hasInteractedRef} />
{/* <FitBounds coords={routeGeometry || []} canFitRef={canFitRef} /> */} <FitBounds coords={routeGeometry || []} destination={routeData?.destination || null} hasInteractedRef={hasInteractedRef} />
{routeGeometry && ( {routeGeometry && (
<Polyline positions={routeGeometry} color="#2563eb" weight={6} opacity={0.9} smoothFactor={1} /> <Polyline positions={routeGeometry} color="#2563eb" weight={6} opacity={0.9} smoothFactor={1} />
)} )}
<MapSizeHandler mapRef={mapRef} /> <MapSizeHandler mapRef={mapRef} />
<MapRotationHandler rotation={currentHeading} /> <MapRotationHandler rotation={currentHeading} />
<CompassInteractionDetector onUserInteraction={handleUserInteraction} /> <CompassInteractionDetector onUserInteraction={handleUserInteraction} hasInteractedRef={hasInteractedRef} />
</MapContainer> </MapContainer>
{/* Locate User Button */} {/* Locate User Button */}