Files
3dtours/frontend/js/main_map.js
T

788 lines
29 KiB
JavaScript

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;
let markerClusterGroup;
let currentSceneId = null;
let previousSceneId = null;
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
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);
}
});
/**
* Initializes the full-screen Leaflet Map
*/
function initMap() {
// Đọc vị trí và zoom đã lưu từ localStorage
const savedLat = localStorage.getItem('map-lat');
const savedLng = localStorage.getItem('map-lng');
const savedZoom = localStorage.getItem('map-zoom');
// Đả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);
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,
attribution: '© OpenStreetMap contributors'
}).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();
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>' });
}
});
// 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
map.on('moveend', () => {
const center = map.getCenter();
localStorage.setItem('map-lat', center.lat);
localStorage.setItem('map-lng', center.lng);
localStorage.setItem('map-zoom', map.getZoom());
});
// Event listener for right-click on map to open modal
map.on('contextmenu', (e) => {
// Nếu viewer 3D đang hiển thị, không thực hiện tạo scene trên bản đồ
const viewerContainer = document.getElementById('viewer-container');
// Kiểm tra z-index và display để chắc chắn viewer đang ẩn
if (viewerContainer && viewerContainer.style.display !== 'none') {
return;
}
const { lat, lng } = e.latlng;
openCreateSceneModal(lat, lng);
});
}
/**
* Checks if user is logged in (via localStorage JWT) and updates UI
*/
function checkAuthStatus() {
const token = localStorage.getItem('jwt');
const username = localStorage.getItem('username');
const role = localStorage.getItem('role');
const authGuest = document.getElementById('auth-guest');
const authLoggedIn = document.getElementById('auth-logged-in');
if (token && username) {
authGuest.style.display = 'none';
authLoggedIn.style.display = 'block';
document.getElementById('logged-username').innerText = username;
document.getElementById('logged-role').innerText = role || 'Thành viên';
} else {
authGuest.style.display = 'block';
authLoggedIn.style.display = 'none';
}
}
/**
* Handles user login
*/
async function handleLogin() {
const username = document.getElementById('username-input').value.trim();
const password = document.getElementById('password-input').value.trim();
if (!username || !password) {
alert('Please fill in both fields');
return;
}
try {
const response = await fetch(`${API_BASE_URL}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (!response.ok) throw new Error(data.message || 'Login failed');
localStorage.setItem('jwt', data.token);
localStorage.setItem('username', data.user.username);
localStorage.setItem('role', data.user.role);
localStorage.setItem('userId', data.user.id);
checkAuthStatus();
loadScenes(); // Reload scenes to show member/private scenes
alert('Logged in successfully!');
} catch (error) {
alert(error.message);
}
}
/**
* Handles user registration
*/
async function handleRegister() {
const username = document.getElementById('username-input').value.trim();
const password = document.getElementById('password-input').value.trim();
if (!username || !password) {
alert('Please fill in both fields');
return;
}
try {
const response = await fetch(`${API_BASE_URL}/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password, role: 'Thành viên' })
});
const data = await response.json();
if (!response.ok) throw new Error(data.message || 'Registration failed');
alert('Registration successful! You can now log in.');
} catch (error) {
alert(error.message);
}
}
/**
* Handles user logout
*/
function handleLogout() {
localStorage.removeItem('jwt');
localStorage.removeItem('username');
localStorage.removeItem('role');
localStorage.removeItem('activeSceneId');
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');
}
/**
* Opens Modal for creating a Scene and sets lat/lng inputs
*/
function openCreateSceneModal(lat, lng) {
const token = localStorage.getItem('jwt');
if (!token) {
alert('Please log in first to create a 3D scene.');
return;
}
// Place a temporary marker on the map
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);
}
/**
* Closes the Create Scene Modal and removes temporary marker
*/
function closeModal() {
document.getElementById('create-scene-modal').style.display = 'none';
if (tempMarker) {
map.removeLayer(tempMarker);
tempMarker = null;
}
document.getElementById('create-scene-form').reset();
document.getElementById('shared-with-group').style.display = 'none';
}
/**
* Toggles visibility of the shared users input based on privacy selection
*/
function toggleSharedUsers() {
const privacy = document.getElementById('modal-privacy').value;
const group = document.getElementById('shared-with-group');
if (privacy === 'shared') {
group.style.display = 'block';
} else {
group.style.display = 'none';
}
}
/**
* Form submission for Scene creation (multipart/form-data)
*/
async function submitScene(e) {
e.preventDefault();
const form = document.getElementById('create-scene-form');
const formData = new FormData(form);
const sceneId = document.getElementById('modal-scene-id').value;
const token = localStorage.getItem('jwt');
const url = sceneId ? `${API_BASE_URL}/scenes/${sceneId}` : `${API_BASE_URL}/scenes`;
const method = sceneId ? 'PUT' : 'POST';
uploadWithProgress(url, method, formData, token, 'create', () => {
alert(sceneId ? 'Scene đang được cập nhật ngầm!' : 'Scene đã được tạo! Ảnh đang được xử lý 8K...');
closeModal();
loadScenes();
});
}
/**
* Helper function to handle uploads with progress bars
*/
function uploadWithProgress(url, method, formData, token, prefix, callback) {
const xhr = new XMLHttpRequest();
const container = document.getElementById(`${prefix}-progress-container`);
const bar = document.getElementById(`${prefix}-progress-bar`);
const percentText = document.getElementById(`${prefix}-progress-percent`);
const statusText = document.getElementById(`${prefix}-progress-status`);
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);
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', () => {
if (container) container.style.display = 'none';
if (xhr.status >= 200 && xhr.status < 300) {
callback(JSON.parse(xhr.responseText));
} else {
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', () => {
if (container) container.style.display = 'none';
alert('Lỗi kết nối mạng.');
});
xhr.open(method, url);
if (token) xhr.setRequestHeader('Authorization', `Bearer ${token}`);
xhr.send(formData);
}
/**
* Loads and displays visible Scenes on the map
*/
async function loadScenes() {
try {
const token = localStorage.getItem('jwt');
const headers = {};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
// 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ũ trước khi nạp mới
markerClusterGroup.clearLayers();
const markersToAdd = [];
const activeSceneId = localStorage.getItem('activeSceneId');
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) => {
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}`;
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([latNum, lngNum], {
icon: calloutIcon,
title: scene.title // Tooltip khi di chuột qua
});
// Tạo nội dung thông tin khi Hover (Tooltip)
const createdDate = scene.assetId?.createdAt ? new Date(scene.assetId.createdAt).toLocaleDateString('vi-VN') : 'N/A';
const tooltipContent = `
<div class="scene-hover-info">
<strong>${scene.title}</strong><br>
${scene.description ? `<small>${scene.description}</small><br>` : ''}
<span>Người tạo: ${scene.owner ? scene.owner.username : 'Ẩn danh'}</span><br>
<span>Ngày tạo: ${createdDate}</span>
</div>
`;
// Gán Tooltip cho sự kiện Hover
marker.bindTooltip(tooltipContent, {
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 || '');
});
marker.on('contextmenu', (e) => {
if (e.originalEvent) {
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);
} 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) {
console.error('Error loading scenes:', error);
}
}
/**
* Handles Edit/Delete options for a scene
*/
async function handleEditDeleteScene(scene) {
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');
title.innerText = `Scene: ${scene.title}`;
modal.style.display = 'flex';
// Gán sự kiện cho nút Sửa
editBtn.onclick = () => {
closeActionModal();
openEditSceneModal(scene);
};
// 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);
}
};
}
/**
* Closes the Action Choice Modal
*/
function closeActionModal() {
document.getElementById('action-choice-modal').style.display = 'none';
}
/**
* Opens the modal in Edit mode
*/
function openEditSceneModal(scene) {
document.getElementById('modal-scene-id').value = scene._id;
document.getElementById('modal-lat').value = scene.lat;
document.getElementById('modal-lng').value = scene.lng;
document.getElementById('modal-title').value = scene.title;
document.getElementById('modal-privacy').value = scene.privacy;
document.getElementById('modal-panorama').required = false; // Photo update is optional
toggleSharedUsers();
document.getElementById('create-scene-modal').style.display = 'flex';
}
/**
* Deletes a scene via API
*/
async function deleteScene(sceneId) {
const token = localStorage.getItem('jwt');
try {
const response = await fetch(`${API_BASE_URL}/scenes/${sceneId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` }
});
if (!response.ok) throw new Error('Failed to delete scene');
alert('Scene deleted successfully');
loadScenes();
} catch (error) {
alert(error.message);
}
}
/**
* Fetches secure scene details and triggers the Panorama viewer
*/
async function openScene(sceneId, privacy, shareToken, force = false) {
// Nếu đang xem chính scene này và không yêu cầu làm mới (force), không cần nạp lại
if (!force && currentSceneId === sceneId && document.getElementById('viewer-container').style.display === 'block') {
return;
}
try {
const token = localStorage.getItem('jwt');
const headers = {};
if (token) {
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}`;
}
// Lưu trạng thái Scene hiện tại để khôi phục sau khi reload trang
localStorage.setItem('activeSceneId', sceneId);
localStorage.setItem('activeScenePrivacy', privacy || '');
localStorage.setItem('activeSceneToken', shareToken || '');
const response = await fetch(url, {
method: 'GET',
headers
});
const scene = await response.json();
if (!response.ok) throw new Error(scene.message || 'Failed to fetch scene details');
// Tự động focus bản đồ vào vị trí của Scene
if (map) {
map.flyTo([scene.lat, scene.lng], 16);
}
// Cập nhật tọa độ vào các input ẩn để hỗ trợ GPS inheritance cho hotspot khi tải ảnh mới
document.getElementById('modal-lat').value = scene.lat;
document.getElementById('modal-lng').value = scene.lng;
// Cập nhật lịch sử di chuyển để hỗ trợ tạo hotspot ngược tự động
if (currentSceneId && currentSceneId !== sceneId) {
previousSceneId = currentSceneId;
}
currentSceneId = sceneId;
// Construct secure image URL passing shareToken if applicable
let secureImageUrl = `${API_BASE_URL}/assets/view/${scene.assetId._id}`;
// Ưu tiên JWT token nếu đang đăng nhập, nếu không thì dùng shareToken
if (token) {
secureImageUrl += `?token=${token}`;
} else if (privacy === 'shared' && scene.shareToken) {
secureImageUrl += `?token=${scene.shareToken}`;
}
// Initialize 3D Viewer with secure, referer-protected image stream
initPanoramaViewer(secureImageUrl, scene.hotspots || []);
} catch (error) {
localStorage.removeItem('activeSceneId');
localStorage.removeItem('activeScenePrivacy');
localStorage.removeItem('activeSceneToken');
alert(error.message);
}
}
/**
* Khôi phục Scene đang xem từ localStorage sau khi reload trang
*/
function restoreActiveScene() {
const savedSceneId = localStorage.getItem('activeSceneId');
if (savedSceneId) {
const savedPrivacy = localStorage.getItem('activeScenePrivacy');
const savedToken = localStorage.getItem('activeSceneToken');
openScene(savedSceneId, savedPrivacy, savedToken);
}
}
/**
* Xử lý việc tạo hotspot sau khi click chuột phải trong trình xem 360
* @param {number} pitch - Tọa độ dọc (-90 đến 90)
* @param {number} yaw - Tọa độ ngang (-180 đến 180)
* @param {Object} existingHotspot - Thông tin hotspot cũ nếu có
*/
window.handleHotspotCreation = async function(pitch, yaw, existingHotspot = null) {
const token = localStorage.getItem('jwt');
if (!token) {
alert('Vui lòng đăng nhập để thực hiện thao tác này.');
return;
}
const modal = document.getElementById('hotspot-modal');
const form = document.getElementById('hotspot-form');
// Reset form và gán tọa độ
form.reset();
switchHSTab('select'); // Luôn mặc định về tab chọn ảnh có sẵn khi mở modal
document.getElementById('hs-pitch').value = pitch;
document.getElementById('hs-yaw').value = yaw;
document.getElementById('hs-id').value = existingHotspot ? existingHotspot._id : '';
document.getElementById('hotspot-modal-title').innerText = existingHotspot ? 'Cập nhật điểm điều hướng' : 'Thêm điểm điều hướng mới';
// Lấy danh sách Scene có sẵn để đổ vào dropdown
try {
const res = await fetch(`${API_BASE_URL}/scenes`, { headers: { 'Authorization': `Bearer ${token}` } });
const scenes = await res.json();
const select = document.getElementById('hs-target-id');
select.innerHTML = '<option value="">-- Chọn một cảnh để liên kết --</option>';
scenes.forEach(s => {
if (s._id !== currentSceneId) { // Không liên kết tới chính nó
select.innerHTML += `<option value="${s._id}">${s.title}</option>`;
}
});
// QUAN TRỌNG: Chỉ điền dữ liệu hotspot cũ SAU KHI dropdown đã được nạp đầy đủ options
if (existingHotspot) {
document.getElementById('hs-title').value = existingHotspot.text || '';
document.getElementById('hs-desc').value = existingHotspot.description || '';
if (existingHotspot.targetSceneId) {
select.value = existingHotspot.targetSceneId;
}
}
} catch (e) { console.error("Lỗi nạp danh sách scene:", e); }
modal.style.display = 'flex';
// Xử lý sự kiện submit form
form.onsubmit = async (e) => {
e.preventDefault();
const formData = new FormData(form);
// Logic: Nếu chọn upload file mới, tạo Scene trước
let finalTargetId = formData.get('targetSceneId');
const file = document.getElementById('hs-panorama-file').files[0];
if (file) {
const sceneData = new FormData();
sceneData.append('panorama', file);
sceneData.append('title', formData.get('title'));
const gpsMode = document.getElementById('hs-gps-mode').value;
if (gpsMode === 'manual') {
sceneData.append('lat', document.getElementById('hs-lat').value);
sceneData.append('lng', document.getElementById('hs-lng').value);
} else if (gpsMode === 'inherit') {
sceneData.append('lat', document.getElementById('modal-lat').value);
sceneData.append('lng', document.getElementById('modal-lng').value);
}
uploadWithProgress(`${API_BASE_URL}/scenes`, 'POST', sceneData, token, 'hs', async (sceneRes) => {
await saveHotspotToDB(pitch, yaw, formData.get('title'), formData.get('description'), sceneRes.scene._id, existingHotspot?._id);
closeHotspotModal();
});
return; // Dừng luồng cũ vì uploadWithProgress đã tiếp quản
}
// Lưu Hotspot
await saveHotspotToDB(pitch, yaw, formData.get('title'), formData.get('description'), finalTargetId, existingHotspot?._id);
modal.style.display = 'none';
};
};
/**
* Đóng Modal biên tập Hotspot
*/
function closeHotspotModal() {
document.getElementById('hotspot-modal').style.display = 'none';
}
/**
* Chuyển đổi giữa tab Chọn ảnh có sẵn và Tải ảnh mới
*/
function switchHSTab(tabName) {
const selectTab = document.getElementById('hs-tab-select');
const uploadTab = document.getElementById('hs-tab-upload');
const btns = document.querySelectorAll('.tab-btn');
btns.forEach(btn => btn.classList.remove('active'));
if (tabName === 'select') {
selectTab.style.display = 'block';
uploadTab.style.display = 'none';
btns[0].classList.add('active');
document.getElementById('hs-panorama-file').value = ''; // Reset file input
} else {
selectTab.style.display = 'none';
uploadTab.style.display = 'block';
btns[1].classList.add('active');
document.getElementById('hs-target-id').value = ''; // Reset select
}
}
/**
* Ẩn/hiện input nhập GPS thủ công
*/
function toggleManualGPS() {
const mode = document.getElementById('hs-gps-mode').value;
const manualDiv = document.getElementById('hs-manual-gps');
manualDiv.style.display = mode === 'manual' ? 'block' : 'none';
}
/**
* Gửi dữ liệu lưu Hotspot lên Backend
*/
async function saveHotspotToDB(pitch, yaw, text, description, targetSceneId, hotspotId) {
const token = localStorage.getItem('jwt');
try {
const response = await fetch(`${API_BASE_URL}/scenes/${currentSceneId}/hotspots`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
hotspotId,
pitch,
yaw,
text,
description,
targetSceneId
})
});
const data = await response.json();
if (!response.ok) throw new Error(data.message || 'Lỗi khi lưu hotspot');
alert('Lưu điểm điều hướng thành công!');
// Refresh lại scene hiện tại để cập nhật viewer
// Chúng ta cần lấy lại thông tin scene để có assetId mới nếu có
const res = await fetch(`${API_BASE_URL}/scenes/${currentSceneId}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const updatedScene = await res.json();
let secureImageUrl = `${API_BASE_URL}/assets/view/${updatedScene.assetId._id}?token=${token}`;
// Buộc nạp lại để cập nhật danh sách hotspot mới
openScene(currentSceneId, updatedScene.privacy, updatedScene.shareToken || '', true);
} catch (error) {
console.error(error);
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);
}
};