From 3a3296c340c4f41801284ad55953c2dde7d556be Mon Sep 17 00:00:00 2001 From: 3dtours Date: Wed, 17 Jun 2026 18:27:52 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20th=C3=AAm=20t=C3=ADnh=20n=C4=83ng=20hi?= =?UTF-8?q?=E1=BB=83n=20th=E1=BB=8B=20v=E1=BB=8B=20tr=C3=AD=20ng=C6=B0?= =?UTF-8?q?=E1=BB=9Di=20d=C3=B9ng=20tr=C3=AAn=20b=E1=BA=A3n=20=C4=91?= =?UTF-8?q?=E1=BB=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/TourDetailPage.tsx | 98 +++++++++++++++++++++++---- 1 file changed, 86 insertions(+), 12 deletions(-) diff --git a/frontend/src/pages/TourDetailPage.tsx b/frontend/src/pages/TourDetailPage.tsx index d3bf435..2b9bc1d 100644 --- a/frontend/src/pages/TourDetailPage.tsx +++ b/frontend/src/pages/TourDetailPage.tsx @@ -30,6 +30,7 @@ import { Loader2, Car, Bike, + Navigation, Footprints, Flag, Clock, @@ -182,6 +183,7 @@ export const TourDetailPage = ({ const publicTours = useTourStore(state => state.publicTours); const userRole = useTourStore(state => state.userRole); const mapCenter = useTourStore(state => state.mapCenter); + const [userLocation, setUserLocation] = useState<[number, number] | null>(null); // Định nghĩa Icons bên trong Component bằng useMemo để đảm bảo tính ổn định và tránh lỗi render const mapIcons = useMemo(() => ({ @@ -202,9 +204,31 @@ export const TourDetailPage = ({ html: `
`, iconSize: [20, 20], iconAnchor: [10, 10] + }), + user: L.divIcon({ + className: '!bg-transparent !border-none', + html: ` +
+
+
+
+ `, + iconSize: [16, 16], + iconAnchor: [8, 8] }) }), []); + useEffect(() => { + if (!isPublicView && navigator.geolocation) { + const watchId = navigator.geolocation.watchPosition( + (pos) => setUserLocation([pos.coords.latitude, pos.coords.longitude]), + (err) => console.warn("Lỗi định vị người dùng:", err), + { enableHighAccuracy: true } + ); + return () => navigator.geolocation.clearWatch(watchId); + } + }, [isPublicView]); + const [activeTab, setActiveTab] = useState<'plan' | 'expense' | 'photo' | 'settings'>('plan'); const [viewMode, setViewMode] = useState<'map' | 'timeline'>('timeline'); const [isAddLocationOpen, setIsAddLocationOpen] = useState(false); @@ -451,8 +475,8 @@ export const TourDetailPage = ({ setIsRoutingLoading(true); try { - // Thêm alternatives=3 để yêu cầu tối đa 3 phương án lộ trình khác từ OSRM - const response = await fetch(`https://router.project-osrm.org/route/v1/${travelMode}/${coordsString}?overview=full&geometries=geojson&alternatives=3`); + // Thêm alternatives=true để yêu cầu các phương án lộ trình khác từ OSRM (Lưu ý: OSRM thường chỉ trả về lộ trình thay thế cho 2 điểm tọa độ) + const response = await fetch(`https://router.project-osrm.org/route/v1/${travelMode}/${coordsString}?overview=full&geometries=geojson&alternatives=true`); const data = await response.json(); if (data.code === 'Ok' && data.routes.length > 0) { setRoutes(data.routes); @@ -736,6 +760,19 @@ export const TourDetailPage = ({ } }; + // Ngăn chặn sự kiện click trên menu lộ trình làm ảnh hưởng bản đồ + const routeMenuRef = React.useRef(null); + useEffect(() => { + if (routeMenu && routeMenuRef.current) { + L.DomEvent.disableClickPropagation(routeMenuRef.current); + } + }, [routeMenu]); + + // Đóng menu lộ trình khi chuyển tab hoặc thay đổi chế độ xem + useEffect(() => { + setRouteMenu(null); + }, [activeTab, viewMode]); + // Xác định Điểm xuất phát và Điểm kết thúc hiển thị dưới widget tài chính const startPoint = useMemo(() => legs.flatMap(l => l.locations).find(loc => loc.plannedStart && new Date(loc.plannedStart).getTime() === 0), @@ -1155,11 +1192,25 @@ export const TourDetailPage = ({ preferCanvas={true} > - {canEdit && !isPublicView && setRouteMenu(null)} />} {/* Hide map context menu in public view */} + {canEdit && !isPublicView && ( + setRouteMenu(null)} + /> + )} {/* Tự động đóng khung Start và End khi dữ liệu thay đổi */} + {/* Hiển thị vị trí hiện tại của người dùng */} + {userLocation && ( + + +
Bạn đang ở đây
+
+
+ )} + {/* Vẽ tất cả lộ trình: Vẽ các đường phụ trước, đường chính sau để hiển thị đè lên trên */} {routes.length > 0 ? ( [...routes] @@ -1171,23 +1222,40 @@ export const TourDetailPage = ({ }) .map(({ data, index }) => ( [c[1], c[0]])} color={index === selectedRouteIndex ? "#2563eb" : "#94a3b8"} - weight={index === selectedRouteIndex ? 6 : 4} - opacity={index === selectedRouteIndex ? 0.9 : 0.4} - dashArray={index === selectedRouteIndex ? undefined : "10, 10"} + weight={index === selectedRouteIndex ? 6 : 14} + opacity={index === selectedRouteIndex ? 1 : 0.4} + dashArray={index === selectedRouteIndex ? undefined : "15, 15"} smoothFactor={1} eventHandlers={{ click: (e) => { - L.DomEvent.stopPropagation(e as any); + const originalEvent = (e as any).originalEvent; + if (originalEvent) L.DomEvent.stopPropagation(originalEvent); setRouteMenu(null); setSelectedRouteIndex(index); }, contextmenu: (e) => { - L.DomEvent.stopPropagation(e as any); - L.DomEvent.preventDefault(e as any); + const originalEvent = (e as any).originalEvent; + if (originalEvent) { + L.DomEvent.stopPropagation(originalEvent); + L.DomEvent.preventDefault(originalEvent); + // Đánh dấu để MapContextMenu biết đã có Layer xử lý + (originalEvent as any)._routeTriggered = true; + } + setRouteMenu({ x: (e as any).containerPoint.x, y: (e as any).containerPoint.y, index }); + }, + mouseover: (e) => { + if (index !== selectedRouteIndex) { + (e.target as L.Polyline).setStyle({ opacity: 0.8, weight: 16, color: '#64748b' }); + } + }, + mouseout: (e) => { + if (index !== selectedRouteIndex) { + (e.target as L.Polyline).setStyle({ opacity: 0.4, weight: 14, color: '#94a3b8' }); + } } }} /> @@ -1272,6 +1340,7 @@ export const TourDetailPage = ({ {/* Menu ngữ cảnh khi click chuột phải vào con đường */} {routeMenu && (
e.stopPropagation()} @@ -1328,9 +1397,14 @@ export const TourDetailPage = ({ )} {/* Danh sách lựa chọn lộ trình đề xuất */} - {routes.length > 1 && ( -
+ {routes.length > 0 && ( +
Lộ trình đề xuất
+ {routes.length === 1 && allLocations.length > 2 && ( +
+ Lưu ý: Dịch vụ bản đồ chỉ gợi ý đường đi khác khi lộ trình có đúng 2 điểm (Điểm đầu & Điểm cuối). +
+ )}
{routes.map((route, idx) => (