fix: bản đồ xoay mượt trên Iphone
This commit is contained in:
@@ -222,7 +222,7 @@ const MapRotationHandler = ({ rotation }: { rotation: number }) => {
|
|||||||
// 1. Dùng translateZ(0) để kích hoạt GPU trên iOS.
|
// 1. Dùng translateZ(0) để kích hoạt GPU trên iOS.
|
||||||
// 2. Giảm scale xuống ~1.6 (vừa đủ che góc) để giảm tải cho bộ nhớ đệm đồ họa.
|
// 2. Giảm scale xuống ~1.6 (vừa đủ che góc) để giảm tải cho bộ nhớ đệm đồ họa.
|
||||||
container.style.transform = `rotate(${cumulativeRotationRef.current}deg) scale(2.2) translateZ(0)`;
|
container.style.transform = `rotate(${cumulativeRotationRef.current}deg) scale(2.2) translateZ(0)`;
|
||||||
container.style.transition = 'transform 0.2s cubic-bezier(0.25, 0.1, 0.25, 1)';
|
container.style.transition = 'transform 0.15s ease-out';
|
||||||
}, [rotation, map]);
|
}, [rotation, map]);
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
@@ -867,6 +867,8 @@ export const TourDetailPage = ({
|
|||||||
|
|
||||||
// Theo dõi hướng thiết bị (la bàn)
|
// Theo dõi hướng thiết bị (la bàn)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
let lastHeading: number | null = null;
|
||||||
|
|
||||||
const handleOrientation = (event: any) => {
|
const handleOrientation = (event: any) => {
|
||||||
let heading: number | null = null;
|
let heading: number | null = null;
|
||||||
|
|
||||||
@@ -876,17 +878,17 @@ export const TourDetailPage = ({
|
|||||||
}
|
}
|
||||||
// 2. Đối với Android (Chrome): Cần kiểm tra tính tuyệt đối của dữ liệu
|
// 2. Đối với Android (Chrome): Cần kiểm tra tính tuyệt đối của dữ liệu
|
||||||
else if (event.alpha !== null && event.alpha !== undefined) {
|
else if (event.alpha !== null && event.alpha !== undefined) {
|
||||||
// Chrome trên Android chỉ cung cấp hướng la bàn chuẩn khi event.absolute là true
|
|
||||||
// hoặc khi nhận từ sự kiện 'deviceorientationabsolute'
|
|
||||||
if (event.absolute === true || event.type === 'deviceorientationabsolute') {
|
if (event.absolute === true || event.type === 'deviceorientationabsolute') {
|
||||||
// Alpha trên Android tăng theo chiều ngược kim đồng hồ (0=North, 90=West)
|
|
||||||
// Cần chuyển đổi sang chiều kim đồng hồ để khớp với logic quay bản đồ
|
|
||||||
heading = (360 - event.alpha) % 360;
|
heading = (360 - event.alpha) % 360;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (heading !== null) {
|
if (heading !== null) {
|
||||||
setDeviceOrientationHeading(heading);
|
// Chỉ cập nhật nếu hướng thay đổi lớn hơn 2 độ để giảm tải vẽ lại
|
||||||
|
if (lastHeading === null || Math.abs(heading - lastHeading) > 2) {
|
||||||
|
lastHeading = heading;
|
||||||
|
setDeviceOrientationHeading(heading);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1242,6 +1244,31 @@ export const TourDetailPage = ({
|
|||||||
const rejectJoinRequest = useTourStore(state => state.rejectJoinRequest);
|
const rejectJoinRequest = useTourStore(state => state.rejectJoinRequest);
|
||||||
const deleteTour = useTourStore(state => state.deleteTour);
|
const deleteTour = useTourStore(state => state.deleteTour);
|
||||||
|
|
||||||
|
const toggleCompassMode = async () => {
|
||||||
|
if (!isHeadingMode) {
|
||||||
|
if (
|
||||||
|
typeof DeviceOrientationEvent !== 'undefined' &&
|
||||||
|
typeof (DeviceOrientationEvent as any).requestPermission === 'function'
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const permissionState = await (DeviceOrientationEvent as any).requestPermission();
|
||||||
|
if (permissionState === 'granted') {
|
||||||
|
setIsHeadingMode(true);
|
||||||
|
} else {
|
||||||
|
alert("Để xoay bản đồ theo hướng di chuyển, vui lòng cấp quyền truy cập cảm biến hướng (La bàn).");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error requesting compass permission:", error);
|
||||||
|
setIsHeadingMode(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setIsHeadingMode(true);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setIsHeadingMode(false);
|
||||||
|
setMapRotation(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Khôi phục vị trí và mức zoom từ localStorage
|
// Khôi phục vị trí và mức zoom từ localStorage
|
||||||
const [initialViewState] = useState(() => {
|
const [initialViewState] = useState(() => {
|
||||||
@@ -1905,7 +1932,9 @@ export const TourDetailPage = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Tour Header Info */}
|
{!(activeTab === 'plan' && viewMode === 'map') && (
|
||||||
|
<>
|
||||||
|
{/* Tour Header Info */}
|
||||||
<div className="relative min-h-[480px] w-full bg-blue-900 flex flex-col justify-end">
|
<div className="relative min-h-[480px] w-full bg-blue-900 flex flex-col justify-end">
|
||||||
<img
|
<img
|
||||||
src={tourInfo.coverImage}
|
src={tourInfo.coverImage}
|
||||||
@@ -2168,11 +2197,14 @@ export const TourDetailPage = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Main Content Area - Điều chỉnh max-width khi ở chế độ bản đồ */}
|
{/* Main Content Area - Điều chỉnh max-width khi ở chế độ bản đồ */}
|
||||||
<div className={`${activeTab === 'plan' && viewMode === 'map' ? 'w-full' : 'max-w-2xl mx-auto'} px-4 pb-24`}>
|
<div className={`${activeTab === 'plan' && viewMode === 'map' ? 'w-full !px-0 !pb-0' : 'max-w-2xl mx-auto px-4 pb-24'}`}>
|
||||||
{/* Tab Switcher */}
|
{/* Tab Switcher */}
|
||||||
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 p-1 flex mb-6 sticky top-[60px] z-40">
|
{!(activeTab === 'plan' && viewMode === 'map') && (
|
||||||
|
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 p-1 flex mb-6 sticky top-[60px] z-40">
|
||||||
{tabs.map((tab) => (
|
{tabs.map((tab) => (
|
||||||
<button
|
<button
|
||||||
key={tab.id}
|
key={tab.id}
|
||||||
@@ -2196,44 +2228,49 @@ export const TourDetailPage = ({
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Tab Panels */}
|
{/* Tab Panels */}
|
||||||
<div className="transition-opacity duration-300">
|
<div className="transition-opacity duration-300">
|
||||||
{activeTab === 'plan' && (
|
{activeTab === 'plan' && (
|
||||||
<div className="animate-in fade-in slide-in-from-bottom-2">
|
<div className="animate-in fade-in slide-in-from-bottom-2">
|
||||||
{/* View Mode Toggle */}
|
{/* View Mode Toggle */}
|
||||||
<div className="flex justify-center items-center mb-3">
|
{viewMode !== 'map' && (
|
||||||
<div className="bg-gray-100 dark:bg-slate-800 p-1 rounded-2xl flex gap-1">
|
<div className="flex justify-center items-center mb-3">
|
||||||
<button
|
<div className="bg-gray-100 dark:bg-slate-800 p-1 rounded-2xl flex gap-1">
|
||||||
onClick={() => setViewMode('timeline')}
|
<button
|
||||||
className={`flex items-center gap-2 px-4 py-2 rounded-xl text-xs font-bold transition-all ${viewMode === 'timeline' ? 'bg-white dark:bg-slate-900 shadow-sm text-blue-600 dark:text-blue-400' : 'text-gray-500'}`}
|
onClick={() => setViewMode('timeline')}
|
||||||
>
|
className={`flex items-center gap-2 px-4 py-2 rounded-xl text-xs font-bold transition-all ${viewMode === 'timeline' ? 'bg-white dark:bg-slate-900 shadow-sm text-blue-600 dark:text-blue-400' : 'text-gray-500'}`}
|
||||||
<List className="w-3.5 h-3.5" /> Danh sách
|
>
|
||||||
</button>
|
<List className="w-3.5 h-3.5" /> Danh sách
|
||||||
<button
|
</button>
|
||||||
onClick={() => setViewMode('map')}
|
<button
|
||||||
className={`flex items-center gap-2 px-4 py-2 rounded-xl text-xs font-bold transition-all ${viewMode === 'map' ? 'bg-white dark:bg-slate-900 shadow-sm text-blue-600 dark:text-blue-400' : 'text-gray-500'}`}
|
onClick={() => setViewMode('map')}
|
||||||
>
|
className={`flex items-center gap-2 px-4 py-2 rounded-xl text-xs font-bold transition-all ${viewMode === 'map' ? 'bg-white dark:bg-slate-900 shadow-sm text-blue-600 dark:text-blue-400' : 'text-gray-500'}`}
|
||||||
<MapIconLucide className="w-3.5 h-3.5" /> Bản đồ
|
>
|
||||||
</button>
|
<MapIconLucide className="w-3.5 h-3.5" /> Bản đồ
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{/* Export Buttons */}
|
{/* Export Buttons */}
|
||||||
<div className="flex justify-center gap-2 mb-6">
|
{viewMode !== 'map' && (
|
||||||
<button
|
<div className="flex justify-center gap-2 mb-6">
|
||||||
onClick={handleExportCSV}
|
<button
|
||||||
className="flex items-center gap-1 px-3 py-1.5 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg text-[11px] font-bold shadow-md transition-all active:scale-95 whitespace-nowrap"
|
onClick={handleExportCSV}
|
||||||
>
|
className="flex items-center gap-1 px-3 py-1.5 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg text-[11px] font-bold shadow-md transition-all active:scale-95 whitespace-nowrap"
|
||||||
📊 Google Sheets
|
>
|
||||||
</button>
|
📊 Google Sheets
|
||||||
<button
|
</button>
|
||||||
onClick={handleExportPDF}
|
<button
|
||||||
className="flex items-center gap-1 px-3 py-1.5 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-[11px] font-bold shadow-md transition-all active:scale-95 whitespace-nowrap"
|
onClick={handleExportPDF}
|
||||||
>
|
className="flex items-center gap-1 px-3 py-1.5 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-[11px] font-bold shadow-md transition-all active:scale-95 whitespace-nowrap"
|
||||||
📥 {t('exportPDF') || 'Xuất PDF'}
|
>
|
||||||
</button>
|
📥 {t('exportPDF') || 'Xuất PDF'}
|
||||||
</div>
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{viewMode === 'timeline' ? (
|
{viewMode === 'timeline' ? (
|
||||||
<ItineraryTimeline onAddLocation={(legId, isStart, isEnd) => {
|
<ItineraryTimeline onAddLocation={(legId, isStart, isEnd) => {
|
||||||
@@ -2259,7 +2296,7 @@ export const TourDetailPage = ({
|
|||||||
onOpenNavigationPage={onOpenNavigationPage}
|
onOpenNavigationPage={onOpenNavigationPage}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-[60vh] w-full rounded-3xl overflow-hidden shadow-xl border-4 border-white relative animate-in fade-in duration-500">
|
<div className="h-[calc(100vh-53px)] w-full md:rounded-3xl overflow-hidden md:shadow-xl md:border-4 md:border-white relative animate-in fade-in duration-500">
|
||||||
{/* Search Bar Overlay - Thanh tìm kiếm địa điểm trên bản đồ lộ trình */}
|
{/* Search Bar Overlay - Thanh tìm kiếm địa điểm trên bản đồ lộ trình */}
|
||||||
{!isPublicView && (
|
{!isPublicView && (
|
||||||
<div className="absolute top-3 right-3 z-[1001] w-48 md:w-64">
|
<div className="absolute top-3 right-3 z-[1001] w-48 md:w-64">
|
||||||
@@ -2485,6 +2522,19 @@ export const TourDetailPage = ({
|
|||||||
</MarkerClusterGroup>
|
</MarkerClusterGroup>
|
||||||
</MapContainer>
|
</MapContainer>
|
||||||
|
|
||||||
|
{/* Floating Compass Button */}
|
||||||
|
<button
|
||||||
|
onClick={toggleCompassMode}
|
||||||
|
className={`absolute bottom-16 right-4 z-[1001] w-10 h-10 rounded-xl border flex items-center justify-center shadow-xl transition-all active:scale-95 ${
|
||||||
|
isHeadingMode
|
||||||
|
? 'bg-blue-600 border-blue-400 text-white shadow-md'
|
||||||
|
: 'bg-white/90 backdrop-blur-md border-white text-gray-500 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
title={isHeadingMode ? "Khóa hướng Bắc" : "Xoay theo hướng nhìn"}
|
||||||
|
>
|
||||||
|
<Compass className="w-5 h-5 transition-transform duration-300" style={{ transform: `rotate(${mapRotation}deg)` }} />
|
||||||
|
</button>
|
||||||
|
|
||||||
{/* Floating Locate User Button */}
|
{/* Floating Locate User Button */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setLocateTrigger(prev => prev + 1)}
|
onClick={() => setLocateTrigger(prev => prev + 1)}
|
||||||
@@ -2494,6 +2544,15 @@ export const TourDetailPage = ({
|
|||||||
>
|
>
|
||||||
<LocateFixed className="w-5 h-5" />
|
<LocateFixed className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Floating Back to Timeline Button */}
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode('timeline')}
|
||||||
|
className="absolute top-3 left-14 z-[1001] h-9 px-3 bg-white/90 backdrop-blur-md rounded-xl shadow-xl border border-white text-gray-700 hover:bg-gray-50 hover:text-blue-600 transition-all active:scale-95 flex items-center gap-1.5 text-xs font-bold"
|
||||||
|
title="Quay lại danh sách chặng"
|
||||||
|
>
|
||||||
|
<List className="w-4 h-4" /> Danh sách
|
||||||
|
</button>
|
||||||
|
|
||||||
{/* Menu ngữ cảnh khi click chuột phải vào con đường */}
|
{/* Menu ngữ cảnh khi click chuột phải vào con đường */}
|
||||||
{routeMenu && (
|
{routeMenu && (
|
||||||
@@ -3194,7 +3253,7 @@ export const TourDetailPage = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Floating Action Button (Mobile) */}
|
{/* Floating Action Button (Mobile) */}
|
||||||
{((activeTab === 'plan' && canEdit) || (activeTab === 'photo' && canUploadPhoto)) && !isPublicView && (
|
{((activeTab === 'plan' && viewMode === 'timeline' && canEdit) || (activeTab === 'photo' && canUploadPhoto)) && !isPublicView && (
|
||||||
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-40">
|
<div className="fixed bottom-6 left-1/2 -translate-x-1/2 z-40">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -3281,6 +3340,19 @@ export const TourDetailPage = ({
|
|||||||
})}
|
})}
|
||||||
</MapContainer>
|
</MapContainer>
|
||||||
|
|
||||||
|
{/* Floating Compass Button (Fullscreen) */}
|
||||||
|
<button
|
||||||
|
onClick={toggleCompassMode}
|
||||||
|
className={`absolute bottom-22 right-6 z-[1001] w-12 h-12 rounded-2xl border flex items-center justify-center shadow-xl transition-all active:scale-95 ${
|
||||||
|
isHeadingMode
|
||||||
|
? 'bg-blue-600 border-blue-400 text-white shadow-md'
|
||||||
|
: 'bg-white/90 backdrop-blur-md border-white text-gray-500 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
title={isHeadingMode ? "Khóa hướng Bắc" : "Xoay theo hướng nhìn"}
|
||||||
|
>
|
||||||
|
<Compass className="w-6 h-6 transition-transform duration-300" style={{ transform: `rotate(${mapRotation}deg)` }} />
|
||||||
|
</button>
|
||||||
|
|
||||||
{/* Floating Locate User Button (Fullscreen) */}
|
{/* Floating Locate User Button (Fullscreen) */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setLocateTrigger(prev => prev + 1)}
|
onClick={() => setLocateTrigger(prev => prev + 1)}
|
||||||
@@ -3307,9 +3379,7 @@ export const TourDetailPage = ({
|
|||||||
<button onClick={() => { setTravelMode('foot'); setIsMapControlsOpen(false); }} className={`w-9 h-9 flex items-center justify-center rounded-xl ${travelMode === 'foot' ? 'bg-blue-600 text-white' : 'text-gray-500'}`}><Footprints className="w-4 h-4" /></button>
|
<button onClick={() => { setTravelMode('foot'); setIsMapControlsOpen(false); }} className={`w-9 h-9 flex items-center justify-center rounded-xl ${travelMode === 'foot' ? 'bg-blue-600 text-white' : 'text-gray-500'}`}><Footprints className="w-4 h-4" /></button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newMode = !isHeadingMode;
|
toggleCompassMode();
|
||||||
setIsHeadingMode(newMode);
|
|
||||||
if (!newMode) setMapRotation(0);
|
|
||||||
setIsMapControlsOpen(false);
|
setIsMapControlsOpen(false);
|
||||||
}}
|
}}
|
||||||
className={`w-9 h-9 flex items-center justify-center rounded-xl border-t border-gray-100 transition-all ${isHeadingMode ? 'bg-blue-600 text-white' : 'text-gray-500'}`}
|
className={`w-9 h-9 flex items-center justify-center rounded-xl border-t border-gray-100 transition-all ${isHeadingMode ? 'bg-blue-600 text-white' : 'text-gray-500'}`}
|
||||||
|
|||||||
Reference in New Issue
Block a user