Files
planet/frontend/public/earth/js/ui.js
linkong fbca381512
Some checks failed
ci / backend (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / delivery (push) Has been cancelled
release / images (push) Has been cancelled
release: bump version to 0.62.0
2026-05-21 01:37:32 +08:00

402 lines
12 KiB
JavaScript

// ui.js - UI update functions
let statusTimeoutId = null;
let statusHideTimeoutId = null;
const STATUS_BASE_CLASS = "earth-status-message";
const STATUS_TICKER_STACK_CLASS = "earth-status-message--ticker-stack";
const STATUS_DISPLAY_MS = 3000;
const STATUS_FADE_MS = 280;
const GESTURE_STATUS_DISPLAY_MS = 760;
let statusQueue = [];
let statusBusy = false;
let loadingActive = false;
let loadingLockedWidth = 0;
let pendingLoadingMessage = "";
function createStatusEntry(message, type = "info") {
return { message, type };
}
function getElement(id) {
return document.getElementById(id);
}
function getEarthStatTargets(statKey) {
return Array.from(
document.querySelectorAll(`[data-earth-stat="${statKey}"]`),
);
}
export function setEarthStatValue(statKey, value) {
getEarthStatTargets(statKey).forEach((element) => {
if (element instanceof HTMLElement) {
element.textContent = value;
}
});
}
function setElementDisplay(element, visible, displayValue = "block") {
if (!element) return;
element.style.display = visible ? displayValue : "none";
}
function clearStatusTimers() {
if (statusTimeoutId) {
clearTimeout(statusTimeoutId);
statusTimeoutId = null;
}
if (statusHideTimeoutId) {
clearTimeout(statusHideTimeoutId);
statusHideTimeoutId = null;
}
}
function clearLoadingWidthLock(statusEl) {
loadingLockedWidth = 0;
if (statusEl) {
statusEl.style.minWidth = "";
}
}
function updateLoadingWidthLock(statusEl) {
if (!statusEl || !loadingActive) return;
const nextWidth = Math.ceil(statusEl.getBoundingClientRect().width || statusEl.scrollWidth || 0);
if (nextWidth <= 0) return;
loadingLockedWidth = Math.max(loadingLockedWidth, nextWidth);
statusEl.style.minWidth = `${loadingLockedWidth}px`;
}
function buildStatusContent(statusEl, message, type) {
statusEl.innerHTML = "";
const indicator = document.createElement("span");
indicator.className = "earth-status-indicator";
indicator.setAttribute("aria-hidden", "true");
const dotCount = type === "loading" ? 3 : 1;
for (let i = 0; i < dotCount; i++) {
const dot = document.createElement("span");
dot.className = "earth-status-dot";
indicator.appendChild(dot);
}
const text = document.createElement("span");
text.className = "earth-status-text";
text.textContent = message;
statusEl.appendChild(indicator);
statusEl.appendChild(text);
}
function getStatusSidePlacement(statusEl) {
if (!(statusEl instanceof HTMLElement)) return false;
if (document.querySelector(".layout-mode-mobile")) return false;
const ticker = document.getElementById("desktop-news-ticker");
const brand = document.getElementById("brand-panel");
if (!(ticker instanceof HTMLElement) || !(brand instanceof HTMLElement)) return false;
if (ticker.classList.contains("is-hidden") || ticker.offsetParent === null) return false;
const tickerRect = ticker.getBoundingClientRect();
const brandRect = brand.getBoundingClientRect();
const statusWidth = Math.ceil(statusEl.getBoundingClientRect().width || statusEl.scrollWidth || 0);
if (!statusWidth || !tickerRect.width || !brandRect.width) return false;
const rootStyle = getComputedStyle(document.documentElement);
const hudScale = Number.parseFloat(rootStyle.getPropertyValue("--hud-scale")) || 1;
const requiredGap = Math.max(10, Math.round(12 * hudScale));
const availableWidth = tickerRect.left - brandRect.right - requiredGap * 2;
return {
shouldStack: availableWidth < statusWidth,
left: Math.round(brandRect.right + requiredGap),
maxWidth: Math.max(160, Math.floor(availableWidth)),
};
}
function syncStatusPlacement(statusEl) {
if (!(statusEl instanceof HTMLElement)) return;
if (document.querySelector(".layout-mode-mobile")) {
statusEl.classList.remove(STATUS_TICKER_STACK_CLASS);
statusEl.style.left = "";
statusEl.style.maxWidth = "";
return;
}
const placement = getStatusSidePlacement(statusEl);
const shouldStack = !placement || placement.shouldStack;
statusEl.classList.toggle(STATUS_TICKER_STACK_CLASS, shouldStack);
if (!placement || shouldStack) {
statusEl.style.left = "";
statusEl.style.maxWidth = "";
return;
}
statusEl.style.left = `${placement.left}px`;
statusEl.style.maxWidth = `${placement.maxWidth}px`;
}
function syncVisibleStatusPlacement() {
const statusEl = getElement("status-message");
if (statusEl?.classList.contains("visible")) {
syncStatusPlacement(statusEl);
}
}
function buildPersistentErrorContent(errorEl, message) {
buildStatusContent(errorEl, message, "error");
}
function hideStatusElement(statusEl, onHidden) {
statusEl.classList.remove("visible");
statusHideTimeoutId = setTimeout(() => {
if (!statusEl.classList.contains("visible")) {
setElementDisplay(statusEl, false);
statusEl.className = STATUS_BASE_CLASS;
statusEl.style.left = "";
statusEl.style.maxWidth = "";
statusEl.innerHTML = "";
}
statusHideTimeoutId = null;
if (typeof onHidden === "function") {
onHidden();
}
}, STATUS_FADE_MS);
}
function processStatusQueue() {
if (loadingActive || statusBusy || statusQueue.length === 0) return;
const next = statusQueue.shift();
if (!next) return;
startTransientStatus(next.message, next.type);
}
function startTransientStatus(message, type = "info") {
const statusEl = getElement("status-message");
if (!statusEl) return;
clearStatusTimers();
statusBusy = true;
buildStatusContent(statusEl, message, type);
statusEl.className = `${STATUS_BASE_CLASS} ${type}`;
setElementDisplay(statusEl, true, "inline-flex");
syncStatusPlacement(statusEl);
statusEl.offsetHeight;
statusEl.classList.add("visible");
statusTimeoutId = setTimeout(() => {
hideStatusElement(statusEl, () => {
statusBusy = false;
processStatusQueue();
});
statusTimeoutId = null;
}, STATUS_DISPLAY_MS);
}
// Show status message
export function showStatusMessage(message, type = "info") {
if (loadingActive) {
statusQueue.unshift(createStatusEntry(message, type));
return;
}
startTransientStatus(message, type);
}
export function queueStatusMessage(message, type = "info") {
statusQueue.push(createStatusEntry(message, type));
processStatusQueue();
}
export function showGestureStatusMessage(message, type = "info") {
if (loadingActive) return;
const statusEl = getElement("status-message");
if (!statusEl) return;
clearStatusTimers();
statusBusy = true;
buildStatusContent(statusEl, message, type);
statusEl.className = `${STATUS_BASE_CLASS} ${type} gesture`;
setElementDisplay(statusEl, true, "inline-flex");
syncStatusPlacement(statusEl);
statusEl.offsetHeight;
statusEl.classList.add("visible");
statusTimeoutId = setTimeout(() => {
hideStatusElement(statusEl, () => {
statusBusy = false;
processStatusQueue();
});
statusTimeoutId = null;
}, GESTURE_STATUS_DISPLAY_MS);
}
// Update coordinates display
export function updateCoordinatesDisplay(lat, lon, alt = 0) {
const longitudeEl = getElement("longitude-value");
const latitudeEl = getElement("latitude-value");
const mouseCoordsEl = getElement("mouse-coords");
if (longitudeEl) longitudeEl.textContent = lon.toFixed(2) + "°";
if (latitudeEl) latitudeEl.textContent = lat.toFixed(2) + "°";
if (mouseCoordsEl) {
mouseCoordsEl.textContent = `鼠标: ${lat.toFixed(2)}°, ${lon.toFixed(2)}°`;
}
}
// Update zoom display
export function updateZoomDisplay(zoomLevel, distance) {
const percent = Math.round(zoomLevel * 100);
const zoomValueEl = getElement("zoom-value");
const zoomLevelEl = getElement("zoom-level");
const slider = getElement("zoom-slider");
const cameraDistanceEl = getElement("camera-distance");
if (zoomValueEl) {
const label = `${percent}%`;
zoomValueEl.textContent = label;
}
if (zoomLevelEl) zoomLevelEl.textContent = "缩放: " + percent + "%";
if (slider) slider.value = zoomLevel;
if (cameraDistanceEl) cameraDistanceEl.textContent = distance + " km";
}
// Update earth stats
export function updateEarthStats(stats) {
const has = (key) => Object.prototype.hasOwnProperty.call(stats, key);
if (has("cableCount")) setEarthStatValue("cable-count", String(stats.cableCount || 0));
if (has("landingPointCount")) {
setEarthStatValue("landing-point-count", String(stats.landingPointCount || 0));
}
if (has("satelliteCount")) setEarthStatValue("satellite-count", String(stats.satelliteCount || 0));
if (has("computeCenterCount")) {
setEarthStatValue("compute-center-count", String(stats.computeCenterCount || 0));
}
if (has("vesselCount")) setEarthStatValue("vessel-count", String(stats.vesselCount || 0));
if (has("vesselLiveSummary")) setEarthStatValue("vessel-live-summary", stats.vesselLiveSummary || "-");
if (has("bgpAnomalyCount")) setEarthStatValue("bgp-anomaly-count", String(stats.bgpAnomalyCount || 0));
if (has("bgpCollectorCount")) {
setEarthStatValue("bgp-collector-count", String(stats.bgpCollectorCount || 0));
}
if (has("bgpStatusSummary")) setEarthStatValue("bgp-status-summary", stats.bgpStatusSummary || "-");
if (has("terrainOn")) setEarthStatValue("terrain-status", stats.terrainOn ? "开启" : "关闭");
if (has("textureQuality")) setEarthStatValue("texture-quality", stats.textureQuality || "8K 卫星图");
}
// Show/hide loading via status message
export function setLoading(loading) {
const statusEl = getElement("status-message");
if (!statusEl) return;
if (loading) {
clearStatusTimers();
loadingActive = true;
statusBusy = false;
clearLoadingWidthLock(statusEl);
buildStatusContent(
statusEl,
pendingLoadingMessage || "正在加载...",
"loading",
);
pendingLoadingMessage = "";
statusEl.className = `${STATUS_BASE_CLASS} loading`;
setElementDisplay(statusEl, true, "inline-flex");
syncStatusPlacement(statusEl);
statusEl.offsetHeight;
statusEl.classList.add("visible");
requestAnimationFrame(() => {
updateLoadingWidthLock(statusEl);
});
} else {
pendingLoadingMessage = "";
if (!statusEl.classList.contains("loading")) {
loadingActive = false;
clearLoadingWidthLock(statusEl);
processStatusQueue();
return;
}
loadingActive = false;
hideStatusElement(statusEl, () => {
clearLoadingWidthLock(statusEl);
statusBusy = false;
processStatusQueue();
});
}
}
window.addEventListener("resize", syncVisibleStatusPlacement, { passive: true });
export function setLoadingMessage(title) {
const statusEl = getElement("status-message");
if (!statusEl || !statusEl.classList.contains("loading")) {
pendingLoadingMessage = title;
return;
}
const textEl = statusEl.querySelector(".earth-status-text");
if (textEl) {
textEl.textContent = title;
requestAnimationFrame(() => {
updateLoadingWidthLock(statusEl);
syncStatusPlacement(statusEl);
});
}
}
// Show tooltip
export function showTooltip(x, y, content) {
const tooltip = getElement("tooltip");
if (!tooltip) return;
tooltip.innerHTML = content;
tooltip.style.left = x + "px";
tooltip.style.top = y + "px";
setElementDisplay(tooltip, true);
}
// Hide tooltip
export function hideTooltip() {
const tooltip = getElement("tooltip");
if (tooltip) {
setElementDisplay(tooltip, false);
}
}
// Show error message
export function showError(message) {
const errorEl = getElement("error-message");
if (!errorEl) return;
buildPersistentErrorContent(errorEl, message);
errorEl.className = `${STATUS_BASE_CLASS} earth-error-message error`;
setElementDisplay(errorEl, true, "inline-flex");
errorEl.offsetHeight;
errorEl.classList.add("visible");
}
// Hide error message
export function hideError() {
const errorEl = getElement("error-message");
if (errorEl) {
errorEl.classList.remove("visible");
setElementDisplay(errorEl, false);
errorEl.className = "earth-error-message";
errorEl.innerHTML = "";
}
}
export function clearUiState() {
clearStatusTimers();
statusQueue = [];
statusBusy = false;
loadingActive = false;
pendingLoadingMessage = "";
const statusEl = getElement("status-message");
if (statusEl) {
statusEl.className = STATUS_BASE_CLASS;
setElementDisplay(statusEl, false);
statusEl.style.left = "";
statusEl.style.maxWidth = "";
statusEl.innerHTML = "";
clearLoadingWidthLock(statusEl);
}
hideTooltip();
hideError();
}