Sửa callout hiển thị scene và click vào scene để vào thẳng xem ảnh

This commit is contained in:
2026-06-08 07:58:46 +07:00
parent 5ba6e37039
commit 81de520071
4 changed files with 227 additions and 33 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 MiB

+94
View File
@@ -213,3 +213,97 @@ html, body {
.save-btn { background: #28a745; color: #fff; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; } .save-btn { background: #28a745; color: #fff; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; }
.cancel-btn { background: #6c757d; color: #fff; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; } .cancel-btn { background: #6c757d; color: #fff; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; }
/* Action Modal Specifics */
.action-modal-content {
text-align: center;
max-width: 400px;
}
.action-buttons {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 20px;
}
.edit-btn-large, .delete-btn-large {
padding: 15px;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
transition: opacity 0.2s;
}
.edit-btn-large { background: #007bff; color: white; }
.delete-btn-large { background: #dc3545; color: white; }
.edit-btn-large:hover, .delete-btn-large:hover { opacity: 0.9; }
.action-modal-content p { color: #666; margin-top: 5px; }
/* Scene Callout Bubble Marker */
.scene-callout {
background: white;
border: 3px solid #007bff;
border-radius: 10px;
width: 64px;
height: 64px;
position: relative;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
transition: transform 0.2s ease-in-out;
display: flex; /* Căn giữa nội dung bên trong */
cursor: pointer;
}
.scene-img-wrapper {
width: 100%;
height: 100%;
overflow: hidden; /* Cắt ảnh nếu tràn ra ngoài border-radius */
border-radius: 7px; /* Bo góc (10px - 3px border) */
padding: 2px; /* Tạo khoảng hở giữa ảnh và viền xanh */
}
.scene-callout:hover {
transform: scale(1.1);
border-color: #0056b3;
z-index: 1000;
}
.scene-callout img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 5px; /* Bo góc ảnh nhẹ hơn wrapper một chút */
display: block;
}
.scene-callout::after {
content: '';
position: absolute;
bottom: -12px;
left: 50%;
transform: translateX(-50%);
border-width: 12px 8px 0;
border-style: solid;
border-color: #007bff transparent transparent;
}
/* Custom Tooltip Styling */
.custom-scene-tooltip {
background: rgba(0, 0, 0, 0.8) !important;
color: white !important;
border: none !important;
border-radius: 4px !important;
padding: 8px 12px !important;
font-size: 13px !important;
box-shadow: 0 2px 8px rgba(0,0,0,0.4) !important;
}
.custom-scene-tooltip .scene-hover-info strong {
font-size: 15px;
color: #ffd700; /* Màu vàng cho tiêu đề */
display: block;
margin-bottom: 3px;
}
+22
View File
@@ -8,6 +8,9 @@
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" /> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
<!-- Pannellum (3D Viewer) CSS --> <!-- Pannellum (3D Viewer) CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pannellum@2.5.6/build/pannellum.css"/> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/pannellum@2.5.6/build/pannellum.css"/>
<!-- Leaflet MarkerCluster CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.Default.css" />
<!-- Custom Style --> <!-- Custom Style -->
<link rel="stylesheet" href="css/style.css"> <link rel="stylesheet" href="css/style.css">
</head> </head>
@@ -74,6 +77,23 @@
</div> </div>
</div> </div>
<!-- Modal for Action Choice (Edit/Delete) -->
<div id="action-choice-modal" class="modal">
<div class="modal-content action-modal-content">
<span class="close-btn" onclick="closeActionModal()">&times;</span>
<h2 id="action-modal-title">Tùy chọn Scene</h2>
<p id="action-modal-desc">Bạn muốn thực hiện thao tác gì với scene này?</p>
<div class="action-buttons">
<button id="btn-edit-action" class="edit-btn-large">
<span class="icon">✏️</span> Chỉnh sửa thông tin
</button>
<button id="btn-delete-action" class="delete-btn-large">
<span class="icon">🗑️</span> Xóa vĩnh viễn
</button>
</div>
</div>
</div>
<!-- 3D Panorama Viewer Container --> <!-- 3D Panorama Viewer Container -->
<div id="viewer-container" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 2000; background: #000;"> <div id="viewer-container" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 2000; background: #000;">
<div id="panorama-viewer"></div> <div id="panorama-viewer"></div>
@@ -163,6 +183,8 @@
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
<!-- Pannellum JS --> <!-- Pannellum JS -->
<script src="https://cdn.jsdelivr.net/npm/pannellum@2.5.6/build/pannellum.js"></script> <script src="https://cdn.jsdelivr.net/npm/pannellum@2.5.6/build/pannellum.js"></script>
<!-- Leaflet MarkerCluster JS -->
<script src="https://unpkg.com/leaflet.markercluster@1.4.1/dist/leaflet.markercluster.js"></script>
<!-- Custom Scripts --> <!-- Custom Scripts -->
<script src="js/viewer360.js"></script> <script src="js/viewer360.js"></script>
+111 -33
View File
@@ -2,6 +2,7 @@ const API_BASE_URL = 'http://localhost:5000/api';
let map; let map;
let tempMarker = null; let tempMarker = null;
let markerClusterGroup;
let currentSceneId = null; let currentSceneId = null;
let previousSceneId = null; let previousSceneId = null;
@@ -34,6 +35,27 @@ function initMap() {
attribution: '© OpenStreetMap contributors' attribution: '© OpenStreetMap contributors'
}).addTo(map); }).addTo(map);
// Khởi tạo Marker Cluster Group CHỈ dành cho Scene (Ảnh mẹ)
markerClusterGroup = L.markerClusterGroup({
zoomToBoundsOnClick: false,
spiderfyOnMaxZoom: true,
maxClusterRadius: 50,
spiderfyDistanceMultiplier: 3.5, // Tăng thêm khoảng cách để callout ảnh mẹ tách rõ ràng khi tỏa ra
showCoverageOnHover: false,
iconCreateFunction: function(cluster) {
const childMarkers = cluster.getAllChildMarkers();
// Lấy icon của Scene đầu tiên trong nhóm để làm đại diện cho cả cụm
return childMarkers[0].options.icon;
}
});
// Khi click chuột trái vào một cụm callout, thực hiện tách chúng ra
markerClusterGroup.on('clusterclick', (a) => {
a.layer.spiderfy();
});
map.addLayer(markerClusterGroup);
// Lưu vị trí bản đồ mỗi khi người dùng di chuyển hoặc zoom xong // Lưu vị trí bản đồ mỗi khi người dùng di chuyển hoặc zoom xong
map.on('moveend', () => { map.on('moveend', () => {
const center = map.getCenter(); const center = map.getCenter();
@@ -284,45 +306,84 @@ async function loadScenes() {
if (!response.ok) throw new Error('Failed to load scenes'); if (!response.ok) throw new Error('Failed to load scenes');
const scenes = await response.json(); const scenes = await response.json();
// Xóa sạch các layers cũ và chuẩn bị lọc tọa độ
markerClusterGroup.clearLayers();
const markersToAdd = [];
const activeSceneId = localStorage.getItem('activeSceneId'); const activeSceneId = localStorage.getItem('activeSceneId');
const currentUserId = localStorage.getItem('userId'); const seenCoordinates = new Set();
// Clear existing markers (excluding tempMarker) // Chỉ lặp qua danh sách Scene mẹ, lọc bỏ các hotspots trùng tọa độ
map.eachLayer((layer) => {
if (layer instanceof L.Marker && layer !== tempMarker) {
map.removeLayer(layer);
}
});
// Add Markers for each scene
scenes.forEach((scene) => { scenes.forEach((scene) => {
const marker = L.marker([scene.lat, scene.lng]).addTo(map); // Tạo khóa tọa độ để đảm bảo mỗi vị trí địa lý chỉ có 1 Marker duy nhất
const coordKey = `${scene.lat.toFixed(6)},${scene.lng.toFixed(6)}`;
if (seenCoordinates.has(coordKey)) return; // Bỏ qua nếu tọa độ này đã có Marker
seenCoordinates.add(coordKey);
let thumbUrl = `${API_BASE_URL}/assets/view/${scene.assetId._id}`;
if (token) thumbUrl += `?token=${token}`;
else if (scene.privacy === 'shared' && scene.shareToken) thumbUrl += `?token=${scene.shareToken}`;
const calloutIcon = L.divIcon({
className: 'custom-scene-marker',
html: `
<div class="scene-callout">
<div class="scene-img-wrapper">
<img src="${thumbUrl}" alt="${scene.title}">
</div>
</div>`,
iconSize: [64, 64],
iconAnchor: [32, 76] // Căn giữa ngang, đáy mũi tên tại tọa độ lat/lng
});
const marker = L.marker([scene.lat, scene.lng], {
icon: calloutIcon,
title: scene.title // Tooltip khi di chuột qua
});
// Generate Popup content with a View button // Tạo nội dung thông tin khi Hover (Tooltip)
let popupContent = ` const createdDate = scene.assetId?.createdAt ? new Date(scene.assetId.createdAt).toLocaleDateString('vi-VN') : 'N/A';
<div class="popup-box"> const tooltipContent = `
<h4>${scene.title}</h4> <div class="scene-hover-info">
<p>Owner: <strong>${scene.owner ? scene.owner.username : 'Unknown'}</strong></p> <strong>${scene.title}</strong><br>
<p>Privacy: <span class="badge">${scene.privacy.toUpperCase()}</span></p> ${scene.description ? `<small>${scene.description}</small><br>` : ''}
<button class="view-btn" onclick="openScene('${scene._id}', '${scene.privacy}', '${scene.shareToken || ''}')">View 360° Panorama</button> <span>Người tạo: ${scene.owner ? scene.owner.username : 'Ẩn danh'}</span><br>
<span>Ngày tạo: ${createdDate}</span>
</div> </div>
`; `;
marker.bindPopup(popupContent);
// Nếu đây là scene đang xem, tự động mở popup // Gán Tooltip cho sự kiện Hover
if (activeSceneId && scene._id === activeSceneId) { marker.bindTooltip(tooltipContent, {
marker.openPopup(); direction: 'top',
} offset: [0, -70],
className: 'custom-scene-tooltip'
});
// Sự kiện Click chuột trái: Vào thẳng trình xem 360
marker.on('click', () => {
openScene(scene._id, scene.privacy, scene.shareToken || '');
});
// Right-click on marker to Edit/Delete (Owner only)
marker.on('contextmenu', (e) => { marker.on('contextmenu', (e) => {
L.DomEvent.stopPropagation(e); if (e.originalEvent) {
if (currentUserId && scene.owner && scene.owner._id === currentUserId) { L.DomEvent.stop(e.originalEvent);
}
const currentUserId = localStorage.getItem('userId');
const ownerId = scene.owner?._id || scene.owner;
if (currentUserId && ownerId && ownerId.toString() === currentUserId.toString()) {
handleEditDeleteScene(scene); handleEditDeleteScene(scene);
} else {
alert("Bạn không có quyền chỉnh sửa scene này.");
} }
}); });
markersToAdd.push(marker);
}); });
// Thêm danh sách marker đã lọc vào group
markerClusterGroup.addLayers(markersToAdd);
} catch (error) { } catch (error) {
console.error('Error loading scenes:', error); console.error('Error loading scenes:', error);
} }
@@ -332,17 +393,34 @@ async function loadScenes() {
* Handles Edit/Delete options for a scene * Handles Edit/Delete options for a scene
*/ */
async function handleEditDeleteScene(scene) { async function handleEditDeleteScene(scene) {
const action = confirm(`Bạn muốn làm gì với scene "${scene.title}"?\n\n- Nhấn OK để CHỈNH SỬA\n- Nhấn Cancel để XÓA`); const modal = document.getElementById('action-choice-modal');
const title = document.getElementById('action-modal-title');
if (action) { const editBtn = document.getElementById('btn-edit-action');
// EDIT MODE const deleteBtn = document.getElementById('btn-delete-action');
title.innerText = `Scene: ${scene.title}`;
modal.style.display = 'flex';
// Gán sự kiện cho nút Sửa
editBtn.onclick = () => {
closeActionModal();
openEditSceneModal(scene); openEditSceneModal(scene);
} else { };
// DELETE MODE
if (confirm(`Bạn có chắc chắn muốn xóa vĩnh viễn scene "${scene.title}"?`)) { // Gán sự kiện cho nút Xóa
deleteBtn.onclick = async () => {
if (confirm(`Cảnh báo: Thao tác này sẽ xóa vĩnh viễn scene "${scene.title}" và tệp tin ảnh 360 liên quan. Bạn có chắc chắn?`)) {
closeActionModal();
await deleteScene(scene._id); await deleteScene(scene._id);
} }
} };
}
/**
* Closes the Action Choice Modal
*/
function closeActionModal() {
document.getElementById('action-choice-modal').style.display = 'none';
} }
/** /**