Sửa callout của hotspot hiển thị thumbnail và tooltip
This commit is contained in:
@@ -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 |
@@ -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 */
|
||||||
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|
||||||
|
|||||||
+69
-12
@@ -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');
|
||||||
pitch: h.coordinates?.pitch || h.pitch,
|
const pannellumHotspots = hotspots.map(h => {
|
||||||
yaw: h.coordinates?.yaw || h.yaw,
|
const target = h.target_scene_id;
|
||||||
type: "info",
|
let thumbUrl = '';
|
||||||
text: h.title || "Điểm điều hướng",
|
|
||||||
id: h._id,
|
// Kiểm tra target phải là Object đã được populate thành công
|
||||||
clickHandlerFunc: () => {
|
if (target && typeof target === 'object' && target.assetId) {
|
||||||
if (h.target_scene_id || h.targetSceneId) {
|
const assetId = target.assetId._id || target.assetId;
|
||||||
// Gọi hàm openScene từ main_map.js
|
thumbUrl = `/api/assets/view/${assetId}`;
|
||||||
openScene(h.target_scene_id || h.targetSceneId);
|
|
||||||
}
|
// Đí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,
|
||||||
|
yaw: h.coordinates?.yaw || h.yaw,
|
||||||
|
type: "custom",
|
||||||
|
createTooltipFunc: renderCustomHotspot,
|
||||||
|
createTooltipArgs: {
|
||||||
|
title: h.title || target?.name || target?.title || "Điểm điều hướng",
|
||||||
|
thumbUrl: thumbUrl
|
||||||
|
},
|
||||||
|
id: h._id,
|
||||||
|
clickHandlerFunc: () => {
|
||||||
|
if (target) {
|
||||||
|
const targetId = target._id || target;
|
||||||
|
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', {
|
||||||
|
|||||||
Reference in New Issue
Block a user