release: bump version to 0.35.0
This commit is contained in:
694
frontend/public/earth/js/controls.js
vendored
694
frontend/public/earth/js/controls.js
vendored
@@ -26,13 +26,19 @@ import {
|
||||
} from "./satellites.js";
|
||||
import { getShowCables } from "./cables.js";
|
||||
import { toggleBGP, getShowBGP, getBGPCount } from "./bgp.js";
|
||||
import { ensureTVPanelReady } from "./tv.js";
|
||||
import { ensureTVPanelReady, isTVPanelVisible, setTVPanelVisible } from "./tv.js";
|
||||
import { createHUDPanel } from "./hud-panels.js";
|
||||
import {
|
||||
ensureNewsPanelReady,
|
||||
updateNewsToggleUI,
|
||||
} from "./news.js";
|
||||
import { openSearchPanel } from "./search.js";
|
||||
import {
|
||||
closeSearchPanel,
|
||||
focusSearchInput,
|
||||
isSearchPanelOpen,
|
||||
openSearchPanel,
|
||||
refreshSearchResults,
|
||||
} from "./search.js";
|
||||
import {
|
||||
setButtonTooltip,
|
||||
setLayerButtonState,
|
||||
@@ -86,6 +92,436 @@ let focusViewAnimationToken = 0;
|
||||
let earthSettingsDefaults = null;
|
||||
let layerRegistry = new Map();
|
||||
let layerPanelInitialized = false;
|
||||
let layoutMode = "desktop";
|
||||
let activeMobileDrawerId = null;
|
||||
let mobileDrawerOpen = false;
|
||||
let mobileDrawerCard = "layers";
|
||||
let mobileDrawerHintTimer = null;
|
||||
|
||||
function detectLayoutMode() {
|
||||
const width = window.innerWidth;
|
||||
const height = window.innerHeight;
|
||||
|
||||
if (width <= 820) {
|
||||
return "mobile";
|
||||
}
|
||||
|
||||
if (width <= 1080 || height <= 760) {
|
||||
return "compact";
|
||||
}
|
||||
|
||||
return "desktop";
|
||||
}
|
||||
|
||||
export function getLayoutMode() {
|
||||
return layoutMode;
|
||||
}
|
||||
|
||||
export function isMobileLayout() {
|
||||
return layoutMode === "mobile";
|
||||
}
|
||||
|
||||
function isCompactLayout() {
|
||||
return layoutMode === "compact";
|
||||
}
|
||||
|
||||
function syncMobileDrawerState() {
|
||||
const shell = document.getElementById("mobile-drawer-shell");
|
||||
const overlay = document.getElementById("mobile-drawer-overlay");
|
||||
const sheet = shell?.querySelector(".earth-mobile-drawer-sheet");
|
||||
const tabs = document.querySelectorAll("[data-drawer-card]");
|
||||
const slots = document.querySelectorAll("[data-drawer-slot]");
|
||||
const isMobile = isMobileLayout();
|
||||
|
||||
document.body.classList.toggle(
|
||||
"earth-mobile-drawer-open",
|
||||
isMobile && mobileDrawerOpen,
|
||||
);
|
||||
|
||||
if (shell instanceof HTMLElement) {
|
||||
shell.setAttribute("aria-hidden", (!isMobile).toString());
|
||||
}
|
||||
if (overlay instanceof HTMLElement) {
|
||||
overlay.hidden = !isMobile;
|
||||
}
|
||||
|
||||
tabs.forEach((tab) => {
|
||||
if (!(tab instanceof HTMLButtonElement)) return;
|
||||
const isActive = isMobile && tab.dataset.drawerCard === mobileDrawerCard;
|
||||
tab.classList.toggle("is-active", isActive);
|
||||
tab.setAttribute("aria-selected", String(isActive));
|
||||
});
|
||||
|
||||
slots.forEach((slot) => {
|
||||
if (!(slot instanceof HTMLElement)) return;
|
||||
slot.classList.toggle(
|
||||
"is-active",
|
||||
isMobile && slot.dataset.drawerSlot === mobileDrawerCard,
|
||||
);
|
||||
});
|
||||
|
||||
const layerPanel = document.getElementById("layer-toggles");
|
||||
if (layerPanel instanceof HTMLElement) {
|
||||
layerPanel.classList.toggle("is-mobile-open", false);
|
||||
}
|
||||
|
||||
if (sheet instanceof HTMLElement) {
|
||||
if (isMobile && !mobileDrawerOpen) {
|
||||
if (!mobileDrawerHintTimer) {
|
||||
mobileDrawerHintTimer = setInterval(() => {
|
||||
if (mobileDrawerOpen) return;
|
||||
sheet.classList.remove("is-hinting");
|
||||
void sheet.offsetWidth;
|
||||
sheet.classList.add("is-hinting");
|
||||
}, 5000);
|
||||
}
|
||||
} else {
|
||||
clearInterval(mobileDrawerHintTimer);
|
||||
mobileDrawerHintTimer = null;
|
||||
sheet.classList.remove("is-hinting");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setMobileDrawerOpen(panelId, open) {
|
||||
if (!isMobileLayout()) return;
|
||||
if (panelId === "layer-toggles") {
|
||||
mobileDrawerCard = "layers";
|
||||
}
|
||||
mobileDrawerOpen = open;
|
||||
activeMobileDrawerId = open ? panelId : null;
|
||||
syncMobileDrawerState();
|
||||
}
|
||||
|
||||
function closeTransientMobileOverlays({ except = null } = {}) {
|
||||
if (isMobileLayout()) {
|
||||
if (!except) {
|
||||
mobileDrawerOpen = false;
|
||||
syncMobileDrawerState();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (except !== "search" && isSearchPanelOpen()) {
|
||||
closeSearchPanel();
|
||||
}
|
||||
|
||||
if (except !== "settings" && isSettingsModalOpen()) {
|
||||
closeSettingsModal();
|
||||
}
|
||||
|
||||
if (except !== "layer-toggles" && activeMobileDrawerId === "layer-toggles") {
|
||||
setMobileDrawerOpen("layer-toggles", false);
|
||||
}
|
||||
|
||||
if (except !== "media" && isTVPanelVisible()) {
|
||||
setTVPanelVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
function applyResponsiveLayout() {
|
||||
layoutMode = detectLayoutMode();
|
||||
|
||||
const isMobile = isMobileLayout();
|
||||
const isCompact = isCompactLayout();
|
||||
const container = document.getElementById("container");
|
||||
|
||||
document.documentElement.classList.toggle("layout-mode-mobile", isMobile);
|
||||
document.documentElement.classList.toggle("layout-mode-compact", isCompact);
|
||||
document.body.classList.toggle("layout-mode-mobile", isMobile);
|
||||
document.body.classList.toggle("layout-mode-compact", isCompact);
|
||||
container?.classList.toggle("layout-mode-mobile", isMobile);
|
||||
container?.classList.toggle("layout-mode-compact", isCompact);
|
||||
|
||||
if (!isMobile) {
|
||||
activeMobileDrawerId = null;
|
||||
mobileDrawerOpen = false;
|
||||
}
|
||||
|
||||
syncMobileDrawerState();
|
||||
}
|
||||
|
||||
function setMobileDrawerState({ open = mobileDrawerOpen, card = mobileDrawerCard } = {}) {
|
||||
mobileDrawerOpen = Boolean(open);
|
||||
mobileDrawerCard = card || "layers";
|
||||
activeMobileDrawerId = mobileDrawerOpen && mobileDrawerCard === "layers"
|
||||
? "layer-toggles"
|
||||
: null;
|
||||
syncMobileDrawerState();
|
||||
|
||||
if (!isMobileLayout()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mobileDrawerOpen) {
|
||||
closeFloatingMenus();
|
||||
closeSearchPanel();
|
||||
if (isSettingsModalOpen()) {
|
||||
closeSettingsModal();
|
||||
}
|
||||
}
|
||||
|
||||
if (!mobileDrawerOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mobileDrawerCard === "search") {
|
||||
window.setTimeout(() => {
|
||||
focusSearchInput({ select: true });
|
||||
refreshSearchResults().catch((error) => {
|
||||
console.warn("刷新抽屉搜索失败:", error);
|
||||
});
|
||||
}, 16);
|
||||
} else if (mobileDrawerCard === "tv") {
|
||||
ensureTVPanelReady().catch((error) => {
|
||||
console.error("初始化媒体抽屉失败:", error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getMobileLayerButtons(layerId) {
|
||||
return Array.from(
|
||||
document.querySelectorAll(`[data-mobile-layer-button="${layerId}"]`),
|
||||
).filter((button) => button instanceof HTMLButtonElement);
|
||||
}
|
||||
|
||||
function syncMobileLayerCards() {
|
||||
const summary = document.getElementById("mobile-layer-summary");
|
||||
const definitions = getSortedLayerDefinitions();
|
||||
let activeCount = 0;
|
||||
|
||||
definitions.forEach((definition) => {
|
||||
const visible = Boolean(definition.getVisible?.());
|
||||
if (visible) {
|
||||
activeCount += 1;
|
||||
}
|
||||
getMobileLayerButtons(definition.id).forEach((button) => {
|
||||
button.classList.toggle("is-active", visible);
|
||||
button.setAttribute("aria-checked", visible ? "true" : "false");
|
||||
const status = button.querySelector("[data-mobile-layer-status]");
|
||||
if (status) {
|
||||
status.textContent = visible ? "开启" : "关闭";
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (summary) {
|
||||
summary.textContent = `已启用 ${activeCount} 个图层`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderMobileLayerCards() {
|
||||
const list = document.getElementById("mobile-layer-list");
|
||||
if (!(list instanceof HTMLElement)) return;
|
||||
|
||||
const definitions = getSortedLayerDefinitions();
|
||||
list.innerHTML = definitions
|
||||
.map((definition) => `
|
||||
<button
|
||||
class="earth-mobile-layer-card"
|
||||
type="button"
|
||||
data-mobile-layer-button="${definition.id}"
|
||||
role="switch"
|
||||
aria-checked="${definition.getVisible?.() ? "true" : "false"}"
|
||||
>
|
||||
<span class="earth-mobile-layer-card-icon material-symbols-rounded">${definition.icon}</span>
|
||||
<span class="earth-mobile-layer-card-copy">
|
||||
<span class="earth-mobile-layer-card-title">${definition.label}</span>
|
||||
<span class="earth-mobile-layer-card-subtitle">${definition.meta || ""}</span>
|
||||
</span>
|
||||
<span class="earth-mobile-layer-card-status" data-mobile-layer-status>${definition.getVisible?.() ? "开启" : "关闭"}</span>
|
||||
</button>
|
||||
`)
|
||||
.join("");
|
||||
|
||||
list.querySelectorAll("[data-mobile-layer-button]").forEach((button) => {
|
||||
bindListener(button, "click", async (event) => {
|
||||
const target = event.currentTarget;
|
||||
if (!(target instanceof HTMLButtonElement)) return;
|
||||
const layerId = target.dataset.mobileLayerButton;
|
||||
const definition = layerId ? getLayerDefinition(layerId) : null;
|
||||
if (!definition) return;
|
||||
await definition.setVisible(!definition.getVisible());
|
||||
syncMobileLayerCards();
|
||||
});
|
||||
});
|
||||
|
||||
syncMobileLayerCards();
|
||||
}
|
||||
|
||||
function setupMobileDrawerShell() {
|
||||
const overlay = document.getElementById("mobile-drawer-overlay");
|
||||
const shell = document.getElementById("mobile-drawer-shell");
|
||||
const handle = document.getElementById("mobile-drawer-handle");
|
||||
const tabs = document.querySelectorAll("[data-drawer-card]");
|
||||
const sheet = shell?.querySelector(".earth-mobile-drawer-sheet");
|
||||
|
||||
bindListener(overlay, "click", () => {
|
||||
setMobileDrawerState({ open: false });
|
||||
});
|
||||
|
||||
tabs.forEach((tab) => {
|
||||
bindListener(tab, "click", (event) => {
|
||||
const target = event.currentTarget;
|
||||
if (!(target instanceof HTMLButtonElement)) return;
|
||||
const card = target.dataset.drawerCard || "layers";
|
||||
setMobileDrawerState({ open: true, card });
|
||||
});
|
||||
});
|
||||
|
||||
if (handle instanceof HTMLElement && sheet instanceof HTMLElement) {
|
||||
const DRAWER_HANDLE_PX = 36;
|
||||
const SWIPE_OPEN_VELOCITY = 0.3; // px/ms upward → open regardless of position
|
||||
const SWIPE_IDLE_VELOCITY = 0.05; // px/ms threshold below which position decides
|
||||
const SWIPE_CLOSE_VELOCITY = 0.5; // px/ms downward on content → close
|
||||
|
||||
let startY = 0;
|
||||
let startTranslate = 0;
|
||||
let dragging = false;
|
||||
let activePointerId = null;
|
||||
let lastMoveY = 0;
|
||||
let lastMoveTime = 0;
|
||||
let velocityY = 0;
|
||||
|
||||
sheet.addEventListener("animationend", () => {
|
||||
sheet.classList.remove("is-hinting");
|
||||
});
|
||||
|
||||
const getClosedOffset = () => {
|
||||
const safeBottom = Number.parseFloat(
|
||||
getComputedStyle(document.documentElement).getPropertyValue("--safe-bottom"),
|
||||
) || 0;
|
||||
return Math.max(sheet.offsetHeight - DRAWER_HANDLE_PX - safeBottom, 0);
|
||||
};
|
||||
|
||||
const applyTranslate = (value) => {
|
||||
sheet.style.transition = "none";
|
||||
sheet.style.transform = `translateY(${Math.max(0, Math.min(getClosedOffset(), value))}px)`;
|
||||
};
|
||||
|
||||
const stopDragging = (event) => {
|
||||
if (!dragging) return;
|
||||
if (
|
||||
event &&
|
||||
activePointerId !== null &&
|
||||
"pointerId" in event &&
|
||||
event.pointerId !== activePointerId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTransform = sheet.style.transform;
|
||||
const match = currentTransform.match(/translateY\(([-\d.]+)px\)/);
|
||||
const finalOffset = match ? Number.parseFloat(match[1]) : startTranslate;
|
||||
const closedOffset = getClosedOffset();
|
||||
const velocity = velocityY;
|
||||
|
||||
dragging = false;
|
||||
activePointerId = null;
|
||||
velocityY = 0;
|
||||
lastMoveY = 0;
|
||||
lastMoveTime = 0;
|
||||
sheet.style.transition = "";
|
||||
sheet.style.transform = "";
|
||||
|
||||
const shouldOpen =
|
||||
velocity < -SWIPE_OPEN_VELOCITY
|
||||
|| (velocity <= SWIPE_IDLE_VELOCITY && finalOffset < closedOffset * 0.5);
|
||||
|
||||
if (shouldOpen) {
|
||||
setMobileDrawerState({ open: true, card: mobileDrawerCard || "layers" });
|
||||
} else {
|
||||
setMobileDrawerState({ open: false });
|
||||
}
|
||||
};
|
||||
|
||||
bindListener(handle, "click", () => {
|
||||
if (dragging) return;
|
||||
setMobileDrawerState({ open: !mobileDrawerOpen, card: mobileDrawerCard || "layers" });
|
||||
});
|
||||
|
||||
bindListener(handle, "pointerdown", (event) => {
|
||||
if (!isMobileLayout()) return;
|
||||
sheet.classList.remove("is-hinting");
|
||||
dragging = true;
|
||||
activePointerId = event.pointerId;
|
||||
startY = event.clientY;
|
||||
lastMoveY = event.clientY;
|
||||
lastMoveTime = performance.now();
|
||||
velocityY = 0;
|
||||
startTranslate = mobileDrawerOpen ? 0 : getClosedOffset();
|
||||
applyTranslate(startTranslate);
|
||||
handle.setPointerCapture?.(event.pointerId);
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
bindListener(window, "pointermove", (event) => {
|
||||
if (!dragging) return;
|
||||
if (activePointerId !== null && event.pointerId !== activePointerId) return;
|
||||
const now = performance.now();
|
||||
const dt = now - lastMoveTime;
|
||||
if (dt > 0) {
|
||||
velocityY = (event.clientY - lastMoveY) / dt;
|
||||
}
|
||||
lastMoveY = event.clientY;
|
||||
lastMoveTime = now;
|
||||
const deltaY = event.clientY - startY;
|
||||
applyTranslate(startTranslate + deltaY);
|
||||
event.preventDefault();
|
||||
}, { passive: false });
|
||||
|
||||
bindListener(window, "pointerup", stopDragging);
|
||||
bindListener(window, "pointercancel", stopDragging);
|
||||
bindListener(handle, "lostpointercapture", stopDragging);
|
||||
}
|
||||
|
||||
bindListener(window, "earth:open-details-tab", () => {
|
||||
if (isMobileLayout()) setMobileDrawerState({ open: true, card: "details" });
|
||||
});
|
||||
|
||||
const content = shell?.querySelector(".earth-mobile-drawer-content");
|
||||
if (content instanceof HTMLElement) {
|
||||
let contentStartY = 0;
|
||||
let contentLastY = 0;
|
||||
let contentLastTime = 0;
|
||||
let contentVelocityY = 0;
|
||||
let contentTracking = false;
|
||||
|
||||
bindListener(content, "pointerdown", (event) => {
|
||||
if (!isMobileLayout() || !mobileDrawerOpen) return;
|
||||
contentStartY = event.clientY;
|
||||
contentLastY = event.clientY;
|
||||
contentLastTime = performance.now();
|
||||
contentVelocityY = 0;
|
||||
contentTracking = true;
|
||||
});
|
||||
|
||||
bindListener(content, "pointermove", (event) => {
|
||||
if (!contentTracking) return;
|
||||
const now = performance.now();
|
||||
const dt = now - contentLastTime;
|
||||
if (dt > 0) {
|
||||
contentVelocityY = (event.clientY - contentLastY) / dt;
|
||||
}
|
||||
contentLastY = event.clientY;
|
||||
contentLastTime = now;
|
||||
});
|
||||
|
||||
const endContentTrack = () => {
|
||||
if (!contentTracking) return;
|
||||
contentTracking = false;
|
||||
const activeSlot = content.querySelector(".earth-mobile-drawer-slot.is-active");
|
||||
const atTop = !activeSlot || activeSlot.scrollTop <= 2;
|
||||
if (atTop && contentVelocityY > SWIPE_CLOSE_VELOCITY) {
|
||||
setMobileDrawerState({ open: false });
|
||||
}
|
||||
contentVelocityY = 0;
|
||||
};
|
||||
|
||||
bindListener(content, "pointerup", endContentTrack);
|
||||
bindListener(content, "pointercancel", endContentTrack);
|
||||
}
|
||||
}
|
||||
|
||||
function compareLayerDefinitionsByStartupPriority(left, right) {
|
||||
const leftPriority = Number.isFinite(left?.startupPriority)
|
||||
@@ -282,20 +718,23 @@ function persistEarthSettings() {
|
||||
}
|
||||
|
||||
function syncDefaultEarthZoomUi(nextZoom) {
|
||||
const slider = document.getElementById("default-earth-size-slider");
|
||||
const value = document.getElementById("default-earth-size-value");
|
||||
const sliders = document.querySelectorAll("#default-earth-size-slider, [data-default-earth-size-slider]");
|
||||
const values = document.querySelectorAll("#default-earth-size-value, [data-default-earth-size-value]");
|
||||
const zoomValue = document.getElementById("zoom-value");
|
||||
const tooltipText = getZoomResetTooltipText(nextZoom);
|
||||
|
||||
if (slider instanceof HTMLInputElement) {
|
||||
sliders.forEach((slider) => {
|
||||
if (!(slider instanceof HTMLInputElement)) return;
|
||||
slider.min = CONFIG.minZoom.toString();
|
||||
slider.max = CONFIG.maxZoom.toString();
|
||||
slider.step = DEFAULT_EARTH_ZOOM_STEP.toString();
|
||||
slider.value = nextZoom.toFixed(2);
|
||||
}
|
||||
if (value) {
|
||||
value.textContent = formatZoomPercent(nextZoom);
|
||||
}
|
||||
});
|
||||
values.forEach((value) => {
|
||||
if (value instanceof HTMLElement) {
|
||||
value.textContent = formatZoomPercent(nextZoom);
|
||||
}
|
||||
});
|
||||
if (zoomValue instanceof HTMLElement) {
|
||||
zoomValue.title = tooltipText;
|
||||
const tooltip = zoomValue.querySelector(".tooltip");
|
||||
@@ -332,14 +771,16 @@ async function applyEarthSettings(settings) {
|
||||
});
|
||||
|
||||
const appliedOpacity = setTerrainOpacity(settings.terrainOpacity);
|
||||
const terrainOpacitySlider = document.getElementById("terrain-opacity-slider");
|
||||
const terrainOpacityValue = document.getElementById("terrain-opacity-value");
|
||||
if (terrainOpacitySlider instanceof HTMLInputElement) {
|
||||
terrainOpacitySlider.value = appliedOpacity.toFixed(2);
|
||||
}
|
||||
if (terrainOpacityValue) {
|
||||
terrainOpacityValue.textContent = `${Math.round(appliedOpacity * 100)}%`;
|
||||
}
|
||||
document.querySelectorAll("#terrain-opacity-slider, [data-terrain-opacity-slider]").forEach((slider) => {
|
||||
if (slider instanceof HTMLInputElement) {
|
||||
slider.value = appliedOpacity.toFixed(2);
|
||||
}
|
||||
});
|
||||
document.querySelectorAll("#terrain-opacity-value, [data-terrain-opacity-value]").forEach((value) => {
|
||||
if (value instanceof HTMLElement) {
|
||||
value.textContent = `${Math.round(appliedOpacity * 100)}%`;
|
||||
}
|
||||
});
|
||||
|
||||
setRotationMode(settings.rotationMode, { persist: false, suppressStatus: true });
|
||||
|
||||
@@ -377,6 +818,7 @@ async function setTerrainEnabled(button, enabled, { persist = true, silent = fal
|
||||
|
||||
if (!enabled) {
|
||||
applyTerrainUiState(button, false);
|
||||
syncMobileLayerCards();
|
||||
if (persist) persistEarthSettings();
|
||||
if (!silent) {
|
||||
showStatusMessage("地形已隐藏", "info");
|
||||
@@ -399,6 +841,7 @@ async function setTerrainEnabled(button, enabled, { persist = true, silent = fal
|
||||
if (toggleToken !== terrainToggleToken) return showTerrain;
|
||||
|
||||
applyTerrainUiState(button, true);
|
||||
syncMobileLayerCards();
|
||||
if (persist) persistEarthSettings();
|
||||
if (!silent) {
|
||||
showStatusMessage("真实地形已显示", "success");
|
||||
@@ -407,6 +850,7 @@ async function setTerrainEnabled(button, enabled, { persist = true, silent = fal
|
||||
} catch (error) {
|
||||
console.error("加载真实地形失败:", error);
|
||||
applyTerrainUiState(button, false);
|
||||
syncMobileLayerCards();
|
||||
if (persist) persistEarthSettings();
|
||||
if (!silent) {
|
||||
showStatusMessage("真实地形暂时不可用", "error");
|
||||
@@ -429,11 +873,14 @@ async function setSatellitesLayerEnabled(button, enabled, { persist = true, sile
|
||||
if (!enabled && !silent) {
|
||||
showStatusMessage("卫星已隐藏", "info");
|
||||
} else if (enabled) {
|
||||
const satelliteCountEl = document.getElementById("satellite-count");
|
||||
if (satelliteCountEl) {
|
||||
satelliteCountEl.textContent = `${getSatelliteCount()} 颗`;
|
||||
}
|
||||
["satellite-count", "mobile-satellite-count"].forEach((id) => {
|
||||
const satelliteCountEl = document.getElementById(id);
|
||||
if (satelliteCountEl) {
|
||||
satelliteCountEl.textContent = `${getSatelliteCount()} 颗`;
|
||||
}
|
||||
});
|
||||
}
|
||||
syncMobileLayerCards();
|
||||
if (persist) persistEarthSettings();
|
||||
return enabled;
|
||||
} catch (error) {
|
||||
@@ -443,6 +890,7 @@ async function setSatellitesLayerEnabled(button, enabled, { persist = true, sile
|
||||
loading: false,
|
||||
tooltip: "显示卫星",
|
||||
});
|
||||
syncMobileLayerCards();
|
||||
if (persist) persistEarthSettings();
|
||||
return false;
|
||||
}
|
||||
@@ -462,6 +910,7 @@ function setBGPLayerEnabled(button, enabled, { persist = true, silent = false }
|
||||
if (bgpCountEl) {
|
||||
bgpCountEl.textContent = `${getBGPCount()} 条`;
|
||||
}
|
||||
syncMobileLayerCards();
|
||||
if (persist) persistEarthSettings();
|
||||
if (!silent) {
|
||||
showStatusMessage(enabled ? "BGP观测已显示" : "BGP观测已隐藏", "info");
|
||||
@@ -475,6 +924,7 @@ function setTrailsLayerEnabled(button, enabled, { persist = true, silent = false
|
||||
active: enabled,
|
||||
tooltip: enabled ? "隐藏轨迹" : "显示轨迹",
|
||||
});
|
||||
syncMobileLayerCards();
|
||||
if (persist) persistEarthSettings();
|
||||
if (!silent) {
|
||||
showStatusMessage(enabled ? "轨迹已显示" : "轨迹已隐藏", "info");
|
||||
@@ -486,10 +936,12 @@ async function setCablesLayerEnabled(button, enabled, { persist = true, silent =
|
||||
clearSelectionIfHiding(!enabled);
|
||||
try {
|
||||
await setCablesEnabled(enabled, { suppressStatus: silent, suppressLoadingUi: silent });
|
||||
syncMobileLayerCards();
|
||||
if (persist) persistEarthSettings();
|
||||
return enabled;
|
||||
} catch (error) {
|
||||
console.error("切换线缆显示失败:", error);
|
||||
syncMobileLayerCards();
|
||||
if (persist) persistEarthSettings();
|
||||
return getShowCables();
|
||||
}
|
||||
@@ -661,6 +1113,7 @@ function registerLayerDefinition(definition, options = {}) {
|
||||
if (row && layerPanelInitialized) {
|
||||
bindLayerButton(row, normalizedDefinition);
|
||||
}
|
||||
renderMobileLayerCards();
|
||||
return normalizedDefinition;
|
||||
}
|
||||
|
||||
@@ -749,6 +1202,15 @@ export function applyImmediateView(targetEarthObj, camera, options = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
export function setZoomLevel(nextZoom, camera = activeCamera) {
|
||||
zoomLevel = clampEarthZoomLevel(nextZoom);
|
||||
if (camera) {
|
||||
camera.position.z = CONFIG.defaultCameraZ / zoomLevel;
|
||||
updateZoomDisplay(zoomLevel, camera.position.z.toFixed(0));
|
||||
}
|
||||
return zoomLevel;
|
||||
}
|
||||
|
||||
function cancelSettingsSheetAnimation() {
|
||||
if (settingsSheetAnimation) {
|
||||
settingsSheetAnimation.cancel();
|
||||
@@ -892,6 +1354,10 @@ function closeFloatingMenus() {
|
||||
}
|
||||
|
||||
function openSettingsModal() {
|
||||
if (isMobileLayout()) {
|
||||
setMobileDrawerState({ open: true, card: "settings" });
|
||||
return;
|
||||
}
|
||||
const modal = document.getElementById("settings-modal");
|
||||
const trigger = document.getElementById("settings-trigger");
|
||||
const sheet = modal?.querySelector(".earth-settings-sheet");
|
||||
@@ -901,7 +1367,9 @@ function openSettingsModal() {
|
||||
settingsModalTimer = null;
|
||||
}
|
||||
closeFloatingMenus();
|
||||
closeTransientMobileOverlays({ except: "settings" });
|
||||
cancelSettingsSheetAnimation();
|
||||
document.body.classList.add("earth-settings-open");
|
||||
modal.classList.remove("is-closing");
|
||||
modal.classList.add("is-opening");
|
||||
modal.classList.add("is-open");
|
||||
@@ -918,11 +1386,15 @@ function openSettingsModal() {
|
||||
}
|
||||
|
||||
function closeSettingsModal() {
|
||||
if (isMobileLayout()) {
|
||||
return;
|
||||
}
|
||||
const modal = document.getElementById("settings-modal");
|
||||
const trigger = document.getElementById("settings-trigger");
|
||||
const sheet = modal?.querySelector(".earth-settings-sheet");
|
||||
if (!modal) return;
|
||||
cancelSettingsSheetAnimation();
|
||||
document.body.classList.remove("earth-settings-open");
|
||||
modal.classList.remove("is-open");
|
||||
modal.classList.add("is-closing");
|
||||
if (sheet instanceof HTMLElement) {
|
||||
@@ -942,6 +1414,10 @@ function setHudPanelVisibility(panelId, visible, { persist = true } = {}) {
|
||||
const panel = document.getElementById(panelId);
|
||||
if (!panel) return;
|
||||
panel.classList.toggle("hud-panel-hidden", !visible);
|
||||
if (!visible && activeMobileDrawerId === panelId) {
|
||||
activeMobileDrawerId = null;
|
||||
syncMobileDrawerState();
|
||||
}
|
||||
syncSettingsToggle(panelId, visible);
|
||||
if (panelId === "media-panel") {
|
||||
updateTVToggleUI(visible);
|
||||
@@ -974,10 +1450,11 @@ function syncAllHudPanelToggles() {
|
||||
}
|
||||
|
||||
function syncDayNightToggle(enabled) {
|
||||
const input = document.getElementById("toggle-daynight");
|
||||
if (input instanceof HTMLInputElement) {
|
||||
input.checked = enabled;
|
||||
}
|
||||
document.querySelectorAll("#toggle-daynight, [data-daynight-toggle]").forEach((input) => {
|
||||
if (input instanceof HTMLInputElement) {
|
||||
input.checked = enabled;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function applyDayNightEnabled(enabled, { persist = true } = {}) {
|
||||
@@ -1029,24 +1506,29 @@ function setupSettingsControls() {
|
||||
});
|
||||
});
|
||||
|
||||
const terrainOpacitySlider = document.getElementById("terrain-opacity-slider");
|
||||
const terrainOpacityValue = document.getElementById("terrain-opacity-value");
|
||||
const defaultEarthSizeSlider = document.getElementById("default-earth-size-slider");
|
||||
const terrainOpacitySliders = document.querySelectorAll("#terrain-opacity-slider, [data-terrain-opacity-slider]");
|
||||
const terrainOpacityValues = document.querySelectorAll("#terrain-opacity-value, [data-terrain-opacity-value]");
|
||||
const defaultEarthSizeSliders = document.querySelectorAll("#default-earth-size-slider, [data-default-earth-size-slider]");
|
||||
const rotationModeButtons = document.querySelectorAll("[data-rotation-mode]");
|
||||
const syncTerrainOpacityUi = (nextOpacity) => {
|
||||
const safeOpacity = Math.round(nextOpacity * 100);
|
||||
if (terrainOpacitySlider instanceof HTMLInputElement) {
|
||||
terrainOpacitySlider.value = nextOpacity.toFixed(2);
|
||||
}
|
||||
if (terrainOpacityValue) {
|
||||
terrainOpacityValue.textContent = `${safeOpacity}%`;
|
||||
}
|
||||
terrainOpacitySliders.forEach((slider) => {
|
||||
if (slider instanceof HTMLInputElement) {
|
||||
slider.value = nextOpacity.toFixed(2);
|
||||
}
|
||||
});
|
||||
terrainOpacityValues.forEach((value) => {
|
||||
if (value instanceof HTMLElement) {
|
||||
value.textContent = `${safeOpacity}%`;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
syncTerrainOpacityUi(getTerrainOpacity());
|
||||
syncDefaultEarthZoomUi(defaultEarthZoom);
|
||||
|
||||
if (terrainOpacitySlider instanceof HTMLInputElement) {
|
||||
terrainOpacitySliders.forEach((terrainOpacitySlider) => {
|
||||
if (!(terrainOpacitySlider instanceof HTMLInputElement)) return;
|
||||
bindListener(terrainOpacitySlider, "input", (event) => {
|
||||
const target = event.currentTarget;
|
||||
if (!(target instanceof HTMLInputElement)) return;
|
||||
@@ -1057,9 +1539,10 @@ function setupSettingsControls() {
|
||||
syncTerrainOpacityUi(appliedOpacity);
|
||||
persistEarthSettings();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (defaultEarthSizeSlider instanceof HTMLInputElement) {
|
||||
defaultEarthSizeSliders.forEach((defaultEarthSizeSlider) => {
|
||||
if (!(defaultEarthSizeSlider instanceof HTMLInputElement)) return;
|
||||
bindListener(defaultEarthSizeSlider, "input", (event) => {
|
||||
const target = event.currentTarget;
|
||||
if (!(target instanceof HTMLInputElement)) return;
|
||||
@@ -1069,7 +1552,7 @@ function setupSettingsControls() {
|
||||
{ persist: true, applyToCurrentView: true },
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
rotationModeButtons.forEach((button) => {
|
||||
bindListener(button, "click", (event) => {
|
||||
@@ -1081,12 +1564,17 @@ function setupSettingsControls() {
|
||||
});
|
||||
});
|
||||
|
||||
const dayNightToggle = document.getElementById("toggle-daynight");
|
||||
if (dayNightToggle instanceof HTMLInputElement) {
|
||||
document.querySelectorAll("#toggle-daynight, [data-daynight-toggle]").forEach((dayNightToggle) => {
|
||||
if (!(dayNightToggle instanceof HTMLInputElement)) return;
|
||||
bindListener(dayNightToggle, "change", () => {
|
||||
applyDayNightEnabled(dayNightToggle.checked);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const mobileSettingsReset = document.getElementById("mobile-settings-reset");
|
||||
bindListener(mobileSettingsReset, "click", () => {
|
||||
resetEarthSettings();
|
||||
});
|
||||
|
||||
captureEarthSettingsDefaults();
|
||||
applyEarthSettings(loadEarthSettings());
|
||||
@@ -1104,6 +1592,10 @@ function setupHudPanelControls() {
|
||||
if (!(target instanceof HTMLElement)) return;
|
||||
const panelId = target.dataset.closePanel;
|
||||
if (!panelId) return;
|
||||
if (isMobileLayout() && panelId === "layer-toggles") {
|
||||
setMobileDrawerOpen(panelId, false);
|
||||
return;
|
||||
}
|
||||
setHudPanelVisibility(panelId, false);
|
||||
});
|
||||
});
|
||||
@@ -1231,19 +1723,32 @@ function setupDraggableHudPanels() {
|
||||
if (!handle) return;
|
||||
|
||||
let isDragging = false;
|
||||
let activePointerId = null;
|
||||
let startPointerX = 0;
|
||||
let startPointerY = 0;
|
||||
let startLeft = 0;
|
||||
let startTop = 0;
|
||||
|
||||
const stopDragging = () => {
|
||||
const stopDragging = (event) => {
|
||||
if (
|
||||
event &&
|
||||
activePointerId !== null &&
|
||||
"pointerId" in event &&
|
||||
event.pointerId !== activePointerId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
isDragging = false;
|
||||
activePointerId = null;
|
||||
panel.classList.remove("is-dragging");
|
||||
document.body.style.userSelect = "";
|
||||
};
|
||||
|
||||
const onMove = (event) => {
|
||||
if (!isDragging) return;
|
||||
if (activePointerId !== null && event.pointerId !== activePointerId) return;
|
||||
if (isMobileLayout()) return;
|
||||
event.preventDefault();
|
||||
const desiredLeft = startLeft + (event.clientX - startPointerX);
|
||||
const desiredTop = startTop + (event.clientY - startPointerY);
|
||||
capturePanelAnchor(app, panel, desiredLeft, desiredTop);
|
||||
@@ -1263,8 +1768,11 @@ function setupDraggableHudPanels() {
|
||||
};
|
||||
|
||||
bindListener(handle, "pointerdown", (event) => {
|
||||
if (isMobileLayout()) return;
|
||||
if (event.target.closest(".hud-panel-close, .hud-panel__action, .layer-panel-btn, .info-card-close, .tv-panel-select, .media-panel-tab, .tv-panel-player, .tv-panel-edge, .legend-bar-btn, .news-story-card")) return;
|
||||
event.preventDefault();
|
||||
isDragging = true;
|
||||
activePointerId = event.pointerId;
|
||||
startPointerX = event.clientX;
|
||||
startPointerY = event.clientY;
|
||||
const appRect = app.getBoundingClientRect();
|
||||
@@ -1299,9 +1807,9 @@ function setupDraggableHudPanels() {
|
||||
handle.setPointerCapture?.(event.pointerId);
|
||||
});
|
||||
|
||||
bindListener(handle, "pointermove", onMove);
|
||||
bindListener(handle, "pointerup", stopDragging);
|
||||
bindListener(handle, "pointercancel", stopDragging);
|
||||
bindListener(window, "pointermove", onMove, { passive: false });
|
||||
bindListener(window, "pointerup", stopDragging);
|
||||
bindListener(window, "pointercancel", stopDragging);
|
||||
bindListener(handle, "lostpointercapture", stopDragging);
|
||||
});
|
||||
|
||||
@@ -1394,6 +1902,7 @@ export function setupControls(camera, renderer, scene, earth) {
|
||||
resetCleanup();
|
||||
activeCamera = camera;
|
||||
earthObj = earth;
|
||||
applyResponsiveLayout();
|
||||
setupZoomControls(camera);
|
||||
setupWheelZoom(camera, renderer);
|
||||
setupRotateControls(camera, earth);
|
||||
@@ -1401,6 +1910,21 @@ export function setupControls(camera, renderer, scene, earth) {
|
||||
setupLiquidGlassInteractions();
|
||||
setupToolbarHubCluster();
|
||||
setupKeyboardControls();
|
||||
bindListener(window, "resize", () => {
|
||||
applyResponsiveLayout();
|
||||
});
|
||||
bindListener(window, "earth:search-open-change", (event) => {
|
||||
if (event instanceof CustomEvent && event.detail?.open) {
|
||||
closeTransientMobileOverlays({ except: "search" });
|
||||
}
|
||||
});
|
||||
bindListener(window, "earth:tv-visibility-change", (event) => {
|
||||
if (event instanceof CustomEvent && event.detail?.visible) {
|
||||
closeTransientMobileOverlays({ except: "media" });
|
||||
}
|
||||
});
|
||||
// No longer auto-navigates to details tab on mobile — popup handles the display.
|
||||
// Drawer details tab is opened explicitly via earth:open-details-tab when user taps popup.
|
||||
}
|
||||
|
||||
function setupZoomControls(camera) {
|
||||
@@ -1786,6 +2310,7 @@ export function getStartupLoadLayers() {
|
||||
function setupTerrainControls() {
|
||||
initializeLayerRegistry();
|
||||
const container = document.getElementById("container");
|
||||
const layerBtn = document.getElementById("layer-action");
|
||||
const searchBtn = document.getElementById("search-action");
|
||||
const terrainBtn = getLayerButton("terrain");
|
||||
const layoutBtn = document.getElementById("layout-toggle");
|
||||
@@ -1796,8 +2321,29 @@ function setupTerrainControls() {
|
||||
setupHudPanelControls();
|
||||
setupDraggableHudPanels();
|
||||
setupLayerPanel();
|
||||
setupMobileDrawerShell();
|
||||
renderMobileLayerCards();
|
||||
|
||||
bindListener(layerBtn, "click", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (isMobileLayout()) {
|
||||
const nextOpen = !(mobileDrawerOpen && mobileDrawerCard === "layers");
|
||||
setMobileDrawerState({ open: nextOpen, card: "layers" });
|
||||
return;
|
||||
}
|
||||
|
||||
const panel = document.getElementById("layer-toggles");
|
||||
const currentlyVisible = !panel?.classList.contains("hud-panel-hidden");
|
||||
setHudPanelVisibility("layer-toggles", !currentlyVisible);
|
||||
});
|
||||
|
||||
bindListener(searchBtn, "click", () => {
|
||||
if (isMobileLayout()) {
|
||||
setMobileDrawerState({ open: true, card: "search" });
|
||||
return;
|
||||
}
|
||||
closeTransientMobileOverlays({ except: "search" });
|
||||
openSearchPanel();
|
||||
});
|
||||
|
||||
@@ -1821,6 +2367,15 @@ function setupTerrainControls() {
|
||||
const openGroups = [zoomGroup].filter((group) =>
|
||||
group?.classList.contains("open"),
|
||||
);
|
||||
if (
|
||||
isMobileLayout() &&
|
||||
mobileDrawerOpen &&
|
||||
event.target instanceof Element &&
|
||||
!event.target.closest("#mobile-drawer-shell")
|
||||
) {
|
||||
setMobileDrawerState({ open: false });
|
||||
}
|
||||
|
||||
if (openGroups.length === 0) return;
|
||||
|
||||
const clickedInsideOpenGroup = openGroups.some((group) =>
|
||||
@@ -1851,6 +2406,7 @@ function setupTerrainControls() {
|
||||
ensureNewsPanelReady().catch((error) => {
|
||||
console.error("初始化态势新闻内容失败:", error);
|
||||
});
|
||||
applyResponsiveLayout();
|
||||
updateLayoutUI(container);
|
||||
}
|
||||
|
||||
@@ -1858,11 +2414,21 @@ function setupKeyboardControls() {
|
||||
bindListener(document, "keydown", (event) => {
|
||||
if (event.key !== "Escape") return;
|
||||
|
||||
if (isSearchPanelOpen()) {
|
||||
closeSearchPanel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSettingsModalOpen()) {
|
||||
closeSettingsModal();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMobileLayout() && mobileDrawerOpen) {
|
||||
setMobileDrawerState({ open: false });
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFloatingMenuVisible()) {
|
||||
closeFloatingMenus();
|
||||
return;
|
||||
@@ -1970,9 +2536,12 @@ function setupToolbarHubCluster() {
|
||||
let collapseTimer = null;
|
||||
let expandedToolbarBounds = null;
|
||||
let refreshBoundsFrameId = 0;
|
||||
let hubPinnedOpen = false;
|
||||
|
||||
const layoutToolbarOrbs = () => {
|
||||
const orbs = Array.from(cluster.querySelectorAll(".earth-toolbar-orb"));
|
||||
const orbs = Array.from(cluster.querySelectorAll(".earth-toolbar-orb")).filter(
|
||||
(orb) => getComputedStyle(orb).display !== "none",
|
||||
);
|
||||
if (orbs.length === 0) return;
|
||||
|
||||
const toolbarWidth = toolbar.clientWidth || TOOLBAR_BASE_WIDTH_PX;
|
||||
@@ -2056,6 +2625,7 @@ function setupToolbarHubCluster() {
|
||||
};
|
||||
|
||||
const scheduleCollapse = () => {
|
||||
if (hubPinnedOpen) return;
|
||||
if (collapseTimer) clearTimeout(collapseTimer);
|
||||
collapseTimer = window.setTimeout(() => {
|
||||
setExpanded(false);
|
||||
@@ -2094,6 +2664,19 @@ function setupToolbarHubCluster() {
|
||||
setExpanded(true);
|
||||
});
|
||||
|
||||
bindListener(hub, "click", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
cancelCollapse();
|
||||
if (isMobileLayout()) {
|
||||
hubPinnedOpen = !cluster.classList.contains("is-expanded");
|
||||
setExpanded(hubPinnedOpen);
|
||||
return;
|
||||
}
|
||||
hubPinnedOpen = !cluster.classList.contains("is-expanded");
|
||||
setExpanded(hubPinnedOpen);
|
||||
});
|
||||
|
||||
const HOVER_PADDING_PX = 12;
|
||||
const collectExpandedToolbarBounds = () => {
|
||||
const rects = [];
|
||||
@@ -2153,6 +2736,7 @@ function setupToolbarHubCluster() {
|
||||
};
|
||||
|
||||
bindListener(document, "mousemove", (event) => {
|
||||
if (hubPinnedOpen) return;
|
||||
if (!cluster.classList.contains("is-expanded")) return;
|
||||
if (
|
||||
event.target instanceof Element &&
|
||||
@@ -2188,6 +2772,24 @@ function setupToolbarHubCluster() {
|
||||
scheduleExpandedToolbarBoundsRefresh();
|
||||
}
|
||||
});
|
||||
|
||||
cluster.querySelectorAll(".earth-toolbar-orb > button").forEach((button) => {
|
||||
if (!(button instanceof HTMLButtonElement) || button === hub) return;
|
||||
bindListener(button, "click", () => {
|
||||
if (hubPinnedOpen) {
|
||||
hubPinnedOpen = false;
|
||||
setExpanded(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
bindListener(document, "pointerdown", (event) => {
|
||||
if (!hubPinnedOpen) return;
|
||||
if (!(event.target instanceof Element)) return;
|
||||
if (event.target.closest("#toolbar-cluster")) return;
|
||||
hubPinnedOpen = false;
|
||||
setExpanded(false);
|
||||
});
|
||||
}
|
||||
|
||||
export function teardownControls() {
|
||||
|
||||
Reference in New Issue
Block a user