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.
|
||||
// 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,18 +878,18 @@ 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) {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Đăng ký cả hai loại sự kiện để hỗ trợ tối đa các dòng điện thoại
|
||||
@@ -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,6 +1932,8 @@ export const TourDetailPage = ({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!(activeTab === 'plan' && viewMode === 'map') && (
|
||||
<>
|
||||
{/* Tour Header Info */}
|
||||
<div className="relative min-h-[480px] w-full bg-blue-900 flex flex-col justify-end">
|
||||
<img
|
||||
@@ -2168,10 +2197,13 @@ 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 */}
|
||||
{!(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
|
||||
@@ -2196,12 +2228,14 @@ 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 */}
|
||||
{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
|
||||
@@ -2218,8 +2252,10 @@ export const TourDetailPage = ({
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Export Buttons */}
|
||||
{viewMode !== 'map' && (
|
||||
<div className="flex justify-center gap-2 mb-6">
|
||||
<button
|
||||
onClick={handleExportCSV}
|
||||
@@ -2234,6 +2270,7 @@ export const TourDetailPage = ({
|
||||
📥 {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)}
|
||||
@@ -2495,6 +2545,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 && (
|
||||
<div
|
||||
@@ -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'}`}
|
||||
|
||||
Reference in New Issue
Block a user