fix: bản đồ xoay mượt trên Iphone

This commit is contained in:
2026-06-25 16:48:26 +07:00
parent 4e93722ba5
commit d2aced396f
+113 -43
View File
@@ -222,7 +222,7 @@ const MapRotationHandler = ({ rotation }: { rotation: number }) => {
// 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.
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]);
return null;
};
@@ -867,6 +867,8 @@ export const TourDetailPage = ({
// Theo dõi hướng thiết bị (la bàn)
useEffect(() => {
let lastHeading: number | null = null;
const handleOrientation = (event: any) => {
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
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') {
// 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;
}
}
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 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
const [initialViewState] = useState(() => {
@@ -1905,7 +1932,9 @@ export const TourDetailPage = ({
)}
</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">
<img
src={tourInfo.coverImage}
@@ -2168,11 +2197,14 @@ export const TourDetailPage = ({
</div>
)}
</div>
</>
)}
{/* 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 */}
<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) => (
<button
key={tab.id}
@@ -2196,44 +2228,49 @@ export const TourDetailPage = ({
</button>
))}
</div>
)}
{/* Tab Panels */}
<div className="transition-opacity duration-300">
{activeTab === 'plan' && (
<div className="animate-in fade-in slide-in-from-bottom-2">
{/* View Mode Toggle */}
<div className="flex justify-center items-center mb-3">
<div className="bg-gray-100 dark:bg-slate-800 p-1 rounded-2xl flex gap-1">
<button
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>
<button
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>
{viewMode !== 'map' && (
<div className="flex justify-center items-center mb-3">
<div className="bg-gray-100 dark:bg-slate-800 p-1 rounded-2xl flex gap-1">
<button
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>
<button
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>
</div>
</div>
</div>
)}
{/* Export Buttons */}
<div className="flex justify-center gap-2 mb-6">
<button
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>
<button
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>
</div>
{viewMode !== 'map' && (
<div className="flex justify-center gap-2 mb-6">
<button
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>
<button
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>
</div>
)}
{viewMode === 'timeline' ? (
<ItineraryTimeline onAddLocation={(legId, isStart, isEnd) => {
@@ -2259,7 +2296,7 @@ export const TourDetailPage = ({
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 */}
{!isPublicView && (
<div className="absolute top-3 right-3 z-[1001] w-48 md:w-64">
@@ -2485,6 +2522,19 @@ export const TourDetailPage = ({
</MarkerClusterGroup>
</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 */}
<button
onClick={() => setLocateTrigger(prev => prev + 1)}
@@ -2494,6 +2544,15 @@ export const TourDetailPage = ({
>
<LocateFixed className="w-5 h-5" />
</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 */}
{routeMenu && (
@@ -3194,7 +3253,7 @@ export const TourDetailPage = ({
</div>
{/* 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">
<button
onClick={() => {
@@ -3281,6 +3340,19 @@ export const TourDetailPage = ({
})}
</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) */}
<button
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={() => {
const newMode = !isHeadingMode;
setIsHeadingMode(newMode);
if (!newMode) setMapRotation(0);
toggleCompassMode();
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'}`}