Sửa callout của hotspot hiển thị thumbnail và tooltip

This commit is contained in:
2026-06-08 17:35:40 +07:00
parent 3dbf2f2bbf
commit 306d95009f
5 changed files with 170 additions and 13 deletions
+8 -1
View File
@@ -246,7 +246,14 @@ router.get('/scenes/:id', optionalAuth, async (req, res) => {
*/ */
router.get('/hotspots/:scene_id', async (req, res) => { router.get('/hotspots/:scene_id', async (req, res) => {
try { try {
const hotspots = await Hotspot.find({ parent_scene_id: req.params.scene_id }); const hotspots = await Hotspot.find({ parent_scene_id: req.params.scene_id })
.populate({
path: 'target_scene_id',
select: 'name title assetId privacy shareToken',
populate: { path: 'assetId', select: '_id' }
})
.lean();
res.json(hotspots); res.json(hotspots);
} catch (error) { } catch (error) {
res.status(500).json({ message: error.message }); res.status(500).json({ message: error.message });
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

+92
View File
@@ -597,3 +597,95 @@ html, body {
#logout-confirm-modal { #logout-confirm-modal {
z-index: 5500; /* Cao hơn Dashboard (4500) và Close Button (5000) */ z-index: 5500; /* Cao hơn Dashboard (4500) và Close Button (5000) */
} }
/* --- Pannellum Custom Hotspot (Callout Bubble) --- */
/* Container chính của hotspot do Pannellum quản lý */
.pnlm-custom-hotspot {
width: 64px;
height: 64px;
/* Căn chỉnh để tâm bong bóng nằm đúng vị trí tọa độ pitch/yaw */
margin-left: -32px;
margin-top: -32px;
z-index: 10;
/* Xóa bỏ icon sprite mặc định của Pannellum */
background: none !important;
}
.pnlm-custom-hotspot * {
box-sizing: border-box;
}
/* Bong bóng callout */
.hotspot-callout-bubble {
position: relative;
width: 100%;
height: 100%;
cursor: pointer;
transition: transform 0.2s ease-in-out;
}
.hotspot-callout-bubble:hover {
transform: scale(1.15);
z-index: 1000;
}
/* Khung chứa ảnh thumbnail tròn */
.hotspot-thumb-frame {
width: 100%;
height: 100%;
border: 3px solid #ffffff;
border-radius: 50%;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
background-color: #333;
display: flex;
align-items: center;
justify-content: center;
}
.hotspot-thumb-frame img {
width: 100%;
height: 100%;
object-fit: cover;
}
/* Đuôi nhọn của bong bóng trỏ xuống vị trí chính xác */
.hotspot-callout-bubble::after {
content: '';
position: absolute;
bottom: -10px;
left: 50%;
transform: translateX(-50%);
border-width: 10px 7px 0;
border-style: solid;
border-color: #ffffff transparent transparent;
}
/* Tooltip tiêu đề xuất hiện khi hover */
.hotspot-callout-title {
position: absolute;
top: -45px; /* Hiển thị phía trên bong bóng */
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.85);
color: #fff;
padding: 5px 12px;
border-radius: 20px;
font-size: 13px;
font-weight: 500;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
pointer-events: none;
z-index: 100;
}
/* Hiệu ứng khi rê chuột qua hotspot */
.hotspot-callout-bubble:hover .hotspot-callout-title {
opacity: 1;
visibility: visible;
top: -55px; /* Nhích lên một chút khi xuất hiện */
}
+1
View File
@@ -745,6 +745,7 @@ async function openScene(sceneId, privacy, shareToken, force = false) {
const scene = await sceneRes.json(); const scene = await sceneRes.json();
const hotspots = await hotspotsRes.json(); const hotspots = await hotspotsRes.json();
console.log("DEBUG: Hotspots raw data from API:", hotspots);
if (!sceneRes.ok) throw new Error(scene.message || 'Failed to fetch scene details'); if (!sceneRes.ok) throw new Error(scene.message || 'Failed to fetch scene details');
+64 -7
View File
@@ -3,6 +3,40 @@ let currentHotspots = [];
let securityApplied = false; let securityApplied = false;
let currentSceneOwnerId = null; let currentSceneOwnerId = null;
/**
* Hàm render tùy chỉnh cho hotspot để hiển thị bong bóng callout kèm ảnh thumbnail.
* Thay thế icon mặc định của Pannellum bằng cấu trúc HTML mới.
*/
function renderCustomHotspot(hotSpotDiv, args) {
// DỌN DẸP: Xóa sạch các ký tự mặc định (+ - []) của Pannellum
hotSpotDiv.innerHTML = '';
hotSpotDiv.classList.add('pnlm-custom-hotspot');
// Tạo container chính cho bong bóng
const callout = document.createElement('div');
callout.className = 'hotspot-callout-bubble';
// Tạo khung ảnh thumbnail (Luôn tạo để tránh bong bóng bị rỗng)
const imgWrapper = document.createElement('div');
imgWrapper.className = 'hotspot-thumb-frame';
if (args.thumbUrl) {
const img = document.createElement('img');
img.src = args.thumbUrl;
imgWrapper.appendChild(img);
}
callout.appendChild(imgWrapper);
// Thêm nhãn tiêu đề (tên cảnh đích)
const title = document.createElement('div');
title.className = 'hotspot-callout-title';
title.innerText = args.title;
callout.appendChild(title);
hotSpotDiv.appendChild(callout);
}
/** /**
* Initializes and shows the Pannellum 360° panorama viewer with security overlays. * Initializes and shows the Pannellum 360° panorama viewer with security overlays.
* @param {string} imageUrl - Authorized URL to fetch the secure image stream * @param {string} imageUrl - Authorized URL to fetch the secure image stream
@@ -22,19 +56,42 @@ function initPanoramaViewer(imageUrl, hotspots = [], ownerId = null) {
} }
// Chuyển đổi dữ liệu hotspots từ DB sang định dạng Pannellum // Chuyển đổi dữ liệu hotspots từ DB sang định dạng Pannellum
const pannellumHotspots = hotspots.map(h => ({ const token = localStorage.getItem('jwt');
const pannellumHotspots = hotspots.map(h => {
const target = h.target_scene_id;
let thumbUrl = '';
// Kiểm tra target phải là Object đã được populate thành công
if (target && typeof target === 'object' && target.assetId) {
const assetId = target.assetId._id || target.assetId;
thumbUrl = `/api/assets/view/${assetId}`;
// Đính kèm token để vượt qua kiểm tra quyền truy cập của middleware
if (token) thumbUrl += `?token=${token}`;
else if (target.privacy === 'shared' && target.shareToken) thumbUrl += `?token=${target.shareToken}`;
}
return {
pitch: h.coordinates?.pitch || h.pitch, pitch: h.coordinates?.pitch || h.pitch,
yaw: h.coordinates?.yaw || h.yaw, yaw: h.coordinates?.yaw || h.yaw,
type: "info", type: "custom",
text: h.title || "Điểm điều hướng", createTooltipFunc: renderCustomHotspot,
createTooltipArgs: {
title: h.title || target?.name || target?.title || "Điểm điều hướng",
thumbUrl: thumbUrl
},
id: h._id, id: h._id,
clickHandlerFunc: () => { clickHandlerFunc: () => {
if (h.target_scene_id || h.targetSceneId) { if (target) {
// Gọi hàm openScene từ main_map.js const targetId = target._id || target;
openScene(h.target_scene_id || h.targetSceneId); const privacy = target.privacy || '';
const shareToken = target.shareToken || '';
// Chuyển cảnh với đầy đủ thông tin bảo mật
openScene(targetId, privacy, shareToken);
} }
} }
})); };
});
// Initialize Pannellum Equirectangular viewer // Initialize Pannellum Equirectangular viewer
activeViewer = pannellum.viewer('panorama-viewer', { activeViewer = pannellum.viewer('panorama-viewer', {