Sửa callout hiển thị scene và click vào scene để vào thẳng xem ảnh
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 7.7 MiB |
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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()">×</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>
|
||||||
|
|||||||
+109
-31
@@ -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) => {
|
scenes.forEach((scene) => {
|
||||||
if (layer instanceof L.Marker && layer !== tempMarker) {
|
// Tạo khóa tọa độ để đảm bảo mỗi vị trí địa lý chỉ có 1 Marker duy nhất
|
||||||
map.removeLayer(layer);
|
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
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add Markers for each scene
|
const marker = L.marker([scene.lat, scene.lng], {
|
||||||
scenes.forEach((scene) => {
|
icon: calloutIcon,
|
||||||
const marker = L.marker([scene.lat, scene.lng]).addTo(map);
|
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');
|
||||||
|
const editBtn = document.getElementById('btn-edit-action');
|
||||||
|
const deleteBtn = document.getElementById('btn-delete-action');
|
||||||
|
|
||||||
if (action) {
|
title.innerText = `Scene: ${scene.title}`;
|
||||||
// EDIT MODE
|
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';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user