fix: redesign earth hud interactions and legend behavior
This commit is contained in:
@@ -6,6 +6,7 @@ import { CONFIG, CABLE_COLORS, PATHS, CABLE_STATE } from "./constants.js";
|
||||
import { latLonToVector3 } from "./utils.js";
|
||||
import { updateEarthStats, showStatusMessage } from "./ui.js";
|
||||
import { showInfoCard } from "./info-card.js";
|
||||
import { setLegendMode } from "./legend.js";
|
||||
|
||||
export let cableLines = [];
|
||||
export let landingPoints = [];
|
||||
@@ -383,6 +384,7 @@ export function handleCableClick(cable) {
|
||||
lockedCable = cable;
|
||||
|
||||
const data = cable.userData;
|
||||
setLegendMode("cables");
|
||||
showInfoCard("cable", {
|
||||
name: data.name,
|
||||
owner: data.owner,
|
||||
|
||||
135
frontend/public/earth/js/controls.js
vendored
135
frontend/public/earth/js/controls.js
vendored
@@ -16,6 +16,7 @@ export let autoRotate = true;
|
||||
export let zoomLevel = 1.0;
|
||||
export let showTerrain = false;
|
||||
export let isDragging = false;
|
||||
export let layoutExpanded = false;
|
||||
|
||||
let earthObj = null;
|
||||
let listeners = [];
|
||||
@@ -43,6 +44,7 @@ export function setupControls(camera, renderer, scene, earth) {
|
||||
setupWheelZoom(camera, renderer);
|
||||
setupRotateControls(camera, earth);
|
||||
setupTerrainControls();
|
||||
setupLiquidGlassInteractions();
|
||||
}
|
||||
|
||||
function setupZoomControls(camera) {
|
||||
@@ -285,13 +287,18 @@ function setupRotateControls(camera) {
|
||||
}
|
||||
|
||||
function setupTerrainControls() {
|
||||
const container = document.getElementById("container");
|
||||
const searchBtn = document.getElementById("search-action");
|
||||
const infoGroup = document.getElementById("info-control-group");
|
||||
const infoTrigger = document.getElementById("info-trigger");
|
||||
const terrainBtn = document.getElementById("toggle-terrain");
|
||||
const satellitesBtn = document.getElementById("toggle-satellites");
|
||||
const trailsBtn = document.getElementById("toggle-trails");
|
||||
const cablesBtn = document.getElementById("toggle-cables");
|
||||
const layoutBtn = document.getElementById("layout-toggle");
|
||||
const reloadBtn = document.getElementById("reload-data");
|
||||
const toolbarToggle = document.getElementById("toolbar-toggle");
|
||||
const toolbar = document.getElementById("control-toolbar");
|
||||
const zoomGroup = document.getElementById("zoom-control-group");
|
||||
const zoomTrigger = document.getElementById("zoom-trigger");
|
||||
|
||||
if (trailsBtn) {
|
||||
trailsBtn.classList.add("active");
|
||||
@@ -299,6 +306,10 @@ function setupTerrainControls() {
|
||||
if (tooltip) tooltip.textContent = "隐藏轨迹";
|
||||
}
|
||||
|
||||
bindListener(searchBtn, "click", () => {
|
||||
showStatusMessage("搜索功能待开发", "info");
|
||||
});
|
||||
|
||||
bindListener(terrainBtn, "click", function () {
|
||||
showTerrain = !showTerrain;
|
||||
toggleTerrain(showTerrain);
|
||||
@@ -352,11 +363,102 @@ function setupTerrainControls() {
|
||||
await reloadData();
|
||||
});
|
||||
|
||||
if (toolbarToggle && toolbar) {
|
||||
bindListener(toolbarToggle, "click", () => {
|
||||
toolbar.classList.toggle("collapsed");
|
||||
bindListener(zoomTrigger, "click", (event) => {
|
||||
event.stopPropagation();
|
||||
infoGroup?.classList.remove("open");
|
||||
zoomGroup?.classList.toggle("open");
|
||||
});
|
||||
|
||||
bindListener(zoomGroup, "click", (event) => {
|
||||
event.stopPropagation();
|
||||
});
|
||||
|
||||
bindListener(infoTrigger, "click", (event) => {
|
||||
event.stopPropagation();
|
||||
zoomGroup?.classList.remove("open");
|
||||
infoGroup?.classList.toggle("open");
|
||||
});
|
||||
|
||||
bindListener(infoGroup, "click", (event) => {
|
||||
event.stopPropagation();
|
||||
});
|
||||
|
||||
bindListener(document, "click", (event) => {
|
||||
if (zoomGroup?.classList.contains("open")) {
|
||||
if (!zoomGroup.contains(event.target)) {
|
||||
zoomGroup.classList.remove("open");
|
||||
}
|
||||
}
|
||||
if (infoGroup?.classList.contains("open")) {
|
||||
if (!infoGroup.contains(event.target)) {
|
||||
infoGroup.classList.remove("open");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
bindListener(layoutBtn, "click", () => {
|
||||
const expanded = toggleLayoutExpanded(container);
|
||||
showStatusMessage(expanded ? "布局已最大化" : "布局已恢复", "info");
|
||||
});
|
||||
|
||||
updateLayoutUI(container);
|
||||
}
|
||||
|
||||
function setupLiquidGlassInteractions() {
|
||||
const surfaces = document.querySelectorAll(".liquid-glass-surface");
|
||||
|
||||
const resetSurface = (surface) => {
|
||||
surface.style.setProperty("--elastic-x", "0px");
|
||||
surface.style.setProperty("--elastic-y", "0px");
|
||||
surface.style.setProperty("--tilt-x", "0deg");
|
||||
surface.style.setProperty("--tilt-y", "0deg");
|
||||
surface.style.setProperty("--glow-x", "50%");
|
||||
surface.style.setProperty("--glow-y", "22%");
|
||||
surface.style.setProperty("--glow-opacity", "0.24");
|
||||
surface.classList.remove("is-pressed");
|
||||
};
|
||||
|
||||
surfaces.forEach((surface) => {
|
||||
resetSurface(surface);
|
||||
|
||||
bindListener(surface, "pointermove", (event) => {
|
||||
const rect = surface.getBoundingClientRect();
|
||||
const px = (event.clientX - rect.left) / rect.width;
|
||||
const py = (event.clientY - rect.top) / rect.height;
|
||||
const offsetX = (px - 0.5) * 6;
|
||||
const offsetY = (py - 0.5) * 6;
|
||||
const tiltX = (0.5 - py) * 8;
|
||||
const tiltY = (px - 0.5) * 10;
|
||||
|
||||
surface.style.setProperty("--elastic-x", `${offsetX.toFixed(2)}px`);
|
||||
surface.style.setProperty("--elastic-y", `${offsetY.toFixed(2)}px`);
|
||||
surface.style.setProperty("--tilt-x", `${tiltX.toFixed(2)}deg`);
|
||||
surface.style.setProperty("--tilt-y", `${tiltY.toFixed(2)}deg`);
|
||||
surface.style.setProperty("--glow-x", `${(px * 100).toFixed(1)}%`);
|
||||
surface.style.setProperty("--glow-y", `${(py * 100).toFixed(1)}%`);
|
||||
surface.style.setProperty("--glow-opacity", "0.34");
|
||||
});
|
||||
}
|
||||
|
||||
bindListener(surface, "pointerenter", () => {
|
||||
surface.style.setProperty("--glow-opacity", "0.28");
|
||||
});
|
||||
|
||||
bindListener(surface, "pointerleave", () => {
|
||||
resetSurface(surface);
|
||||
});
|
||||
|
||||
bindListener(surface, "pointerdown", () => {
|
||||
surface.classList.add("is-pressed");
|
||||
});
|
||||
|
||||
bindListener(surface, "pointerup", () => {
|
||||
surface.classList.remove("is-pressed");
|
||||
});
|
||||
|
||||
bindListener(surface, "pointercancel", () => {
|
||||
resetSurface(surface);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function teardownControls() {
|
||||
@@ -396,3 +498,24 @@ export function getZoomLevel() {
|
||||
export function getShowTerrain() {
|
||||
return showTerrain;
|
||||
}
|
||||
|
||||
function updateLayoutUI(container) {
|
||||
if (container) {
|
||||
container.classList.toggle("layout-expanded", layoutExpanded);
|
||||
}
|
||||
|
||||
const btn = document.getElementById("layout-toggle");
|
||||
if (btn) {
|
||||
btn.classList.toggle("active", layoutExpanded);
|
||||
const tooltip = btn.querySelector(".tooltip");
|
||||
const nextLabel = layoutExpanded ? "恢复布局" : "最大化布局";
|
||||
btn.title = nextLabel;
|
||||
if (tooltip) tooltip.textContent = nextLabel;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleLayoutExpanded(container) {
|
||||
layoutExpanded = !layoutExpanded;
|
||||
updateLayoutUI(container);
|
||||
return layoutExpanded;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// info-card.js - Unified info card module
|
||||
import { showStatusMessage } from './ui.js';
|
||||
|
||||
let currentType = null;
|
||||
|
||||
@@ -55,7 +56,32 @@ const CARD_CONFIG = {
|
||||
};
|
||||
|
||||
export function initInfoCard() {
|
||||
// Close button removed - now uses external clear button
|
||||
const content = document.getElementById('info-card-content');
|
||||
if (!content || content.dataset.copyBound === 'true') return;
|
||||
|
||||
content.addEventListener('click', async (event) => {
|
||||
const label = event.target.closest('.info-card-label');
|
||||
if (!label) return;
|
||||
|
||||
const property = label.closest('.info-card-property');
|
||||
const valueEl = property?.querySelector('.info-card-value');
|
||||
const value = valueEl?.textContent?.trim();
|
||||
|
||||
if (!value || value === '-') {
|
||||
showStatusMessage('无可复制内容', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
showStatusMessage(`已复制${label.textContent}:${value}`, 'success');
|
||||
} catch (error) {
|
||||
console.error('Copy failed:', error);
|
||||
showStatusMessage('复制失败', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
content.dataset.copyBound = 'true';
|
||||
}
|
||||
|
||||
export function setInfoCardNoBorder(noBorder = true) {
|
||||
|
||||
59
frontend/public/earth/js/legend.js
Normal file
59
frontend/public/earth/js/legend.js
Normal file
@@ -0,0 +1,59 @@
|
||||
const LEGEND_MODES = {
|
||||
cables: {
|
||||
title: "线缆图例",
|
||||
items: [
|
||||
{ color: "#ff4444", label: "Americas II" },
|
||||
{ color: "#44ff44", label: "AU Aleutian A" },
|
||||
{ color: "#4444ff", label: "AU Aleutian B" },
|
||||
{ color: "#ffff44", label: "其他电缆" },
|
||||
],
|
||||
},
|
||||
satellites: {
|
||||
title: "卫星图例",
|
||||
items: [
|
||||
{ color: "#4db8ff", label: "卫星本体" },
|
||||
{ color: "#9be7ff", label: "卫星轨迹" },
|
||||
{ color: "#7dffb3", label: "悬停高亮" },
|
||||
{ color: "#ffd166", label: "选中目标" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
let currentLegendMode = "cables";
|
||||
|
||||
export function initLegend() {
|
||||
renderLegend(currentLegendMode);
|
||||
}
|
||||
|
||||
export function setLegendMode(mode) {
|
||||
const nextMode = LEGEND_MODES[mode] ? mode : "cables";
|
||||
if (nextMode === currentLegendMode) return;
|
||||
currentLegendMode = nextMode;
|
||||
renderLegend(currentLegendMode);
|
||||
}
|
||||
|
||||
export function getLegendMode() {
|
||||
return currentLegendMode;
|
||||
}
|
||||
|
||||
function renderLegend(mode) {
|
||||
const legend = document.getElementById("legend");
|
||||
if (!legend) return;
|
||||
|
||||
const config = LEGEND_MODES[mode] || LEGEND_MODES.cables;
|
||||
const itemsHtml = config.items
|
||||
.map(
|
||||
(item) => `
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background-color: ${item.color};"></div>
|
||||
<span>${item.label}</span>
|
||||
</div>
|
||||
`,
|
||||
)
|
||||
.join("");
|
||||
|
||||
legend.innerHTML = `
|
||||
<h3 class="legend-title">${config.title}</h3>
|
||||
<div class="legend-list">${itemsHtml}</div>
|
||||
`;
|
||||
}
|
||||
@@ -74,6 +74,7 @@ import {
|
||||
hideInfoCard,
|
||||
setInfoCardNoBorder,
|
||||
} from "./info-card.js";
|
||||
import { initLegend, setLegendMode } from "./legend.js";
|
||||
|
||||
export let scene;
|
||||
export let camera;
|
||||
@@ -194,6 +195,7 @@ function isSameCable(cable1, cable2) {
|
||||
}
|
||||
|
||||
function showCableInfo(cable) {
|
||||
setLegendMode("cables");
|
||||
showInfoCard("cable", {
|
||||
name: cable.userData.name,
|
||||
owner: cable.userData.owner,
|
||||
@@ -211,6 +213,7 @@ function showSatelliteInfo(props) {
|
||||
const perigee = (6371 * (1 - ecc)).toFixed(0);
|
||||
const apogee = (6371 * (1 + ecc)).toFixed(0);
|
||||
|
||||
setLegendMode("satellites");
|
||||
showInfoCard("satellite", {
|
||||
name: props?.name || "-",
|
||||
norad_id: props?.norad_cat_id,
|
||||
@@ -333,6 +336,7 @@ export function init() {
|
||||
|
||||
addLights();
|
||||
initInfoCard();
|
||||
initLegend();
|
||||
const earthObj = createEarth(scene);
|
||||
targetRotation = {
|
||||
x: earthObj.rotation.x,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// ui.js - UI update functions
|
||||
|
||||
let statusTimeoutId = null;
|
||||
let statusHideTimeoutId = null;
|
||||
let statusReplayTimeoutId = null;
|
||||
|
||||
// Show status message
|
||||
export function showStatusMessage(message, type = "info") {
|
||||
@@ -12,15 +14,44 @@ export function showStatusMessage(message, type = "info") {
|
||||
statusTimeoutId = null;
|
||||
}
|
||||
|
||||
statusEl.textContent = message;
|
||||
statusEl.className = `status-message ${type}`;
|
||||
statusEl.style.display = "block";
|
||||
if (statusHideTimeoutId) {
|
||||
clearTimeout(statusHideTimeoutId);
|
||||
statusHideTimeoutId = null;
|
||||
}
|
||||
|
||||
statusTimeoutId = setTimeout(() => {
|
||||
statusEl.style.display = "none";
|
||||
statusEl.textContent = "";
|
||||
statusTimeoutId = null;
|
||||
}, 3000);
|
||||
if (statusReplayTimeoutId) {
|
||||
clearTimeout(statusReplayTimeoutId);
|
||||
statusReplayTimeoutId = null;
|
||||
}
|
||||
|
||||
const startShow = () => {
|
||||
statusEl.textContent = message;
|
||||
statusEl.className = `status-message ${type}`;
|
||||
statusEl.style.display = "block";
|
||||
statusEl.offsetHeight;
|
||||
statusEl.classList.add("visible");
|
||||
|
||||
statusTimeoutId = setTimeout(() => {
|
||||
statusEl.classList.remove("visible");
|
||||
statusHideTimeoutId = setTimeout(() => {
|
||||
statusEl.style.display = "none";
|
||||
statusEl.textContent = "";
|
||||
statusHideTimeoutId = null;
|
||||
}, 280);
|
||||
statusTimeoutId = null;
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
if (statusEl.classList.contains("visible")) {
|
||||
statusEl.classList.remove("visible");
|
||||
statusReplayTimeoutId = setTimeout(() => {
|
||||
startShow();
|
||||
statusReplayTimeoutId = null;
|
||||
}, 180);
|
||||
return;
|
||||
}
|
||||
|
||||
startShow();
|
||||
}
|
||||
|
||||
// Update coordinates display
|
||||
|
||||
Reference in New Issue
Block a user