fix: redesign earth hud interactions and legend behavior

This commit is contained in:
linkong
2026-03-26 17:58:03 +08:00
parent ab09f0ba78
commit 30a29a6e34
15 changed files with 988 additions and 364 deletions

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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) {

View 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>
`;
}

View File

@@ -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,

View File

@@ -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