Sửa lỗi đăng nhập vào admin mà không reload được page do lỗi tạo scene trước đó, sử dụng lệnh resetDB.js để khởi tạo lại, xóa các scene trước và ảnh đã upload
This commit is contained in:
@@ -138,6 +138,7 @@ html, body {
|
||||
|
||||
/* 3D Viewer Overlays (Full Screen) */
|
||||
#viewer-container {
|
||||
display: none; /* Khởi tạo ẩn để không chặn chuột */
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -169,15 +170,6 @@ html, body {
|
||||
#close-viewer-btn:hover {
|
||||
background: white;
|
||||
}
|
||||
/* Modal Overlay */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0; left: 0; width: 100%; height: 100%;
|
||||
background: rgba(0,0,0,0.7);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
|
||||
@@ -72,6 +72,16 @@
|
||||
<label for="modal-shared-users">Shared with User IDs (JSON Array):</label>
|
||||
<input type="text" id="modal-shared-users" name="sharedWithUsers" placeholder='["60c72b2f9b1d8a41c8888888"]'>
|
||||
</div>
|
||||
<!-- Progress Bar for Scene Upload -->
|
||||
<div id="create-progress-container" style="display: none; margin-bottom: 15px;">
|
||||
<div style="display: flex; justify-content: space-between; font-size: 12px; margin-bottom: 5px;">
|
||||
<span id="create-progress-status">Uploading image...</span>
|
||||
<span id="create-progress-percent">0%</span>
|
||||
</div>
|
||||
<div style="width: 100%; height: 8px; background: #eee; border-radius: 4px; overflow: hidden;">
|
||||
<div id="create-progress-bar" style="width: 0%; height: 100%; background: #28a745; transition: width 0.3s;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="submit-btn">Save Scene</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
+110
-28
@@ -1,4 +1,4 @@
|
||||
const API_BASE_URL = 'http://localhost:5000/api';
|
||||
const API_BASE_URL = '/api'; // Sử dụng đường dẫn tương đối để tránh lỗi CORS/Hostname
|
||||
|
||||
let map;
|
||||
let tempMarker = null;
|
||||
@@ -8,10 +8,27 @@ let previousSceneId = null;
|
||||
|
||||
// Initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initMap();
|
||||
checkAuthStatus();
|
||||
loadScenes();
|
||||
restoreActiveScene();
|
||||
try {
|
||||
console.log("--- Bắt đầu khởi tạo Frontend ---");
|
||||
if (document.getElementById('map')) {
|
||||
console.log("1. Đang khởi tạo bản đồ Leaflet...");
|
||||
initMap();
|
||||
}
|
||||
|
||||
// Chạy tuần tự để tránh xung đột luồng xử lý
|
||||
checkAuthStatus(); // 2. Kiểm tra đăng nhập
|
||||
|
||||
// Đảm bảo map đã sẵn sàng trước khi nạp data
|
||||
if (map) {
|
||||
loadScenes().then(() => {
|
||||
console.log("4. Đang chuẩn bị khôi phục Scene cũ (nếu có)...");
|
||||
// Chỉ khôi phục khi bản đồ đã nạp xong các marker
|
||||
setTimeout(restoreActiveScene, 500);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Ứng dụng không thể khởi tạo:", error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -23,12 +40,16 @@ function initMap() {
|
||||
const savedLng = localStorage.getItem('map-lng');
|
||||
const savedZoom = localStorage.getItem('map-zoom');
|
||||
|
||||
// Nếu có dữ liệu cũ thì dùng, không thì mặc định là Hà Nội
|
||||
const startLat = savedLat ? parseFloat(savedLat) : 21.0285;
|
||||
const startLng = savedLng ? parseFloat(savedLng) : 105.8542;
|
||||
const startZoom = savedZoom ? parseInt(savedZoom) : 13;
|
||||
// Đảm bảo tọa độ khởi tạo luôn hợp lệ
|
||||
let startLat = parseFloat(savedLat);
|
||||
let startLng = parseFloat(savedLng);
|
||||
let startZoom = parseInt(savedZoom);
|
||||
|
||||
map = L.map('map').setView([startLat, startLng], startZoom);
|
||||
if (isNaN(startLat)) startLat = 21.0285;
|
||||
if (isNaN(startLng)) startLng = 105.8542;
|
||||
if (isNaN(startZoom)) startZoom = 13;
|
||||
|
||||
map = L.map('map', { zoomControl: true }).setView([startLat, startLng], startZoom);
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
@@ -44,8 +65,13 @@ function initMap() {
|
||||
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;
|
||||
try {
|
||||
// Thay vì tạo object mới phức tạp, lấy HTML của marker đầu tiên
|
||||
const firstIcon = childMarkers[0].options.icon;
|
||||
if (firstIcon) return firstIcon;
|
||||
} catch (e) {}
|
||||
// Fallback an toàn nếu có lỗi
|
||||
return L.divIcon({ className: 'cluster-fallback', html: '<div style="background:#007bff;width:10px;height:10px;border-radius:50%;"></div>' });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -174,6 +200,12 @@ function handleLogout() {
|
||||
localStorage.removeItem('activeScenePrivacy');
|
||||
localStorage.removeItem('activeSceneToken');
|
||||
localStorage.removeItem('userId');
|
||||
|
||||
// Đảm bảo đóng viewer nếu đang mở
|
||||
if (typeof closeViewer === 'function') {
|
||||
closeViewer();
|
||||
}
|
||||
|
||||
checkAuthStatus();
|
||||
loadScenes(); // Reload scenes to filter out private ones
|
||||
alert('Logged out successfully');
|
||||
@@ -193,10 +225,10 @@ function openCreateSceneModal(lat, lng) {
|
||||
if (tempMarker) map.removeLayer(tempMarker);
|
||||
tempMarker = L.marker([lat, lng]).addTo(map);
|
||||
|
||||
document.getElementById('create-scene-modal').style.display = 'flex';
|
||||
document.getElementById('modal-scene-id').value = '';
|
||||
document.getElementById('modal-lat').value = lat.toFixed(6);
|
||||
document.getElementById('modal-lng').value = lng.toFixed(6);
|
||||
document.getElementById('create-scene-modal').style.display = 'flex';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -255,30 +287,34 @@ function uploadWithProgress(url, method, formData, token, prefix, callback) {
|
||||
const percentText = document.getElementById(`${prefix}-progress-percent`);
|
||||
const statusText = document.getElementById(`${prefix}-progress-status`);
|
||||
|
||||
container.style.display = 'block';
|
||||
statusText.innerText = "Đang tải ảnh lên...";
|
||||
if (container) container.style.display = 'block';
|
||||
if (statusText) statusText.innerText = "Đang tải ảnh lên...";
|
||||
|
||||
xhr.upload.addEventListener('progress', (e) => {
|
||||
if (e.lengthComputable) {
|
||||
const percent = Math.round((e.loaded / e.total) * 100);
|
||||
bar.style.width = percent + '%';
|
||||
percentText.innerText = percent + '%';
|
||||
if (percent === 100) statusText.innerText = "Tải lên xong! Đang khởi tạo trên server...";
|
||||
if (bar) bar.style.width = percent + '%';
|
||||
if (percentText) percentText.innerText = percent + '%';
|
||||
if (percent === 100 && statusText) statusText.innerText = "Tải lên xong! Đang khởi tạo trên server...";
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener('load', () => {
|
||||
container.style.display = 'none';
|
||||
if (container) container.style.display = 'none';
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
callback(JSON.parse(xhr.responseText));
|
||||
} else {
|
||||
const err = JSON.parse(xhr.responseText);
|
||||
alert('Lỗi: ' + (err.message || 'Không thể tải lên'));
|
||||
let errorMsg = 'Không thể tải lên';
|
||||
try {
|
||||
const err = JSON.parse(xhr.responseText);
|
||||
errorMsg = err.message || errorMsg;
|
||||
} catch (e) {}
|
||||
alert('Lỗi: ' + errorMsg);
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener('error', () => {
|
||||
container.style.display = 'none';
|
||||
if (container) container.style.display = 'none';
|
||||
alert('Lỗi kết nối mạng.');
|
||||
});
|
||||
|
||||
@@ -298,27 +334,45 @@ async function loadScenes() {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/scenes`, {
|
||||
// Thêm timestamp để tránh lỗi 304 hang do cache trình duyệt
|
||||
const timestamp = new Date().getTime();
|
||||
console.log(`3.1 Đang gửi yêu cầu lấy danh sách Scene (ts: ${timestamp})...`);
|
||||
const response = await fetch(`${API_BASE_URL}/scenes?_=${timestamp}`, {
|
||||
method: 'GET',
|
||||
headers
|
||||
});
|
||||
|
||||
console.log(`[API Response] /scenes status: ${response.status}`);
|
||||
if (!response.ok) throw new Error('Failed to load scenes');
|
||||
const scenes = await response.json();
|
||||
|
||||
console.log(`[Data] Nhận được ${scenes.length} scenes từ server`);
|
||||
if (!Array.isArray(scenes)) return;
|
||||
|
||||
// Xóa sạch các layers cũ và chuẩn bị lọc tọa độ
|
||||
// Xóa sạch các layers cũ trước khi nạp mới
|
||||
markerClusterGroup.clearLayers();
|
||||
const markersToAdd = [];
|
||||
const activeSceneId = localStorage.getItem('activeSceneId');
|
||||
const seenCoordinates = new Set();
|
||||
const seenCoordinates = new Set(); // Dùng để lọc "Ảnh mẹ" (1 marker per location)
|
||||
|
||||
// Chỉ lặp qua danh sách Scene mẹ, lọc bỏ các hotspots trùng tọa độ
|
||||
scenes.forEach((scene) => {
|
||||
// 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)}`;
|
||||
const latNum = parseFloat(scene.lat);
|
||||
const lngNum = parseFloat(scene.lng);
|
||||
|
||||
if (isNaN(latNum) || isNaN(lngNum)) return;
|
||||
|
||||
// Logic lọc Ảnh mẹ: Mỗi tọa độ GPS chỉ tạo duy nhất 1 Marker đại diện
|
||||
const coordKey = `${latNum.toFixed(6)},${lngNum.toFixed(6)}`;
|
||||
if (seenCoordinates.has(coordKey)) return; // Bỏ qua nếu tọa độ này đã có Marker
|
||||
seenCoordinates.add(coordKey);
|
||||
|
||||
// Kiểm tra an toàn dữ liệu từ MongoDB trước khi truy cập
|
||||
if (!scene.assetId || !scene.assetId._id) {
|
||||
console.warn(`Scene "${scene.title}" thiếu dữ liệu ảnh (AssetId), bỏ qua.`);
|
||||
return;
|
||||
}
|
||||
|
||||
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}`;
|
||||
@@ -335,7 +389,7 @@ async function loadScenes() {
|
||||
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], {
|
||||
const marker = L.marker([latNum, lngNum], {
|
||||
icon: calloutIcon,
|
||||
title: scene.title // Tooltip khi di chuột qua
|
||||
});
|
||||
@@ -472,6 +526,7 @@ async function openScene(sceneId, privacy, shareToken, force = false) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
console.log(`[Viewer] Đang mở scene: ${sceneId}`);
|
||||
let url = `${API_BASE_URL}/scenes/${sceneId}`;
|
||||
if (privacy === 'shared' && shareToken) {
|
||||
url += `?token=${shareToken}`;
|
||||
@@ -703,3 +758,30 @@ async function saveHotspotToDB(pitch, yaw, text, description, targetSceneId, hot
|
||||
alert(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Công cụ dọn dẹp toàn bộ dữ liệu (Chỉ dùng cho nhà phát triển)
|
||||
* Gọi lệnh: systemReset() từ trình duyệt
|
||||
*/
|
||||
window.systemReset = async function() {
|
||||
if (!confirm("CẢNH BÁO: Thao tác này sẽ xóa sạch TOÀN BỘ scene và ảnh trên server. Bạn có chắc chắn?")) return;
|
||||
|
||||
const token = localStorage.getItem('jwt');
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/maintenance/reset-all`, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
localStorage.clear(); // Xóa sạch token, vị trí map, active scene
|
||||
alert(data.message);
|
||||
location.reload();
|
||||
} else {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
} catch (e) {
|
||||
alert("Lỗi reset: " + e.message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
let activeViewer = null;
|
||||
let currentHotspots = [];
|
||||
let securityApplied = false;
|
||||
|
||||
/**
|
||||
* Initializes and shows the Pannellum 360° panorama viewer with security overlays.
|
||||
@@ -72,6 +73,9 @@ function closeViewer() {
|
||||
* Appends event listeners to block right-clicks and common image saving shortcuts.
|
||||
*/
|
||||
function applyViewerSecurity() {
|
||||
if (securityApplied) return; // Chỉ gán sự kiện một lần duy nhất
|
||||
securityApplied = true;
|
||||
|
||||
// Target the actual viewer element where Pannellum renders
|
||||
const container = document.getElementById('viewer-container');
|
||||
const panoramaViewer = document.getElementById('panorama-viewer');
|
||||
@@ -114,18 +118,16 @@ function applyViewerSecurity() {
|
||||
});
|
||||
}
|
||||
|
||||
// Global safety shortcut listeners (F12, Ctrl+S, Ctrl+U, Ctrl+Shift+I)
|
||||
// Global safety shortcut listeners (Ctrl+S, Ctrl+U) - Tạm thời cho phép F12 và Ctrl+Shift+I để debug
|
||||
document.addEventListener('keydown', (e) => {
|
||||
// Only enforce when viewer is active
|
||||
if (document.getElementById('viewer-container').style.display === 'block') {
|
||||
const isCtrlS = e.ctrlKey && (e.key === 's' || e.key === 'S');
|
||||
const isCtrlU = e.ctrlKey && (e.key === 'u' || e.key === 'U');
|
||||
const isF12 = e.key === 'F12';
|
||||
const isCtrlShiftI = e.ctrlKey && e.shiftKey && (e.key === 'i' || e.key === 'I');
|
||||
|
||||
if (isCtrlS || isCtrlU || isF12 || isCtrlShiftI) {
|
||||
if (isCtrlS || isCtrlU) {
|
||||
e.preventDefault();
|
||||
alert('Security Alert: Inspection and saving functions are restricted on this viewer.');
|
||||
console.warn('Security Alert: Inspection and saving functions are restricted.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user