Sửa chế độ chia sẻ privacy trực tiếp khi nhấn chuột phải lên scene

This commit is contained in:
2026-06-08 19:56:46 +07:00
parent a2263b9005
commit d1aa2209a7
5 changed files with 564 additions and 16 deletions
+99 -1
View File
@@ -597,9 +597,11 @@ html, body {
#logout-confirm-modal,
#delete-asset-confirm-modal,
#success-modal {
z-index: 5500; /* Cao hơn Dashboard (4500) và Close Button (5000) */
z-index: 5500;
}
#share-member-modal, #share-link-modal { z-index: 6000; }
/* --- Pannellum Custom Hotspot (Callout Bubble) --- */
/* Container chính của hotspot do Pannellum quản lý */
@@ -752,3 +754,99 @@ html, body {
.edit-btn-small { background: #28a745; color: white; }
.delete-btn-small { background: #dc3545; color: white; }
.media-actions button:hover { opacity: 0.8; }
/* --- Edit Metadata Modal (Dark Theme) --- */
#edit-scene-metadata-modal .modal-content {
background: rgba(30, 30, 30, 0.95);
color: #fff;
border: 1px solid rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
max-width: 500px;
}
#edit-scene-metadata-modal .form-group label {
color: #ccc;
}
#edit-scene-metadata-modal input,
#edit-scene-metadata-modal textarea,
#edit-scene-metadata-modal select {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #fff;
}
#edit-mini-map {
height: 200px;
width: 100%;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
/* --- Privacy Settings Enhancements --- */
.privacy-settings-btn {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: white;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 18px;
transition: all 0.2s;
}
.privacy-settings-btn:hover {
background: rgba(0, 123, 255, 0.4);
border-color: #007bff;
}
/* Search Dropdown */
.search-dropdown {
position: absolute;
top: 100%; left: 0; width: 100%;
background: #2a2a2a;
border: 1px solid #444;
border-top: none;
max-height: 200px;
overflow-y: auto;
z-index: 100;
border-radius: 0 0 4px 4px;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
}
.search-item {
padding: 10px 15px;
cursor: pointer;
border-bottom: 1px solid #333;
font-size: 14px;
}
.search-item:hover { background: #3a3a3a; color: #00d4ff; }
/* Shared Users List */
.shared-users-list {
margin-top: 10px;
max-height: 150px;
overflow-y: auto;
background: rgba(0,0,0,0.2);
border-radius: 4px;
padding: 5px;
}
.share-list-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: rgba(255,255,255,0.05);
margin-bottom: 4px;
border-radius: 3px;
font-size: 13px;
}
.remove-share-btn {
color: #ff4d4d;
cursor: pointer;
font-weight: bold;
padding: 0 5px;
}
+109 -1
View File
@@ -182,6 +182,111 @@
</div>
</div>
<!-- Modal for Editing Scene Metadata (No Upload) -->
<div id="edit-scene-metadata-modal" class="modal">
<div class="modal-content">
<span class="close-btn" onclick="closeEditMetadataModal()">&times;</span>
<h2 id="edit-metadata-modal-title">Sửa 3D Scene</h2>
<form id="edit-scene-metadata-form" onsubmit="submitEditScene(event)">
<input type="hidden" id="edit-modal-scene-id">
<div class="form-group">
<label for="edit-modal-title">Tên Scene:</label>
<input type="text" id="edit-modal-title" required placeholder="Tên cảnh quay">
</div>
<div class="form-group">
<label for="edit-modal-description">Mô tả:</label>
<textarea id="edit-modal-description" rows="3" placeholder="Mô tả chi tiết về cảnh này..."></textarea>
</div>
<div class="form-group">
<label>Vị trí tọa độ (Click bản đồ để đổi):</label>
<div style="display: flex; gap: 10px; margin-bottom: 10px;">
<input type="text" id="edit-modal-lat" readonly>
<input type="text" id="edit-modal-lng" readonly>
</div>
<div id="edit-mini-map" style="height: 200px; width: 100%; border-radius: 4px; border: 1px solid #ccc; background: #eee;"></div>
</div>
<div class="form-group" id="edit-privacy-container">
<label for="edit-modal-privacy">Quyền riêng tư:</label>
<div style="display: flex; gap: 8px; align-items: center;">
<select id="edit-modal-privacy" onchange="handleEditPrivacyChange()" style="flex: 1;">
<option value="public">Public (Everyone)</option>
<option value="private">Private (Only Me)</option>
<option value="member">Members (Specific People)</option>
<option value="shared">Shared via Link</option>
</select>
<button type="button" id="btn-edit-privacy-settings" class="privacy-settings-btn" title="Cài đặt chia sẻ" onclick="openPrivacySettingsModal()" style="display: none;">⚙️</button>
</div>
<div id="edit-child-privacy-info" style="display: none; color: #888; font-size: 12px; margin-top: 5px;">
️ Cảnh con kế thừa quyền riêng tư từ cảnh cha.
</div>
</div>
<div class="modal-footer" style="padding: 0; border: none; background: transparent;">
<button type="button" class="cancel-btn" onclick="closeEditMetadataModal()">Hủy bỏ</button>
<button type="submit" class="save-btn">Lưu thay đổi</button>
</div>
</form>
</div>
</div>
<!-- Modal Manage Shared Members -->
<div id="share-member-modal" class="modal-overlay">
<div class="modal-content logout-modal-dark" style="max-width: 500px;">
<h2 style="color: #fff; margin-bottom: 15px;">Chia sẻ với thành viên</h2>
<div class="form-group">
<label>Thêm thành viên (Username hoặc Email):</label>
<div style="position: relative;">
<input type="text" id="share-user-search" placeholder="Nhập tên hoặc email..." oninput="searchUsersToShare(this.value)">
<div id="search-results-dropdown" class="search-dropdown" style="display: none;"></div>
</div>
</div>
<div class="shared-list-container">
<label>Danh sách đã chia sẻ:</label>
<div id="current-shared-list" class="shared-users-list">
<!-- Sẽ được fill bằng JS -->
</div>
</div>
<div class="modal-footer" style="padding: 20px 0 0 0; background: transparent; border: none;">
<button onclick="closeShareMemberModal()" class="save-btn">Xong</button>
</div>
</div>
</div>
<!-- Modal Shared Link -->
<div id="share-link-modal" class="modal-overlay">
<div class="modal-content logout-modal-dark" style="max-width: 500px; text-align: center;">
<h2 style="color: #fff; margin-bottom: 15px;">Liên kết chia sẻ</h2>
<p style="color: #ccc; font-size: 14px; margin-bottom: 20px;">Bất kỳ ai có liên kết này đều có thể xem cảnh quay mà không cần đăng nhập.</p>
<div class="form-group" style="text-align: left; margin-bottom: 20px;">
<label style="color: #fff;">Thời hạn liên kết:</label>
<select id="share-link-expire" style="background: #222; color: #fff; border: 1px solid #444;">
<option value="7">7 ngày (Mặc định)</option>
<option value="1">1 ngày</option>
<option value="30">30 ngày</option>
<option value="never">Vĩnh viễn</option>
</select>
</div>
<div class="link-display-area">
<input type="text" id="shared-link-input" readonly style="background: rgba(0,0,0,0.3); color: #00d4ff; border: 1px solid #444; padding: 12px; width: 100%; border-radius: 4px; font-family: monospace; font-size: 13px;">
</div>
<div class="action-buttons" style="margin-top: 25px;">
<button onclick="copySharedLink()" class="edit-btn-large" style="background: #007bff; width: 100%;">
📋 Sao chép liên kết & Đóng
</button>
<button onclick="closeShareLinkModal()" class="edit-btn-large" style="background: #444; width: 100%; margin-top: 10px;">
Đóng
</button>
</div>
</div>
</div>
<!-- Delete Asset Confirmation Modal -->
<div id="delete-asset-confirm-modal" class="modal-overlay">
<div class="modal-content action-modal-content logout-modal-dark">
@@ -210,8 +315,11 @@
<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-privacy-action" class="edit-btn-large" style="background: #6f42c1;">
<span class="icon">🔒</span> Chỉnh sửa privacy
</button>
<button id="btn-edit-action" class="edit-btn-large">
<span class="icon">✏️</span> Chỉnh sửa thông tin
<span class="icon">✏️</span> Chế độ sửa scene
</button>
<button id="btn-delete-action" class="delete-btn-large">
<span class="icon">🗑️</span> Xóa vĩnh viễn
+272 -6
View File
@@ -10,11 +10,22 @@ let miniMapMarker = null;
let systemSettings = { timezone: 'Asia/Ho_Chi_Minh', language: 'vi' };
let returnToDashboardAfterEdit = false;
let assetIdToDelete = null;
let editMiniMap = null;
let editMiniMapMarker = null;
let currentEditingScene = null; // Lưu object scene đang sửa để quản lý chia sẻ
let sharedUsersData = []; // [{id, username, email}]
let sharedEmailsData = []; // [email]
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
try {
console.log("--- Bắt đầu khởi tạo Frontend ---");
// 0. Kiểm tra tham số URL để truy cập trực tiếp
const urlParams = new URLSearchParams(window.location.search);
const urlSceneId = urlParams.get('sceneId');
const urlToken = urlParams.get('token');
// Ưu tiên nạp cấu hình hệ thống trước
fetchSystemSettings();
@@ -26,8 +37,13 @@ document.addEventListener('DOMContentLoaded', () => {
// Chạy tuần tự để tránh xung đột luồng xử lý
checkAuthStatus(); // 2. Kiểm tra đăng nhập
// 3. Khôi phục cảnh đang xem nếu có (sau khi người dùng reload trang)
restoreActiveScene();
// 3. Xử lý logic vào thẳng Scene hoặc khôi phục trang
if (urlSceneId) {
console.log(`[Direct Access] Opening scene ${urlSceneId} from URL`);
openScene(urlSceneId, urlToken ? 'shared' : null, urlToken);
} else {
restoreActiveScene();
}
// Đảm bảo map đã sẵn sàng trước khi nạp data
if (map) {
@@ -662,16 +678,25 @@ 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 editPrivacyBtn = document.getElementById('btn-edit-privacy-action');
const deleteBtn = document.getElementById('btn-delete-action');
title.innerText = `Scene: ${scene.title}`;
modal.style.display = 'flex';
// Hành động Chỉnh sửa privacy
editPrivacyBtn.onclick = () => {
returnToDashboardAfterEdit = false;
closeActionModal();
// Mở modal metadata, false vì ảnh trên map luôn là ảnh mẹ (không phải child)
openEditMetadataModal(scene, false);
};
// Gán sự kiện cho nút Sửa
editBtn.onclick = () => {
returnToDashboardAfterEdit = false;
closeActionModal();
openEditSceneModal(scene);
openEditMetadataModal(scene, false);
};
// Gán sự kiện cho nút Xóa
@@ -805,9 +830,22 @@ async function openScene(sceneId, privacy, shareToken, force = false, initialPit
initPanoramaViewer(secureImageUrl, hotspots || [], sceneOwnerId, initialPitch, initialYaw);
} catch (error) {
if (typeof closeViewer === 'function') closeViewer();
localStorage.removeItem('activeSceneId');
localStorage.removeItem('activeScenePrivacy');
localStorage.removeItem('activeSceneToken');
// Kiểm tra nếu đang truy cập qua link trực tiếp (URL có sceneId) mà gặp lỗi (do xóa token hoặc token không hợp lệ)
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('sceneId')) {
alert("Bạn không có quyền truy cập hoặc liên kết chia sẻ đã hết hạn. Quay về bản đồ công cộng.");
// Xóa toàn bộ tham số URL và tải lại trang để làm mới trạng thái (về trang chủ dành cho khách)
window.history.replaceState({}, document.title, window.location.pathname);
location.reload();
return;
}
alert(error.message);
}
}
@@ -1275,7 +1313,8 @@ async function loadMyAssets() {
const editButton = document.createElement('button');
editButton.className = 'edit-btn-small';
editButton.innerText = 'Sửa Scene';
editButton.addEventListener('click', () => openEditFromMedia(scene)); // Pass scene object directly
const isChild = asset.parentScenes && asset.parentScenes.length > 0;
editButton.addEventListener('click', () => openEditFromMedia(scene, isChild));
card.querySelector('.media-actions').appendChild(editButton);
}
@@ -1362,16 +1401,243 @@ window.closeSuccessModal = function(e) {
modal.style.display = 'none';
};
window.openEditFromMedia = function(scene) {
window.openEditFromMedia = function(scene, isChild = false) {
if (!scene || !scene._id) {
alert("Không thể chỉnh sửa: Ảnh này không được gắn với một Scene hợp lệ.");
return;
}
returnToDashboardAfterEdit = true;
closeDashboard();
openEditSceneModal(scene);
openEditMetadataModal(scene, isChild);
};
/**
* Mở Modal sửa thông tin Metadata chuyên biệt
*/
window.openEditMetadataModal = function(scene, isChild = false) {
currentEditingScene = scene; // Lưu lại để dùng cho chia sẻ
// Load dữ liệu chia sẻ hiện tại
sharedUsersData = scene.sharedWith || [];
sharedEmailsData = scene.sharedEmails || [];
document.getElementById('edit-modal-scene-id').value = scene._id;
document.getElementById('edit-modal-title').value = scene.name || scene.title || '';
document.getElementById('edit-modal-description').value = scene.description || '';
const lat = scene.gps?.lat || scene.lat;
const lng = scene.gps?.lng || scene.lng;
document.getElementById('edit-modal-lat').value = lat;
document.getElementById('edit-modal-lng').value = lng;
// Xử lý logic Privacy cho Cảnh con
const privacySelect = document.getElementById('edit-modal-privacy');
const childInfo = document.getElementById('edit-child-privacy-info');
if (isChild) {
privacySelect.value = scene.privacy;
privacySelect.disabled = true;
childInfo.style.display = 'block';
} else {
privacySelect.value = scene.privacy;
privacySelect.disabled = false;
childInfo.style.display = 'none';
}
handleEditPrivacyChange(); // Cập nhật hiển thị nút bánh răng
document.getElementById('edit-scene-metadata-modal').style.display = 'flex';
// Khởi tạo Mini Map tại vị trí hiện tại của Scene
setTimeout(() => initEditSceneMiniMap(lat, lng), 100);
};
function closeEditMetadataModal() {
document.getElementById('edit-scene-metadata-modal').style.display = 'none';
if (returnToDashboardAfterEdit) {
returnToDashboardAfterEdit = false;
openDashboard();
openDashboardTab('media-library');
}
}
function initEditSceneMiniMap(lat, lng) {
if (editMiniMap) {
editMiniMap.setView([lat, lng], 16);
if (editMiniMapMarker) editMiniMapMarker.setLatLng([lat, lng]);
editMiniMap.invalidateSize();
return;
}
editMiniMap = L.map('edit-mini-map').setView([lat, lng], 16);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(editMiniMap);
editMiniMapMarker = L.marker([lat, lng], { draggable: true }).addTo(editMiniMap);
editMiniMap.on('click', (e) => {
const { lat, lng } = e.latlng;
editMiniMapMarker.setLatLng([lat, lng]);
document.getElementById('edit-modal-lat').value = lat.toFixed(6);
document.getElementById('edit-modal-lng').value = lng.toFixed(6);
});
}
/**
* Xử lý khi thay đổi Dropdown Privacy trong Modal sửa
*/
window.handleEditPrivacyChange = function() {
const privacy = document.getElementById('edit-modal-privacy').value;
const settingsBtn = document.getElementById('btn-edit-privacy-settings');
const isChild = document.getElementById('edit-modal-privacy').disabled;
if (!isChild && (privacy === 'member' || privacy === 'shared')) {
settingsBtn.style.display = 'block';
} else {
settingsBtn.style.display = 'none';
}
};
/**
* Mở modal cài đặt chi tiết dựa trên loại Privacy
*/
window.openPrivacySettingsModal = function() {
const privacy = document.getElementById('edit-modal-privacy').value;
if (privacy === 'member') {
renderSharedList();
document.getElementById('share-member-modal').style.display = 'flex';
} else if (privacy === 'shared') {
const baseUrl = window.location.origin + window.location.pathname;
const token = currentEditingScene.shareToken || 'đang_tạo_mới...';
document.getElementById('shared-link-input').value = `${baseUrl}?sceneId=${currentEditingScene._id}&token=${token}`;
document.getElementById('share-link-modal').style.display = 'flex';
}
};
/**
* Tìm kiếm người dùng để chia sẻ
*/
window.searchUsersToShare = async function(query) {
const dropdown = document.getElementById('search-results-dropdown');
if (!query || query.length < 2) {
dropdown.style.display = 'none';
return;
}
const token = localStorage.getItem('jwt');
try {
const res = await fetch(`${API_BASE_URL}/users/search?q=${encodeURIComponent(query)}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const users = await res.json();
dropdown.innerHTML = '';
if (users.length > 0) {
users.forEach(user => {
const item = document.createElement('div');
item.className = 'search-item';
item.innerText = `${user.username} (${user.email})`;
item.onclick = () => addMemberToShare(user);
dropdown.appendChild(item);
});
dropdown.style.display = 'block';
} else {
// Nếu không tìm thấy user, cho phép thêm email thủ công
if (query.includes('@')) {
dropdown.innerHTML = `<div class="search-item" onclick="addEmailToShare('${query}')">Thêm email: ${query}</div>`;
dropdown.style.display = 'block';
} else {
dropdown.style.display = 'none';
}
}
} catch (e) { console.error(e); }
};
function addMemberToShare(user) {
if (!sharedUsersData.some(u => (u._id || u) === user._id)) {
sharedUsersData.push(user);
renderSharedList();
}
document.getElementById('share-user-search').value = '';
document.getElementById('search-results-dropdown').style.display = 'none';
}
window.addEmailToShare = function(email) {
if (!sharedEmailsData.includes(email)) {
sharedEmailsData.push(email);
renderSharedList();
}
document.getElementById('share-user-search').value = '';
document.getElementById('search-results-dropdown').style.display = 'none';
};
function renderSharedList() {
const list = document.getElementById('current-shared-list');
list.innerHTML = '';
sharedUsersData.forEach(user => {
const name = user.username || 'User';
list.innerHTML += `<div class="share-list-item">👤 ${name} <span class="remove-share-btn" onclick="removeShared('user', '${user._id || user}')">&times;</span></div>`;
});
sharedEmailsData.forEach(email => {
list.innerHTML += `<div class="share-list-item">📧 ${email} <span class="remove-share-btn" onclick="removeShared('email', '${email}')">&times;</span></div>`;
});
}
window.removeShared = function(type, id) {
if (type === 'user') sharedUsersData = sharedUsersData.filter(u => (u._id || u) !== id);
else sharedEmailsData = sharedEmailsData.filter(e => e !== id);
renderSharedList();
};
window.closeShareMemberModal = () => document.getElementById('share-member-modal').style.display = 'none';
window.closeShareLinkModal = () => document.getElementById('share-link-modal').style.display = 'none';
/**
* Copy link chia sẻ và đóng modal
*/
window.copySharedLink = function() {
const linkInput = document.getElementById('shared-link-input');
linkInput.select();
navigator.clipboard.writeText(linkInput.value).then(() => {
showSuccessModal("Đã sao chép liên kết vào bộ nhớ!");
closeShareLinkModal();
});
};
async function submitEditScene(e) {
e.preventDefault();
const id = document.getElementById('edit-modal-scene-id').value;
const token = localStorage.getItem('jwt');
// Sử dụng FormData vì API Backend hiện tại đang dùng Multer
const formData = new FormData();
formData.append('title', document.getElementById('edit-modal-title').value);
formData.append('description', document.getElementById('edit-modal-description').value);
formData.append('lat', document.getElementById('edit-modal-lat').value);
formData.append('lng', document.getElementById('edit-modal-lng').value);
formData.append('privacy', document.getElementById('edit-modal-privacy').value);
formData.append('shareExpireDays', document.getElementById('share-link-expire').value);
// Đính kèm dữ liệu chia sẻ nâng cao
formData.append('sharedWithUsers', JSON.stringify(sharedUsersData.map(u => u._id || u)));
formData.append('sharedEmails', JSON.stringify(sharedEmailsData));
try {
const res = await fetch(`${API_BASE_URL}/scenes/${id}`, {
method: 'PUT',
headers: { 'Authorization': `Bearer ${token}` },
body: formData
});
const data = await res.json();
if (!res.ok) throw new Error(data.message);
showSuccessModal("Đã cập nhật thông tin cảnh thành công!");
closeEditMetadataModal();
loadScenes();
} catch (err) {
alert("Lỗi cập nhật: " + err.message);
}
}
/**
* Opens a specific tab within the dashboard.
* @param {string} tabName - The ID of the tab pane to open (e.g., 'profile', 'my-scenes').