release: bump version to 0.35.0
This commit is contained in:
@@ -8,6 +8,24 @@ This project follows the repository versioning rule:
|
||||
- `improvement` -> `+0.0.1`(bugfix + 小功能混合)
|
||||
- `bugfix` -> `+0.0.1`
|
||||
|
||||
## [0.35.0] — 2026-04-22
|
||||
|
||||
### ✨ Highlights
|
||||
- Earth 移动端底部抽屉系统全面上线:响应式布局自动切换、Tab 导航、手势上拉/下滑开合、惯性速度判定
|
||||
- 移动端点击可交互物件(海缆、登陆点、卫星、BGP)后弹出智能定位悬浮卡片,可拖动,点击跳转详情
|
||||
|
||||
### 🔧 Improvements
|
||||
- 抽屉把手区域缩小至 36px(collapsed 时仅露出把手,不遮挡地球操作区)
|
||||
- 抽屉定期弹跳动画提示用户可上拉,5 秒间隔,打开后自动停止
|
||||
- 通知胶囊位置调整,不再覆盖品牌 logo
|
||||
- 移动端单指旋转、双指捏合缩放地球,触控事件冲突修复(pointer-events 级联)
|
||||
|
||||
### 🐛 Fixes
|
||||
- 修复移动端抽屉 shell 因 layout 高度(240px+)遮挡地球触控区域,pointer-events 改为按层级精确控制
|
||||
- 修复悬浮卡片因 setPointerCapture 在 iOS Safari 抑制合成 click 事件导致无法点击的问题
|
||||
|
||||
---
|
||||
|
||||
## [0.34.0] — 2026-04-22
|
||||
|
||||
### ✨ Highlights
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
当前重点入口:
|
||||
|
||||
- [earth-mobile-drawer-ui-plan.md](/home/ray/dev/linkong/planet/docs/plans/earth-mobile-drawer-ui-plan.md)
|
||||
- [earth-renderer-architecture-separation-plan.md](/home/ray/dev/linkong/planet/docs/plans/earth-renderer-architecture-separation-plan.md)
|
||||
- [earth-predicted-orbit-plan.md](/home/ray/dev/linkong/planet/docs/plans/earth-predicted-orbit-plan.md)
|
||||
- [earth-webgl-instancing-satellites-plan.md](/home/ray/dev/linkong/planet/docs/plans/earth-webgl-instancing-satellites-plan.md)
|
||||
|
||||
@@ -16,12 +16,13 @@
|
||||
## Current Version
|
||||
|
||||
- `main` 当前主线历史推导到:`0.16.5`
|
||||
- `dev` 当前开发分支历史推导到:`0.34.0`
|
||||
- `dev` 当前开发分支历史推导到:`0.35.0`
|
||||
|
||||
## Timeline
|
||||
|
||||
| Version | Type | Branch | Commit | Summary |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `0.35.0` | feature | `dev` | `pending` | Earth 移动端抽屉系统与悬浮卡片全面上线:手势驱动抽屉、点击物件弹出可拖动详情卡、单指旋转双指缩放地球 |
|
||||
| `0.34.0` | feature | `dev` | `pending` | Earth 搜索面板正式接入,`planet.sh --allow-lan` 打通 Bun + Vite 局域网开放链路,并自动输出推荐访问地址与健康检查地址 |
|
||||
| `0.33.0` | feature | `dev` | `pending` | `news_live_streams` 默认接入 iptv-org 频道目录,内置数据源支持直接编辑 override,并修复 TV 合并采集源后默认频道消失的问题 |
|
||||
| `0.32.0` | feature | `dev` | `pending` | Earth 设置新增默认地球大小真源,并继续收口卫星焦点层次、toolbar/scrollbar 性能与 HUD 设置面板细节 |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "planet-frontend",
|
||||
"version": "0.34.0",
|
||||
"version": "0.35.0",
|
||||
"private": true,
|
||||
"packageManager": "bun@1",
|
||||
"dependencies": {
|
||||
|
||||
@@ -8,6 +8,10 @@
|
||||
|
||||
:root {
|
||||
--hud-scale: 1;
|
||||
--safe-top: env(safe-area-inset-top, 0px);
|
||||
--safe-right: env(safe-area-inset-right, 0px);
|
||||
--safe-bottom: env(safe-area-inset-bottom, 0px);
|
||||
--safe-left: env(safe-area-inset-left, 0px);
|
||||
--hud-offset: calc(20px * var(--hud-scale));
|
||||
--hud-radius: calc(22px * var(--hud-scale));
|
||||
--hud-panel-padding: calc(18px * var(--hud-scale));
|
||||
@@ -72,6 +76,10 @@ body.earth-page {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.earth-app canvas {
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.earth-app.dragging {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
@@ -133,3 +133,20 @@
|
||||
right: var(--hud-offset);
|
||||
transform: translate(calc(100% - var(--hud-offset)), calc(-100% + var(--hud-offset)));
|
||||
}
|
||||
|
||||
.layout-mode-mobile .hud-panel-stats {
|
||||
position: fixed;
|
||||
top: calc(8px + var(--safe-top));
|
||||
right: 8px;
|
||||
width: min(180px, calc(100vw - 16px));
|
||||
z-index: 205;
|
||||
}
|
||||
|
||||
.layout-mode-mobile.earth-search-open .hud-panel-stats,
|
||||
.layout-mode-mobile.earth-settings-open .hud-panel-stats,
|
||||
.layout-mode-mobile.earth-media-open .hud-panel-stats,
|
||||
.layout-mode-mobile.earth-info-open .hud-panel-stats {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: translateY(-12px);
|
||||
}
|
||||
|
||||
@@ -117,6 +117,7 @@
|
||||
.hud-panel-drag-handle {
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.hud-panel-drag-handle:active {
|
||||
@@ -260,6 +261,779 @@
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* ── Mobile popup card ───────────────────────────────────────── */
|
||||
|
||||
.earth-mobile-popup {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.layout-mode-mobile .earth-mobile-popup {
|
||||
display: flex;
|
||||
position: fixed;
|
||||
z-index: 260;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px 10px 12px;
|
||||
max-width: 220px;
|
||||
min-width: 120px;
|
||||
background: linear-gradient(145deg, rgba(14, 25, 42, 0.97), rgba(7, 15, 28, 0.97));
|
||||
border: 1px solid rgba(200, 224, 255, 0.13);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 10px 36px rgba(0, 0, 0, 0.48), 0 0 0 1px rgba(140, 190, 255, 0.05);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
pointer-events: auto;
|
||||
opacity: 0;
|
||||
transform: scale(0.86) translateY(4px);
|
||||
transition: opacity 0.17s ease, transform 0.17s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.layout-mode-mobile .earth-mobile-popup:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.layout-mode-mobile .earth-mobile-popup[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.layout-mode-mobile .earth-mobile-popup.is-visible {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
|
||||
.earth-mobile-popup-icon {
|
||||
font-size: 1.25rem;
|
||||
flex-shrink: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.earth-mobile-popup-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.earth-mobile-popup-title {
|
||||
color: var(--hud-text);
|
||||
font-size: 0.84rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.25;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.earth-mobile-popup-sub {
|
||||
color: var(--hud-text-muted);
|
||||
font-size: 0.70rem;
|
||||
margin-top: 2px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
.earth-mobile-popup-chevron {
|
||||
color: rgba(160, 200, 255, 0.5);
|
||||
font-size: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── Mobile drawer ───────────────────────────────────────────── */
|
||||
|
||||
.earth-mobile-drawer-overlay,
|
||||
.earth-mobile-drawer-shell {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.layout-mode-mobile .earth-mobile-drawer-overlay,
|
||||
.layout-mode-mobile .earth-mobile-drawer-shell {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.earth-mobile-drawer-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: linear-gradient(180deg, rgba(3, 8, 16, 0.06), rgba(3, 8, 16, 0.54));
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.22s ease;
|
||||
z-index: 245;
|
||||
}
|
||||
|
||||
.earth-mobile-drawer-shell {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 250;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.earth-mobile-drawer-sheet {
|
||||
position: relative;
|
||||
min-height: calc(240px + var(--safe-bottom));
|
||||
max-height: min(78vh, calc(100vh - 72px - var(--safe-top)));
|
||||
padding: 10px 14px calc(14px + var(--safe-bottom)) 14px;
|
||||
border-top-left-radius: 28px;
|
||||
border-top-right-radius: 28px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(15, 27, 43, 0.98), rgba(7, 14, 24, 0.98)),
|
||||
rgba(5, 10, 19, 0.92);
|
||||
border-top: 1px solid rgba(220, 238, 255, 0.1);
|
||||
box-shadow: 0 -26px 60px rgba(0, 0, 0, 0.38);
|
||||
transform: translateY(calc(100% - 36px - var(--safe-bottom)));
|
||||
transition: transform 0.28s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
pointer-events: none;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.earth-mobile-drawer-header {
|
||||
padding: 10px 4px 8px;
|
||||
cursor: ns-resize;
|
||||
user-select: none;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.earth-mobile-drawer-grabber {
|
||||
width: 54px;
|
||||
height: 5px;
|
||||
margin: 0 auto;
|
||||
border-radius: 999px;
|
||||
background: rgba(225, 239, 255, 0.18);
|
||||
}
|
||||
|
||||
.earth-mobile-drawer-tabs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.earth-mobile-drawer-tab {
|
||||
min-height: 40px;
|
||||
border: 1px solid rgba(201, 225, 247, 0.12);
|
||||
border-radius: 12px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
color: var(--hud-text-muted);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.03em;
|
||||
cursor: pointer;
|
||||
transition: background 0.18s ease, border-color 0.18s ease, color 0.18s ease;
|
||||
}
|
||||
|
||||
.earth-mobile-drawer-tab.is-active {
|
||||
color: var(--hud-title);
|
||||
background: rgba(122, 180, 255, 0.14);
|
||||
border-color: rgba(122, 180, 255, 0.24);
|
||||
}
|
||||
|
||||
.earth-mobile-drawer-content {
|
||||
position: relative;
|
||||
min-height: 0;
|
||||
max-height: calc(78vh - 108px - var(--safe-bottom));
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.earth-mobile-drawer-slot {
|
||||
display: none;
|
||||
min-height: 0;
|
||||
max-height: inherit;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.earth-mobile-drawer-slot.is-active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.earth-mobile-drawer-slot--situation {
|
||||
display: none;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.earth-mobile-drawer-slot--situation.is-active {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
@keyframes earth-drawer-hint {
|
||||
0% { transform: translateY(calc(100% - 36px - var(--safe-bottom))); }
|
||||
14% { transform: translateY(calc(100% - 36px - var(--safe-bottom) - 22px)); }
|
||||
26% { transform: translateY(calc(100% - 36px - var(--safe-bottom) - 4px)); }
|
||||
38% { transform: translateY(calc(100% - 36px - var(--safe-bottom) - 14px)); }
|
||||
50% { transform: translateY(calc(100% - 36px - var(--safe-bottom) - 1px)); }
|
||||
62% { transform: translateY(calc(100% - 36px - var(--safe-bottom) - 7px)); }
|
||||
76% { transform: translateY(calc(100% - 36px - var(--safe-bottom))); }
|
||||
100% { transform: translateY(calc(100% - 36px - var(--safe-bottom))); }
|
||||
}
|
||||
|
||||
.layout-mode-mobile .earth-mobile-drawer-sheet.is-hinting {
|
||||
animation: earth-drawer-hint 0.85s cubic-bezier(0.22, 1, 0.36, 1) forwards;
|
||||
}
|
||||
|
||||
.layout-mode-mobile.earth-mobile-drawer-open .earth-mobile-drawer-overlay {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.layout-mode-mobile.earth-mobile-drawer-open .earth-mobile-drawer-sheet {
|
||||
transform: translateY(0);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.layout-mode-mobile .earth-left-column {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.layout-mode-mobile #brand-panel {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.layout-mode-mobile #search-modal,
|
||||
.layout-mode-mobile #settings-modal {
|
||||
display: block;
|
||||
position: static;
|
||||
inset: auto;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.layout-mode-mobile #search-backdrop,
|
||||
.layout-mode-mobile #settings-backdrop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.layout-mode-mobile .earth-mobile-drawer-slot > .hud-panel,
|
||||
.layout-mode-mobile .earth-mobile-drawer-slot > .earth-search-sheet,
|
||||
.layout-mode-mobile .earth-mobile-drawer-slot > .earth-settings-sheet {
|
||||
position: relative !important;
|
||||
inset: auto !important;
|
||||
left: auto !important;
|
||||
right: auto !important;
|
||||
top: auto !important;
|
||||
bottom: auto !important;
|
||||
transform: none !important;
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
min-width: 0 !important;
|
||||
max-height: none !important;
|
||||
height: auto !important;
|
||||
margin: 0;
|
||||
pointer-events: auto;
|
||||
opacity: 1 !important;
|
||||
filter: none !important;
|
||||
visibility: visible !important;
|
||||
}
|
||||
|
||||
.layout-mode-mobile .earth-mobile-drawer-slot > .hud-panel {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.layout-mode-mobile .earth-mobile-drawer-slot > .earth-search-sheet {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.layout-mode-mobile .earth-mobile-drawer-slot > .earth-settings-sheet {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.layout-mode-mobile .earth-mobile-drawer-slot .hud-panel-drag-handle {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.layout-mode-mobile .earth-mobile-drawer-slot .tv-panel-edge {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.layout-mode-mobile .earth-mobile-drawer-slot .tv-panel-player {
|
||||
min-height: 220px;
|
||||
}
|
||||
|
||||
.layout-mode-mobile #layer-toggles,
|
||||
.layout-mode-mobile #legend,
|
||||
.layout-mode-mobile #earth-stats,
|
||||
.layout-mode-mobile #media-panel,
|
||||
.layout-mode-mobile #search-modal,
|
||||
.layout-mode-mobile #settings-modal,
|
||||
.layout-mode-mobile #info-panel {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.earth-mobile-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.earth-mobile-page-intro {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.earth-mobile-page-kicker {
|
||||
color: var(--hud-text-muted);
|
||||
font-size: 0.68rem;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.earth-mobile-page-summary {
|
||||
color: var(--hud-text-soft);
|
||||
font-size: 0.84rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.earth-mobile-layer-list,
|
||||
.earth-mobile-news-board-list,
|
||||
.earth-mobile-search-results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.earth-mobile-layer-card,
|
||||
.earth-mobile-action-btn,
|
||||
.earth-mobile-settings-pill,
|
||||
.earth-mobile-search-clear {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.earth-mobile-layer-card {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
border: 1px solid rgba(212, 227, 244, 0.08);
|
||||
border-radius: 18px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.05), transparent),
|
||||
rgba(255, 255, 255, 0.03);
|
||||
color: var(--hud-text);
|
||||
padding: 14px 16px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.earth-mobile-layer-card.is-active {
|
||||
border-color: rgba(122, 180, 255, 0.24);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(122, 180, 255, 0.12), transparent),
|
||||
rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.earth-mobile-layer-card-icon {
|
||||
font-size: 22px;
|
||||
color: var(--hud-accent-strong);
|
||||
}
|
||||
|
||||
.earth-mobile-layer-card-copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.earth-mobile-layer-card-title,
|
||||
.earth-mobile-detail-title,
|
||||
.earth-mobile-tv-title,
|
||||
.earth-mobile-news-focus-label,
|
||||
.earth-mobile-settings-label {
|
||||
color: var(--hud-title);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.earth-mobile-layer-card-subtitle,
|
||||
.earth-mobile-detail-type,
|
||||
.earth-mobile-tv-subtitle,
|
||||
.earth-mobile-settings-subtitle,
|
||||
.earth-mobile-news-focus-coords,
|
||||
.earth-mobile-tv-notes,
|
||||
.earth-mobile-tv-catalog,
|
||||
.earth-mobile-news-board-status,
|
||||
.earth-mobile-search-meta,
|
||||
.earth-mobile-search-empty {
|
||||
color: var(--hud-text-soft);
|
||||
font-size: 0.76rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.earth-mobile-layer-card-status {
|
||||
color: var(--hud-text-muted);
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.earth-mobile-search-shell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 52px;
|
||||
padding: 0 14px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(214, 230, 247, 0.12);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.05), transparent),
|
||||
rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.earth-mobile-search-input {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--hud-title);
|
||||
font: inherit;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.earth-mobile-search-input::placeholder {
|
||||
color: rgba(190, 208, 227, 0.46);
|
||||
}
|
||||
|
||||
.earth-mobile-search-icon {
|
||||
color: var(--hud-text-muted);
|
||||
}
|
||||
|
||||
.earth-mobile-search-clear {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--hud-text-muted);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.earth-mobile-search-clear[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.earth-mobile-stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.earth-mobile-stat-card,
|
||||
.earth-mobile-situation-card,
|
||||
.earth-mobile-news-focus,
|
||||
.earth-mobile-settings-card,
|
||||
.earth-mobile-detail-card,
|
||||
.earth-mobile-tv-meta {
|
||||
border-radius: 18px;
|
||||
border: 1px solid rgba(212, 227, 244, 0.08);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.05), transparent),
|
||||
rgba(255, 255, 255, 0.03);
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.earth-mobile-stat-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.earth-mobile-stat-num {
|
||||
color: var(--hud-title);
|
||||
font-size: 1.26rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.earth-mobile-stat-label,
|
||||
.earth-mobile-situation-card-subtitle,
|
||||
.earth-mobile-news-focus-kicker,
|
||||
.earth-mobile-settings-title,
|
||||
.earth-mobile-tv-status {
|
||||
color: var(--hud-text-muted);
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.earth-mobile-situation-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.earth-mobile-situation-card-title {
|
||||
color: var(--hud-title);
|
||||
font-size: 0.92rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.earth-mobile-situation-legend-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.earth-mobile-situation-status {
|
||||
color: var(--hud-text);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.earth-mobile-news-focus,
|
||||
.earth-mobile-tv-actions,
|
||||
.earth-mobile-news-actions,
|
||||
.earth-mobile-settings-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.earth-mobile-news-focus {
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.earth-mobile-news-source-count {
|
||||
color: var(--hud-accent-strong);
|
||||
font-size: 0.74rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.earth-mobile-news-board-list .news-story-card {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.earth-mobile-news-board-empty[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.earth-mobile-tv-select,
|
||||
.earth-mobile-settings-slider {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.earth-mobile-tv-select {
|
||||
border: 1px solid rgba(201, 225, 247, 0.14);
|
||||
border-radius: 14px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
color: var(--hud-text);
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.earth-mobile-tv-player {
|
||||
position: relative;
|
||||
min-height: 220px;
|
||||
border-radius: 18px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(201, 225, 247, 0.1);
|
||||
background: linear-gradient(180deg, rgba(9, 18, 32, 0.94), rgba(5, 11, 22, 0.94));
|
||||
}
|
||||
|
||||
.earth-mobile-tv-empty,
|
||||
.earth-mobile-tv-iframe,
|
||||
.earth-mobile-tv-video {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.earth-mobile-tv-empty {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
color: var(--hud-text-muted);
|
||||
}
|
||||
|
||||
.earth-mobile-tv-iframe,
|
||||
.earth-mobile-tv-video {
|
||||
border: 0;
|
||||
background: #050a14;
|
||||
}
|
||||
|
||||
.earth-mobile-action-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 42px;
|
||||
padding: 0 14px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(122, 180, 255, 0.22);
|
||||
background: rgba(122, 180, 255, 0.12);
|
||||
color: var(--hud-title);
|
||||
text-decoration: none;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.earth-mobile-action-btn--ghost {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border-color: rgba(212, 227, 244, 0.1);
|
||||
}
|
||||
|
||||
.earth-mobile-settings-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.earth-mobile-settings-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.earth-mobile-settings-card--stacked {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.earth-mobile-settings-copy {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.earth-mobile-settings-segmented {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.earth-mobile-settings-pill {
|
||||
border: 1px solid rgba(212, 227, 244, 0.1);
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
color: var(--hud-text-soft);
|
||||
padding: 10px 14px;
|
||||
}
|
||||
|
||||
.earth-mobile-settings-pill.is-active {
|
||||
color: var(--hud-title);
|
||||
border-color: rgba(122, 180, 255, 0.24);
|
||||
background: rgba(122, 180, 255, 0.14);
|
||||
}
|
||||
|
||||
.earth-mobile-settings-switch {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
width: 44px;
|
||||
height: 28px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.earth-mobile-settings-switch input {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.earth-mobile-settings-switch-track {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.earth-mobile-settings-switch-track::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 4px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
transition: transform 0.18s ease;
|
||||
}
|
||||
|
||||
.earth-mobile-settings-switch input:checked + .earth-mobile-settings-switch-track {
|
||||
background: rgba(122, 180, 255, 0.34);
|
||||
}
|
||||
|
||||
.earth-mobile-settings-switch input:checked + .earth-mobile-settings-switch-track::after {
|
||||
transform: translateX(16px);
|
||||
}
|
||||
|
||||
.earth-mobile-settings-slider-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.earth-mobile-settings-slider-value {
|
||||
color: var(--hud-title);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.earth-mobile-detail-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.earth-mobile-detail-header {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.earth-mobile-detail-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.earth-mobile-detail-heading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.earth-mobile-detail-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.earth-mobile-detail-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.earth-mobile-detail-row-label {
|
||||
color: var(--hud-text-muted);
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.earth-mobile-detail-row-value,
|
||||
.earth-mobile-detail-empty {
|
||||
color: var(--hud-text);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.layout-mode-mobile .earth-status-message,
|
||||
.layout-mode-mobile .earth-error-message {
|
||||
top: calc(var(--safe-top) + 130px);
|
||||
left: auto;
|
||||
right: calc(8px + var(--safe-right));
|
||||
transform: translate(0, -10px);
|
||||
min-width: 0;
|
||||
max-width: min(200px, 52vw);
|
||||
font-size: 0.78rem;
|
||||
padding: 6px 14px 6px 10px;
|
||||
}
|
||||
|
||||
.layout-mode-mobile .earth-status-message.visible,
|
||||
.layout-mode-mobile .earth-error-message.visible {
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
|
||||
.hud-panel-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -1110,6 +1884,30 @@
|
||||
}
|
||||
}
|
||||
|
||||
.layout-mode-mobile .earth-search-sheet {
|
||||
top: calc(8px + var(--safe-top));
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
width: auto;
|
||||
max-height: calc(100vh - var(--safe-top) - var(--safe-bottom) - 16px);
|
||||
transform: translateX(0) translateY(8px) scale(1);
|
||||
}
|
||||
|
||||
.layout-mode-mobile .earth-search-modal.is-open .earth-search-sheet {
|
||||
transform: translateX(0) translateY(0) scale(1);
|
||||
}
|
||||
|
||||
.layout-mode-mobile .earth-settings-sheet {
|
||||
top: auto;
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
bottom: calc(8px + var(--safe-bottom));
|
||||
width: auto;
|
||||
max-width: none;
|
||||
max-height: min(78vh, 720px);
|
||||
border-radius: 24px 24px 18px 18px;
|
||||
}
|
||||
|
||||
/* .earth-left-column layout-expanded rule lives in info-panel.css */
|
||||
/* .hud-panel-legend layout-expanded rule lives in legend.css */
|
||||
/* .hud-panel-stats layout-expanded rule lives in earth-stats.css */
|
||||
|
||||
@@ -367,3 +367,39 @@
|
||||
.earth-app.layout-expanded .earth-left-column {
|
||||
transform: translate(calc(-100% + var(--hud-offset)), 0);
|
||||
}
|
||||
|
||||
.layout-mode-mobile .earth-left-column {
|
||||
top: calc(8px + var(--safe-top));
|
||||
left: 8px;
|
||||
max-width: min(300px, calc(100vw - 16px));
|
||||
}
|
||||
|
||||
.layout-mode-mobile .hud-panel-info {
|
||||
position: fixed;
|
||||
left: 8px !important;
|
||||
right: 8px !important;
|
||||
top: auto !important;
|
||||
bottom: calc(84px + var(--safe-bottom)) !important;
|
||||
width: auto;
|
||||
max-width: none;
|
||||
max-height: min(58vh, 520px);
|
||||
z-index: 240;
|
||||
}
|
||||
|
||||
.layout-mode-mobile .info-card-header {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.layout-mode-mobile .info-card-content {
|
||||
max-height: min(46vh, 420px);
|
||||
}
|
||||
|
||||
.layout-mode-mobile .info-card-property {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.layout-mode-mobile .info-card-value {
|
||||
max-width: none;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@@ -317,3 +317,29 @@
|
||||
|
||||
/* Layout-expanded: layer panel slides off with .earth-left-column — no
|
||||
individual rule needed since the whole column translates together. */
|
||||
|
||||
.layout-mode-mobile .hud-panel-layers {
|
||||
position: fixed;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
bottom: calc(88px + var(--safe-bottom));
|
||||
width: auto;
|
||||
max-height: min(60vh, 520px);
|
||||
margin-top: 0;
|
||||
z-index: 220;
|
||||
transform: translateY(calc(100% + 28px));
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: transform 0.24s ease, opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.layout-mode-mobile .hud-panel-layers.is-mobile-open {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.layout-mode-mobile .layer-panel-body {
|
||||
max-height: min(52vh, 460px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@@ -138,3 +138,24 @@
|
||||
bottom: var(--hud-offset);
|
||||
transform: translate(calc(-100% + var(--hud-offset)), calc(100% - var(--hud-offset)));
|
||||
}
|
||||
|
||||
.layout-mode-mobile .hud-panel-legend {
|
||||
position: fixed;
|
||||
left: 8px;
|
||||
bottom: calc(84px + var(--safe-bottom));
|
||||
width: min(172px, calc(100vw - 16px));
|
||||
z-index: 205;
|
||||
}
|
||||
|
||||
.layout-mode-mobile .legend-list {
|
||||
max-height: min(20vh, 180px);
|
||||
}
|
||||
|
||||
.layout-mode-mobile.earth-search-open .hud-panel-legend,
|
||||
.layout-mode-mobile.earth-settings-open .hud-panel-legend,
|
||||
.layout-mode-mobile.earth-media-open .hud-panel-legend,
|
||||
.layout-mode-mobile.earth-info-open .hud-panel-legend {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: translateY(12px);
|
||||
}
|
||||
|
||||
@@ -105,6 +105,14 @@
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.earth-toolbar-orb:has(#layer-action) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.layout-mode-mobile .earth-toolbar-group {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.earth-toolbar-cluster.is-collapsed .earth-toolbar-orb > * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -285,6 +285,27 @@
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
.layout-mode-mobile .hud-panel-media {
|
||||
position: fixed;
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
top: calc(8px + var(--safe-top));
|
||||
bottom: calc(84px + var(--safe-bottom));
|
||||
width: auto;
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
min-width: 0;
|
||||
z-index: 230;
|
||||
}
|
||||
|
||||
.layout-mode-mobile .tv-panel-player {
|
||||
min-height: min(42vh, 360px);
|
||||
}
|
||||
|
||||
.layout-mode-mobile .tv-panel-edge {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 右下角视觉标记 */
|
||||
.tv-panel-edge[data-edge="br"]::before {
|
||||
content: "";
|
||||
|
||||
@@ -156,6 +156,14 @@
|
||||
<div id="control-toolbar" class="earth-toolbar">
|
||||
<div id="toolbar-cluster" class="earth-toolbar-cluster is-collapsed">
|
||||
<div class="earth-toolbar-orb" data-orb-index="0" style="--orb-delay: 0s;">
|
||||
<button id="layer-action" class="floating-btn liquid-glass-surface earth-toolbar-btn" title="图层">
|
||||
<span class="icon" aria-hidden="true">
|
||||
<span class="material-symbols-rounded">layers</span>
|
||||
</span>
|
||||
<span class="tooltip earth-toolbar-tooltip">图层</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="earth-toolbar-orb" data-orb-index="1" style="--orb-delay: 0.12s;">
|
||||
<button id="search-action" class="floating-btn liquid-glass-surface earth-toolbar-btn" title="搜索">
|
||||
<span class="icon" aria-hidden="true">
|
||||
<span class="material-symbols-rounded">search</span>
|
||||
@@ -163,7 +171,7 @@
|
||||
<span class="tooltip earth-toolbar-tooltip">搜索</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="earth-toolbar-orb" data-orb-index="1" style="--orb-delay: 0.18s;">
|
||||
<div class="earth-toolbar-orb" data-orb-index="2" style="--orb-delay: 0.24s;">
|
||||
<button id="rotate-toggle" class="floating-btn liquid-glass-surface earth-toolbar-btn earth-rotate-toggle" title="自动旋转">
|
||||
<span class="icon rotate-icon icon-pause" aria-hidden="true">
|
||||
<span class="material-symbols-rounded">pause</span>
|
||||
@@ -174,7 +182,7 @@
|
||||
<span class="tooltip earth-toolbar-tooltip">自动旋转</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="earth-toolbar-orb" data-orb-index="2" style="--orb-delay: 0.36s;">
|
||||
<div class="earth-toolbar-orb" data-orb-index="3" style="--orb-delay: 0.36s;">
|
||||
<button id="toggle-tv" class="floating-btn liquid-glass-surface earth-toolbar-btn" title="新闻直播">
|
||||
<span class="icon" aria-hidden="true">
|
||||
<span class="material-symbols-rounded">live_tv</span>
|
||||
@@ -182,7 +190,7 @@
|
||||
<span class="tooltip earth-toolbar-tooltip">打开媒体面板</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="earth-toolbar-orb" data-orb-index="3" style="--orb-delay: 0.54s;">
|
||||
<div class="earth-toolbar-orb" data-orb-index="4" style="--orb-delay: 0.54s;">
|
||||
<button id="reload-data" class="floating-btn liquid-glass-surface earth-toolbar-btn" title="重新加载数据">
|
||||
<span class="icon" aria-hidden="true">
|
||||
<span class="material-symbols-rounded">refresh</span>
|
||||
@@ -190,7 +198,7 @@
|
||||
<span class="tooltip earth-toolbar-tooltip">重新加载数据</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="earth-toolbar-orb earth-toolbar-popover earth-zoom-group" id="zoom-control-group" data-orb-index="4" style="--orb-delay: 0.72s;">
|
||||
<div class="earth-toolbar-orb earth-toolbar-popover earth-zoom-group" id="zoom-control-group" data-orb-index="5" style="--orb-delay: 0.72s;">
|
||||
<button id="zoom-trigger" class="floating-btn liquid-glass-surface earth-toolbar-btn" title="缩放控制">
|
||||
<span class="icon" aria-hidden="true">
|
||||
<span class="material-symbols-rounded">zoom_in</span>
|
||||
@@ -203,7 +211,7 @@
|
||||
<button id="zoom-out" class="liquid-glass-surface earth-zoom-btn" title="缩小" aria-label="缩小"><span aria-hidden="true">−</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="earth-toolbar-orb" data-orb-index="5" style="--orb-delay: 0.9s;">
|
||||
<div class="earth-toolbar-orb" data-orb-index="6" style="--orb-delay: 0.9s;">
|
||||
<button id="settings-trigger" class="floating-btn liquid-glass-surface earth-toolbar-btn" title="设置">
|
||||
<span class="icon" aria-hidden="true">
|
||||
<span class="material-symbols-rounded">settings</span>
|
||||
@@ -211,7 +219,7 @@
|
||||
<span class="tooltip earth-toolbar-tooltip">设置</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="earth-toolbar-orb" data-orb-index="6" style="--orb-delay: 1.08s;">
|
||||
<div class="earth-toolbar-orb" data-orb-index="7" style="--orb-delay: 1.08s;">
|
||||
<button id="reset-view" class="floating-btn liquid-glass-surface earth-toolbar-btn" title="重置视角">
|
||||
<span class="icon" aria-hidden="true">
|
||||
<span class="material-symbols-rounded">my_location</span>
|
||||
@@ -219,7 +227,7 @@
|
||||
<span class="tooltip earth-toolbar-tooltip">重置视角</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="earth-toolbar-orb" data-orb-index="7" style="--orb-delay: 1.26s;">
|
||||
<div class="earth-toolbar-orb" data-orb-index="8" style="--orb-delay: 1.26s;">
|
||||
<button id="layout-toggle" class="floating-btn liquid-glass-surface earth-toolbar-btn earth-layout-toggle" title="最大化布局">
|
||||
<span class="icon layout-icon layout-expand" aria-hidden="true">
|
||||
<span class="material-symbols-rounded">open_in_full</span>
|
||||
@@ -412,6 +420,260 @@
|
||||
|
||||
<div id="status-message" class="earth-status-message" aria-live="polite" aria-atomic="true"></div>
|
||||
<div id="tooltip" class="earth-tooltip"></div>
|
||||
<div id="earth-mobile-popup" class="earth-mobile-popup" hidden aria-live="polite">
|
||||
<span class="earth-mobile-popup-icon" id="earth-mobile-popup-icon"></span>
|
||||
<div class="earth-mobile-popup-body">
|
||||
<div class="earth-mobile-popup-title" id="earth-mobile-popup-title"></div>
|
||||
<div class="earth-mobile-popup-sub" id="earth-mobile-popup-sub"></div>
|
||||
</div>
|
||||
<span class="material-symbols-rounded earth-mobile-popup-chevron">chevron_right</span>
|
||||
</div>
|
||||
<div id="mobile-drawer-overlay" class="earth-mobile-drawer-overlay" hidden></div>
|
||||
<div id="mobile-drawer-shell" class="earth-mobile-drawer-shell" aria-hidden="true">
|
||||
<div class="earth-mobile-drawer-sheet">
|
||||
<div id="mobile-drawer-handle" class="earth-mobile-drawer-header">
|
||||
<div class="earth-mobile-drawer-grabber" aria-hidden="true"></div>
|
||||
</div>
|
||||
<div class="earth-mobile-drawer-tabs" role="tablist" aria-label="移动端菜单">
|
||||
<button class="earth-mobile-drawer-tab is-active" type="button" role="tab" data-drawer-card="layers" aria-selected="true">图层</button>
|
||||
<button class="earth-mobile-drawer-tab" type="button" role="tab" data-drawer-card="search" aria-selected="false">搜索</button>
|
||||
<button class="earth-mobile-drawer-tab" type="button" role="tab" data-drawer-card="situation" aria-selected="false">态势</button>
|
||||
<button class="earth-mobile-drawer-tab" type="button" role="tab" data-drawer-card="news" aria-selected="false">新闻</button>
|
||||
<button class="earth-mobile-drawer-tab" type="button" role="tab" data-drawer-card="tv" aria-selected="false">TV</button>
|
||||
<button class="earth-mobile-drawer-tab" type="button" role="tab" data-drawer-card="settings" aria-selected="false">设置</button>
|
||||
</div>
|
||||
<div class="earth-mobile-drawer-content">
|
||||
<section class="earth-mobile-drawer-slot is-active" data-drawer-slot="layers">
|
||||
<div class="earth-mobile-page earth-mobile-page--layers">
|
||||
<div class="earth-mobile-page-intro">
|
||||
<span class="earth-mobile-page-kicker">Layer Control</span>
|
||||
<span id="mobile-layer-summary" class="earth-mobile-page-summary">已启用 0 个图层</span>
|
||||
</div>
|
||||
<div id="mobile-layer-list" class="earth-mobile-layer-list"></div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="earth-mobile-drawer-slot" data-drawer-slot="search">
|
||||
<div class="earth-mobile-page earth-mobile-page--search">
|
||||
<div class="earth-mobile-page-intro">
|
||||
<span class="earth-mobile-page-kicker">Object Search</span>
|
||||
<span class="earth-mobile-page-summary">搜索海缆、登陆点、卫星和 BGP 事件</span>
|
||||
</div>
|
||||
<div class="earth-mobile-search-shell">
|
||||
<span class="material-symbols-rounded earth-mobile-search-icon" aria-hidden="true">search</span>
|
||||
<input
|
||||
id="mobile-earth-search-input"
|
||||
class="earth-mobile-search-input"
|
||||
type="text"
|
||||
inputmode="search"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
placeholder="输入名称、地点、NORAD、ASN..."
|
||||
>
|
||||
<button id="mobile-earth-search-clear" class="earth-mobile-search-clear" type="button" aria-label="清除搜索" hidden>
|
||||
<span class="material-symbols-rounded">close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="mobile-earth-search-meta" class="earth-mobile-search-meta">输入关键词以搜索当前地球对象</div>
|
||||
<div id="mobile-earth-search-results" class="earth-mobile-search-results" role="listbox" aria-label="移动端搜索结果"></div>
|
||||
<div id="mobile-earth-search-empty" class="earth-mobile-search-empty">支持搜索海缆、登陆点、卫星、BGP 事件与观测站。</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="earth-mobile-drawer-slot earth-mobile-drawer-slot--situation" data-drawer-slot="situation">
|
||||
<div class="earth-mobile-page earth-mobile-page--situation">
|
||||
<div class="earth-mobile-page-intro">
|
||||
<span class="earth-mobile-page-kicker">Situation</span>
|
||||
<span class="earth-mobile-page-summary">面向移动端整合的全球态势概览</span>
|
||||
</div>
|
||||
<div class="earth-mobile-stats-grid">
|
||||
<div class="earth-mobile-stat-card">
|
||||
<span id="mobile-cable-count" class="earth-mobile-stat-num">—</span>
|
||||
<span class="earth-mobile-stat-label">海缆系统</span>
|
||||
</div>
|
||||
<div class="earth-mobile-stat-card">
|
||||
<span id="mobile-landing-point-count" class="earth-mobile-stat-num">—</span>
|
||||
<span class="earth-mobile-stat-label">登陆点</span>
|
||||
</div>
|
||||
<div class="earth-mobile-stat-card">
|
||||
<span id="mobile-satellite-count" class="earth-mobile-stat-num">—</span>
|
||||
<span class="earth-mobile-stat-label">在轨卫星</span>
|
||||
</div>
|
||||
<div class="earth-mobile-stat-card">
|
||||
<span id="mobile-bgp-anomaly-count" class="earth-mobile-stat-num">—</span>
|
||||
<span class="earth-mobile-stat-label">BGP 事件</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="earth-mobile-situation-card">
|
||||
<div class="earth-mobile-situation-card-title">图例</div>
|
||||
<div id="mobile-situation-legend-mode" class="earth-mobile-situation-card-subtitle">海缆</div>
|
||||
<div id="mobile-situation-legend-list" class="earth-mobile-situation-legend-list"></div>
|
||||
</div>
|
||||
<div class="earth-mobile-situation-card">
|
||||
<div class="earth-mobile-situation-card-title">BGP 状态</div>
|
||||
<div id="mobile-bgp-status-summary" class="earth-mobile-situation-status">暂无观测数据</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="earth-mobile-drawer-slot" data-drawer-slot="news">
|
||||
<div class="earth-mobile-page earth-mobile-page--news">
|
||||
<div class="earth-mobile-page-intro">
|
||||
<span class="earth-mobile-page-kicker">News</span>
|
||||
<span class="earth-mobile-page-summary">跟随当前视角聚焦全球区域新闻</span>
|
||||
</div>
|
||||
<div class="earth-mobile-news-focus">
|
||||
<div>
|
||||
<div class="earth-mobile-news-focus-kicker">当前关注区域</div>
|
||||
<div id="mobile-news-focus-label" class="earth-mobile-news-focus-label">全球焦点</div>
|
||||
<div id="mobile-news-focus-coords" class="earth-mobile-news-focus-coords">跟随当前视角自动聚焦</div>
|
||||
</div>
|
||||
<div id="mobile-news-source-count" class="earth-mobile-news-source-count">0 路聚合源</div>
|
||||
</div>
|
||||
<div id="mobile-news-board-status" class="earth-mobile-news-board-status">正在准备全球态势新闻...</div>
|
||||
<div id="mobile-news-board-list" class="earth-mobile-news-board-list"></div>
|
||||
<div id="mobile-news-board-empty" class="earth-mobile-news-board-empty" hidden>正在准备全球态势新闻聚合源...</div>
|
||||
<div class="earth-mobile-news-actions">
|
||||
<button id="mobile-news-refresh" class="earth-mobile-action-btn" type="button">刷新</button>
|
||||
<button id="mobile-news-open-external" class="earth-mobile-action-btn" type="button">打开源站</button>
|
||||
</div>
|
||||
<a id="mobile-news-feed-anchor" hidden rel="noreferrer noopener" target="_blank"></a>
|
||||
</div>
|
||||
</section>
|
||||
<section class="earth-mobile-drawer-slot" data-drawer-slot="tv">
|
||||
<div class="earth-mobile-page earth-mobile-page--tv">
|
||||
<div class="earth-mobile-page-intro">
|
||||
<span class="earth-mobile-page-kicker">TV</span>
|
||||
<span class="earth-mobile-page-summary">移动端新闻直播和频道切换</span>
|
||||
</div>
|
||||
<select id="mobile-tv-source-select" class="earth-mobile-tv-select" aria-label="选择移动端新闻直播源"></select>
|
||||
<div class="earth-mobile-tv-meta">
|
||||
<span id="mobile-tv-source-status" class="earth-mobile-tv-status">等待加载直播源</span>
|
||||
<div id="mobile-tv-source-title" class="earth-mobile-tv-title">暂无可用频道</div>
|
||||
<div id="mobile-tv-source-meta" class="earth-mobile-tv-subtitle">当前未配置可播放新闻直播源</div>
|
||||
<div id="mobile-tv-source-catalog" class="earth-mobile-tv-catalog">频道目录待同步</div>
|
||||
<div id="mobile-tv-source-notes" class="earth-mobile-tv-notes">支持后台配置默认源与采集器补充源。</div>
|
||||
</div>
|
||||
<div class="earth-mobile-tv-player">
|
||||
<div id="mobile-tv-empty-state" class="earth-mobile-tv-empty">暂无可播放直播源,请先在系统配置中添加频道。</div>
|
||||
<iframe
|
||||
id="mobile-tv-iframe"
|
||||
class="earth-mobile-tv-iframe"
|
||||
hidden
|
||||
title="移动端新闻直播"
|
||||
referrerpolicy="strict-origin-when-cross-origin"
|
||||
allow="autoplay; fullscreen; picture-in-picture"
|
||||
></iframe>
|
||||
<video id="mobile-tv-video" class="earth-mobile-tv-video" hidden controls autoplay muted playsinline></video>
|
||||
</div>
|
||||
<div class="earth-mobile-tv-actions">
|
||||
<button id="mobile-tv-refresh" class="earth-mobile-action-btn" type="button">刷新</button>
|
||||
<button id="mobile-tv-open-external" class="earth-mobile-action-btn" type="button">访问官网</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="earth-mobile-drawer-slot" data-drawer-slot="settings">
|
||||
<div class="earth-mobile-page earth-mobile-page--settings">
|
||||
<div class="earth-mobile-page-intro">
|
||||
<span class="earth-mobile-page-kicker">Settings</span>
|
||||
<span class="earth-mobile-page-summary">仅保留移动端仍有意义的 Earth 配置</span>
|
||||
</div>
|
||||
<div class="earth-mobile-settings-group">
|
||||
<div class="earth-mobile-settings-title">旋转</div>
|
||||
<div class="earth-mobile-settings-card earth-mobile-settings-card--stacked">
|
||||
<div class="earth-mobile-settings-copy">
|
||||
<span class="earth-mobile-settings-label">旋转模式</span>
|
||||
<span class="earth-mobile-settings-subtitle">巡航模式会按 BGP 事件轮播聚焦</span>
|
||||
</div>
|
||||
<div class="earth-mobile-settings-segmented" role="group" aria-label="移动端选择旋转模式">
|
||||
<button type="button" class="earth-mobile-settings-pill is-active" data-rotation-mode="rotate" aria-pressed="true">旋转模式</button>
|
||||
<button type="button" class="earth-mobile-settings-pill" data-rotation-mode="cruise" aria-pressed="false">巡航模式</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="earth-mobile-settings-group">
|
||||
<div class="earth-mobile-settings-title">视图</div>
|
||||
<label class="earth-mobile-settings-card">
|
||||
<div class="earth-mobile-settings-copy">
|
||||
<span class="earth-mobile-settings-label">日夜模式</span>
|
||||
<span class="earth-mobile-settings-subtitle">按真实太阳位置区分地球昼夜明暗</span>
|
||||
</div>
|
||||
<span class="earth-mobile-settings-switch">
|
||||
<input type="checkbox" data-daynight-toggle checked>
|
||||
<span class="earth-mobile-settings-switch-track"></span>
|
||||
</span>
|
||||
</label>
|
||||
<div class="earth-mobile-settings-card earth-mobile-settings-card--stacked">
|
||||
<div class="earth-mobile-settings-copy">
|
||||
<span class="earth-mobile-settings-label">地球默认大小</span>
|
||||
<span class="earth-mobile-settings-subtitle">用于重置视角、缩放重置和巡航视图</span>
|
||||
</div>
|
||||
<div class="earth-mobile-settings-slider-row">
|
||||
<input
|
||||
class="earth-mobile-settings-slider"
|
||||
type="range"
|
||||
min="0.5"
|
||||
max="5"
|
||||
step="0.01"
|
||||
value="1"
|
||||
data-default-earth-size-slider
|
||||
aria-label="移动端调整地球默认大小"
|
||||
>
|
||||
<span class="earth-mobile-settings-slider-value" data-default-earth-size-value>100%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="earth-mobile-settings-group">
|
||||
<div class="earth-mobile-settings-title">地形</div>
|
||||
<div class="earth-mobile-settings-card earth-mobile-settings-card--stacked">
|
||||
<div class="earth-mobile-settings-copy">
|
||||
<span class="earth-mobile-settings-label">地形透明度</span>
|
||||
<span class="earth-mobile-settings-subtitle">调高后会呈现更明显的绿色地形覆盖效果</span>
|
||||
</div>
|
||||
<div class="earth-mobile-settings-slider-row">
|
||||
<input
|
||||
class="earth-mobile-settings-slider"
|
||||
type="range"
|
||||
min="0.05"
|
||||
max="1"
|
||||
step="0.01"
|
||||
value="0.62"
|
||||
data-terrain-opacity-slider
|
||||
aria-label="移动端调整地形透明度"
|
||||
>
|
||||
<span class="earth-mobile-settings-slider-value" data-terrain-opacity-value>62%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="earth-mobile-settings-group">
|
||||
<div class="earth-mobile-settings-title">系统</div>
|
||||
<div class="earth-mobile-settings-actions">
|
||||
<button id="mobile-settings-reset" class="earth-mobile-action-btn earth-mobile-action-btn--ghost" type="button">重置设置</button>
|
||||
<a class="earth-mobile-action-btn" href="/admin" target="_blank" rel="noreferrer noopener">打开 Admin</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="earth-mobile-drawer-slot" data-drawer-slot="details">
|
||||
<div class="earth-mobile-page earth-mobile-page--details">
|
||||
<div class="earth-mobile-page-intro">
|
||||
<span class="earth-mobile-page-kicker">Details</span>
|
||||
<span class="earth-mobile-page-summary">点击地球对象后查看统一详情</span>
|
||||
</div>
|
||||
<div class="earth-mobile-detail-card">
|
||||
<div class="earth-mobile-detail-header">
|
||||
<span id="mobile-info-card-icon" class="earth-mobile-detail-icon">🛰️</span>
|
||||
<div class="earth-mobile-detail-heading">
|
||||
<div id="mobile-info-card-title" class="earth-mobile-detail-title">对象详情</div>
|
||||
<div id="mobile-info-card-type" class="earth-mobile-detail-type">等待选择对象</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mobile-info-card-content" class="earth-mobile-detail-content">
|
||||
<div class="earth-mobile-detail-empty">点击海缆、BGP 事件或卫星后在这里查看详情。</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="search-modal" class="earth-search-modal" aria-hidden="true">
|
||||
<div id="search-backdrop" class="earth-search-backdrop"></div>
|
||||
<div class="earth-search-sheet hud-panel" role="dialog" aria-modal="true" aria-label="搜索">
|
||||
|
||||
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() {
|
||||
|
||||
@@ -4,6 +4,189 @@ import { showStatusMessage } from './ui.js';
|
||||
let currentType = null;
|
||||
let cardMounted = false;
|
||||
|
||||
// ── Mobile popup ─────────────────────────────────────────────
|
||||
|
||||
function getMobilePopupTitle(type, data) {
|
||||
switch (type) {
|
||||
case 'cable': return data.name || '海缆';
|
||||
case 'landing_point': return data.name || '登陆点';
|
||||
case 'satellite': return data.name || '卫星';
|
||||
case 'bgp': return data.anomaly_type || 'BGP事件';
|
||||
case 'bgp_collector': return data.collector || 'BGP观测站';
|
||||
case 'supercomputer': return data.name || '超算';
|
||||
case 'gpu_cluster': return data.name || 'GPU集群';
|
||||
default: return '详情';
|
||||
}
|
||||
}
|
||||
|
||||
function getMobilePopupSubtitle(type, data) {
|
||||
switch (type) {
|
||||
case 'cable': return data.owner || data.status || '海缆';
|
||||
case 'landing_point': return data.country || '登陆点';
|
||||
case 'satellite': return data.norad_id ? `NORAD ${data.norad_id}` : '卫星';
|
||||
case 'bgp': return data.severity || 'BGP路由异常';
|
||||
case 'bgp_collector': return data.location || 'BGP观测站';
|
||||
case 'supercomputer': return data.country || '超级计算机';
|
||||
case 'gpu_cluster': return data.country || 'GPU集群';
|
||||
default: return '';
|
||||
}
|
||||
}
|
||||
|
||||
function positionMobilePopup(popup, touchX, touchY) {
|
||||
const margin = 14;
|
||||
const drawerClearance = 52;
|
||||
const vpW = window.innerWidth;
|
||||
const vpH = window.innerHeight;
|
||||
const safeBottom = parseFloat(
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--safe-bottom')
|
||||
) || 0;
|
||||
const bottomBound = vpH - drawerClearance - safeBottom;
|
||||
|
||||
// Measure actual popup size (it's rendered but invisible via opacity)
|
||||
const popW = popup.offsetWidth || 200;
|
||||
const popH = popup.offsetHeight || 68;
|
||||
|
||||
const gap = 22;
|
||||
const spaceRight = vpW - touchX;
|
||||
const spaceLeft = touchX;
|
||||
const spaceBottom = bottomBound - touchY;
|
||||
const spaceTop = touchY;
|
||||
|
||||
let left, top;
|
||||
|
||||
// Horizontal: side with more room
|
||||
if (spaceRight >= popW + gap + margin) {
|
||||
left = touchX + gap;
|
||||
} else if (spaceLeft >= popW + gap + margin) {
|
||||
left = touchX - gap - popW;
|
||||
} else {
|
||||
left = Math.max(margin, Math.min(touchX - popW / 2, vpW - popW - margin));
|
||||
}
|
||||
|
||||
// Vertical: prefer above touch, then below
|
||||
if (spaceTop >= popH + gap + margin) {
|
||||
top = touchY - gap - popH;
|
||||
} else if (spaceBottom >= popH + gap + margin) {
|
||||
top = touchY + gap;
|
||||
} else {
|
||||
top = Math.max(margin, Math.min(touchY - popH / 2, bottomBound - popH - margin));
|
||||
}
|
||||
|
||||
left = Math.max(margin, Math.min(left, vpW - popW - margin));
|
||||
top = Math.max(margin, Math.min(top, bottomBound - popH - margin));
|
||||
|
||||
popup.style.left = `${left}px`;
|
||||
popup.style.top = `${top}px`;
|
||||
}
|
||||
|
||||
let popupShowToken = 0;
|
||||
|
||||
function showMobilePopup(type, data, x, y) {
|
||||
// Require coordinates — skip if called without position (e.g. from handleCableClick)
|
||||
if (x == null || y == null) return;
|
||||
|
||||
const popup = document.getElementById('earth-mobile-popup');
|
||||
const iconEl = document.getElementById('earth-mobile-popup-icon');
|
||||
const titleEl = document.getElementById('earth-mobile-popup-title');
|
||||
const subEl = document.getElementById('earth-mobile-popup-sub');
|
||||
if (!popup || !iconEl || !titleEl || !subEl) return;
|
||||
|
||||
const config = CARD_CONFIG[type];
|
||||
if (!config) return;
|
||||
|
||||
iconEl.textContent = config.icon;
|
||||
titleEl.textContent = getMobilePopupTitle(type, data);
|
||||
subEl.textContent = getMobilePopupSubtitle(type, data);
|
||||
|
||||
// Invalidate any in-flight hide listener
|
||||
popupShowToken += 1;
|
||||
const token = popupShowToken;
|
||||
|
||||
popup.removeAttribute('hidden');
|
||||
popup.classList.remove('is-visible');
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
positionMobilePopup(popup, x, y);
|
||||
requestAnimationFrame(() => {
|
||||
if (token !== popupShowToken) return; // superseded
|
||||
popup.classList.add('is-visible');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function hideMobilePopup() {
|
||||
const popup = document.getElementById('earth-mobile-popup');
|
||||
if (!popup) return;
|
||||
popupShowToken += 1; // invalidate any pending show
|
||||
popup.classList.remove('is-visible');
|
||||
popup.addEventListener('transitionend', () => {
|
||||
if (!popup.classList.contains('is-visible')) {
|
||||
popup.setAttribute('hidden', '');
|
||||
}
|
||||
}, { once: true });
|
||||
}
|
||||
|
||||
let popupClickBound = false;
|
||||
function ensurePopupClickHandler() {
|
||||
if (popupClickBound) return;
|
||||
popupClickBound = true;
|
||||
const popup = document.getElementById('earth-mobile-popup');
|
||||
if (!popup) return;
|
||||
|
||||
let dragPointerId = null;
|
||||
let startX = 0, startY = 0;
|
||||
let startLeft = 0, startTop = 0;
|
||||
let dragged = false;
|
||||
const DRAG_THRESHOLD = 10;
|
||||
|
||||
popup.addEventListener('pointerdown', (e) => {
|
||||
if (e.button > 0) return;
|
||||
e.stopPropagation();
|
||||
dragPointerId = e.pointerId;
|
||||
startX = e.clientX;
|
||||
startY = e.clientY;
|
||||
const rect = popup.getBoundingClientRect();
|
||||
startLeft = rect.left;
|
||||
startTop = rect.top;
|
||||
dragged = false;
|
||||
});
|
||||
|
||||
// Track drag at document level so pointer can leave popup bounds
|
||||
document.addEventListener('pointermove', (e) => {
|
||||
if (e.pointerId !== dragPointerId) return;
|
||||
const dx = e.clientX - startX;
|
||||
const dy = e.clientY - startY;
|
||||
if (Math.hypot(dx, dy) < DRAG_THRESHOLD) return;
|
||||
dragged = true;
|
||||
e.stopPropagation();
|
||||
const margin = 8;
|
||||
const left = Math.max(margin, Math.min(startLeft + dx, window.innerWidth - popup.offsetWidth - margin));
|
||||
const top = Math.max(margin, Math.min(startTop + dy, window.innerHeight - popup.offsetHeight - margin));
|
||||
popup.style.left = `${left}px`;
|
||||
popup.style.top = `${top}px`;
|
||||
});
|
||||
|
||||
document.addEventListener('pointerup', (e) => {
|
||||
if (e.pointerId !== dragPointerId) return;
|
||||
const wasDragged = dragged;
|
||||
dragPointerId = null;
|
||||
dragged = false;
|
||||
if (!wasDragged) {
|
||||
window.dispatchEvent(new CustomEvent('earth:open-details-tab'));
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('pointercancel', (e) => {
|
||||
if (e.pointerId === dragPointerId) {
|
||||
dragPointerId = null;
|
||||
dragged = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Block click from bubbling to document (which would close the drawer)
|
||||
popup.addEventListener('click', (e) => e.stopPropagation());
|
||||
}
|
||||
|
||||
const CARD_CONFIG = {
|
||||
cable: {
|
||||
icon: '🛥️',
|
||||
@@ -126,19 +309,31 @@ function setupInfoCardDrag(panel) {
|
||||
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;
|
||||
event.preventDefault();
|
||||
const appRect = app.getBoundingClientRect();
|
||||
const panelRect = panel.getBoundingClientRect();
|
||||
const nextLeft = Math.min(
|
||||
@@ -155,7 +350,9 @@ function setupInfoCardDrag(panel) {
|
||||
|
||||
handle.addEventListener('pointerdown', (event) => {
|
||||
if (event.target.closest('.hud-panel-close, .info-card-close')) return;
|
||||
event.preventDefault();
|
||||
isDragging = true;
|
||||
activePointerId = event.pointerId;
|
||||
startPointerX = event.clientX;
|
||||
startPointerY = event.clientY;
|
||||
const appRect = app.getBoundingClientRect();
|
||||
@@ -171,9 +368,9 @@ function setupInfoCardDrag(panel) {
|
||||
handle.setPointerCapture?.(event.pointerId);
|
||||
});
|
||||
|
||||
handle.addEventListener('pointermove', onMove);
|
||||
handle.addEventListener('pointerup', stopDragging);
|
||||
handle.addEventListener('pointercancel', stopDragging);
|
||||
window.addEventListener('pointermove', onMove, { passive: false });
|
||||
window.addEventListener('pointerup', stopDragging);
|
||||
window.addEventListener('pointercancel', stopDragging);
|
||||
handle.addEventListener('lostpointercapture', stopDragging);
|
||||
}
|
||||
|
||||
@@ -252,6 +449,13 @@ function mountCard() {
|
||||
|
||||
function positionPanel(panel, x, y, options = {}) {
|
||||
if (!panel) return;
|
||||
if (document.body.classList.contains('layout-mode-mobile')) {
|
||||
panel.style.left = '8px';
|
||||
panel.style.right = '8px';
|
||||
panel.style.top = 'auto';
|
||||
panel.style.bottom = 'calc(84px + env(safe-area-inset-bottom, 0px))';
|
||||
return;
|
||||
}
|
||||
const margin = 12;
|
||||
const offset = 14;
|
||||
const vpW = window.innerWidth;
|
||||
@@ -296,11 +500,19 @@ function showPanel(x, y, options = {}) {
|
||||
if (!panel) return;
|
||||
if (x != null && y != null) positionPanel(panel, x, y, options);
|
||||
panel.classList.add('is-visible');
|
||||
document.body.classList.add('earth-info-open');
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('earth:info-card-visibility-change', { detail: { visible: true } })
|
||||
);
|
||||
}
|
||||
|
||||
function hidePanel() {
|
||||
const panel = getPanel();
|
||||
if (panel) panel.classList.remove('is-visible');
|
||||
document.body.classList.remove('earth-info-open');
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('earth:info-card-visibility-change', { detail: { visible: false } })
|
||||
);
|
||||
}
|
||||
|
||||
// No-op: event binding now happens lazily in mountCard()
|
||||
@@ -320,6 +532,51 @@ export function showInfoCard(type, data, options = {}) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (document.body.classList.contains('layout-mode-mobile')) {
|
||||
currentType = type;
|
||||
|
||||
// Fill drawer details slot (accessible when user taps popup → opens details tab)
|
||||
const icon = document.getElementById('mobile-info-card-icon');
|
||||
const title = document.getElementById('mobile-info-card-title');
|
||||
const typeLabel = document.getElementById('mobile-info-card-type');
|
||||
const content = document.getElementById('mobile-info-card-content');
|
||||
|
||||
if (icon) icon.textContent = config.icon;
|
||||
if (title) title.textContent = config.title;
|
||||
if (typeLabel) typeLabel.textContent = type.replaceAll('_', ' ');
|
||||
|
||||
if (content) {
|
||||
let html = '';
|
||||
for (const field of config.fields) {
|
||||
let value = data[field.key];
|
||||
if (value === undefined || value === null || value === '') {
|
||||
value = '-';
|
||||
} else if (typeof value === 'number') {
|
||||
value = value.toLocaleString();
|
||||
}
|
||||
if (field.unit && value !== '-') value = value + ' ' + field.unit;
|
||||
html += `
|
||||
<div class="earth-mobile-detail-row">
|
||||
<span class="earth-mobile-detail-row-label">${field.label}</span>
|
||||
<span class="earth-mobile-detail-row-value">${value}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
content.innerHTML = html;
|
||||
}
|
||||
|
||||
// Show the floating mini popup near the touch point (requires coordinates)
|
||||
if (options.x != null && options.y != null) {
|
||||
ensurePopupClickHandler();
|
||||
showMobilePopup(type, data, options.x, options.y);
|
||||
document.body.classList.add('earth-info-open');
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('earth:info-card-visibility-change', { detail: { visible: true } })
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
mountCard();
|
||||
|
||||
currentType = type;
|
||||
@@ -359,6 +616,15 @@ export function showInfoCard(type, data, options = {}) {
|
||||
}
|
||||
|
||||
export function hideInfoCard() {
|
||||
if (document.body.classList.contains('layout-mode-mobile')) {
|
||||
hideMobilePopup();
|
||||
document.body.classList.remove('earth-info-open');
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('earth:info-card-visibility-change', { detail: { visible: false } })
|
||||
);
|
||||
currentType = null;
|
||||
return;
|
||||
}
|
||||
hidePanel();
|
||||
currentType = null;
|
||||
}
|
||||
|
||||
@@ -62,17 +62,18 @@ export function setLegendItems(mode, items) {
|
||||
}
|
||||
|
||||
function syncCurrentLabel(mode) {
|
||||
const labelEl = document.getElementById("legend-current-label");
|
||||
if (!labelEl) return;
|
||||
labelEl.textContent = LEGEND_MODES[mode]?.title || LEGEND_MODES.cables.title;
|
||||
const nextLabel = LEGEND_MODES[mode]?.title || LEGEND_MODES.cables.title;
|
||||
[document.getElementById("legend-current-label"), document.getElementById("mobile-situation-legend-mode")]
|
||||
.forEach((labelEl) => {
|
||||
if (labelEl) {
|
||||
labelEl.textContent = nextLabel;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function renderLegend(mode) {
|
||||
const listEl = document.querySelector("#legend .legend-list");
|
||||
if (!listEl) return;
|
||||
|
||||
const items = legendItemsByMode[mode] || [];
|
||||
listEl.innerHTML = items
|
||||
const html = items
|
||||
.map(
|
||||
(item) => `
|
||||
<div class="legend-item">
|
||||
@@ -81,4 +82,14 @@ function renderLegend(mode) {
|
||||
</div>`,
|
||||
)
|
||||
.join("");
|
||||
|
||||
const desktopList = document.querySelector("#legend .legend-list");
|
||||
if (desktopList) {
|
||||
desktopList.innerHTML = html;
|
||||
}
|
||||
|
||||
const mobileList = document.getElementById("mobile-situation-legend-list");
|
||||
if (mobileList) {
|
||||
mobileList.innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,6 +137,7 @@ import {
|
||||
applyImmediateView,
|
||||
focusEarthView,
|
||||
getZoomLevel,
|
||||
setZoomLevel,
|
||||
teardownControls,
|
||||
} from "./controls.js";
|
||||
import {
|
||||
@@ -206,6 +207,11 @@ let cruisePollTimerId = null;
|
||||
let cruiseConnector = null;
|
||||
let cruiseBGPAdapter = null;
|
||||
let cruiseSequencer = null;
|
||||
let activeDragPointerId = null;
|
||||
let activeTouchPoints = new Map();
|
||||
let pinchGesture = null;
|
||||
let pointerDragDistance = 0;
|
||||
let suppressNextClick = false;
|
||||
|
||||
const clock = new THREE.Clock();
|
||||
const interactionRaycaster = new THREE.Raycaster();
|
||||
@@ -226,6 +232,7 @@ const ACTIVE_BGP_TOOLTIP_TEXT = "隐藏BGP观测";
|
||||
const TOOLTIP_CURSOR_OFFSET = 14; // px offset from cursor for hover tooltips
|
||||
const TOOLTIP_COORDS_OFFSET = 10; // px offset for earth-coordinate tooltip
|
||||
const RELATED_SATELLITE_HIGHLIGHT_COLOR = "#7dd3fc";
|
||||
const DRAG_POINTER_THRESHOLD_PX = 8;
|
||||
const HUD_INTERACTIVE_SELECTORS = [
|
||||
".earth-left-column",
|
||||
".earth-left-column *",
|
||||
@@ -239,6 +246,8 @@ const HUD_INTERACTIVE_SELECTORS = [
|
||||
"#earth-stats *",
|
||||
"#media-panel",
|
||||
"#media-panel *",
|
||||
"#mobile-drawer-shell",
|
||||
"#mobile-drawer-shell *",
|
||||
];
|
||||
|
||||
function bindListener(target, eventName, handler, options) {
|
||||
@@ -276,6 +285,13 @@ function getDragRotationFactor() {
|
||||
return CONFIG.dragRotationFactorBase * scale;
|
||||
}
|
||||
|
||||
function getTouchDistance(firstPoint, secondPoint) {
|
||||
return Math.hypot(
|
||||
secondPoint.clientX - firstPoint.clientX,
|
||||
secondPoint.clientY - firstPoint.clientY,
|
||||
);
|
||||
}
|
||||
|
||||
function disposeMaterial(material) {
|
||||
if (!material) return;
|
||||
if (Array.isArray(material)) {
|
||||
@@ -1026,10 +1042,12 @@ function updateBGPHud(bgpResult) {
|
||||
bgpCollectorEl.textContent = `${bgpResult.collectorCount} 个`;
|
||||
}
|
||||
|
||||
const bgpStatusEl = document.getElementById("bgp-status-summary");
|
||||
if (bgpStatusEl) {
|
||||
bgpStatusEl.textContent = getBGPStatusText(bgpResult);
|
||||
}
|
||||
["bgp-status-summary", "mobile-bgp-status-summary"].forEach((id) => {
|
||||
const bgpStatusEl = document.getElementById(id);
|
||||
if (bgpStatusEl) {
|
||||
bgpStatusEl.textContent = getBGPStatusText(bgpResult);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function ensureCruiseConnector() {
|
||||
@@ -1392,10 +1410,12 @@ function updateSatelliteToggleUi(enabled, satelliteCount = getSatelliteCount())
|
||||
});
|
||||
}
|
||||
|
||||
const satelliteCountEl = document.getElementById("satellite-count");
|
||||
if (satelliteCountEl) {
|
||||
satelliteCountEl.textContent = `${satelliteCount} 颗`;
|
||||
}
|
||||
["satellite-count", "mobile-satellite-count"].forEach((id) => {
|
||||
const satelliteCountEl = document.getElementById(id);
|
||||
if (satelliteCountEl) {
|
||||
satelliteCountEl.textContent = `${satelliteCount} 颗`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateCableToggleUi(enabled) {
|
||||
@@ -1974,9 +1994,9 @@ export async function setSatellitesEnabled(
|
||||
|
||||
function setupEventListeners() {
|
||||
const handleResize = () => onWindowResize();
|
||||
const handleMouseMove = (event) => onMouseMove(event);
|
||||
const handleMouseDown = (event) => onMouseDown(event);
|
||||
const handleMouseUp = () => onMouseUp();
|
||||
const handlePointerMove = (event) => onPointerMove(event);
|
||||
const handlePointerDown = (event) => onPointerDown(event);
|
||||
const handlePointerUp = (event) => onPointerUp(event);
|
||||
const handleMouseLeave = () => onMouseLeave();
|
||||
const handleClick = (event) => onClick(event);
|
||||
const handlePageHide = () => destroy();
|
||||
@@ -1986,11 +2006,15 @@ function setupEventListeners() {
|
||||
bindListener(window, "pagehide", handlePageHide);
|
||||
bindListener(window, "beforeunload", handlePageHide);
|
||||
bindListener(window, "earth:rotation-mode-change", handleRotationMode);
|
||||
bindListener(window, "mousemove", handleMouseMove);
|
||||
bindListener(renderer.domElement, "mousedown", handleMouseDown);
|
||||
bindListener(window, "mouseup", handleMouseUp);
|
||||
bindListener(renderer.domElement, "pointerdown", handlePointerDown);
|
||||
bindListener(window, "pointermove", handlePointerMove);
|
||||
bindListener(window, "pointerup", handlePointerUp);
|
||||
bindListener(window, "pointercancel", handlePointerUp);
|
||||
bindListener(renderer.domElement, "mouseleave", handleMouseLeave);
|
||||
bindListener(renderer.domElement, "click", handleClick);
|
||||
if (renderer?.domElement) {
|
||||
renderer.domElement.style.touchAction = "none";
|
||||
}
|
||||
}
|
||||
|
||||
function updateHudScale() {
|
||||
@@ -2078,6 +2102,9 @@ function onMouseMove(event) {
|
||||
if (Date.now() - dragStartTime > 500) {
|
||||
isLongDrag = true;
|
||||
}
|
||||
if (pointerDragDistance > DRAG_POINTER_THRESHOLD_PX) {
|
||||
isLongDrag = true;
|
||||
}
|
||||
|
||||
const deltaX = event.clientX - previousMousePosition.x;
|
||||
const deltaY = event.clientY - previousMousePosition.y;
|
||||
@@ -2254,6 +2281,100 @@ function onMouseUp() {
|
||||
document.getElementById("container")?.classList.remove("dragging");
|
||||
}
|
||||
|
||||
function onPointerDown(event) {
|
||||
if (isEventOnHud(event)) return;
|
||||
if (event.pointerType !== "touch" && event.button !== 0) return;
|
||||
|
||||
if (event.pointerType === "touch") {
|
||||
activeTouchPoints.set(event.pointerId, {
|
||||
clientX: event.clientX,
|
||||
clientY: event.clientY,
|
||||
});
|
||||
renderer?.domElement?.setPointerCapture?.(event.pointerId);
|
||||
|
||||
if (activeTouchPoints.size === 2) {
|
||||
const [firstPoint, secondPoint] = Array.from(activeTouchPoints.values());
|
||||
pinchGesture = {
|
||||
distance: getTouchDistance(firstPoint, secondPoint),
|
||||
startZoom: getZoomLevel(),
|
||||
};
|
||||
activeDragPointerId = null;
|
||||
onMouseUp();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
activeDragPointerId = event.pointerId;
|
||||
pointerDragDistance = 0;
|
||||
suppressNextClick = false;
|
||||
onMouseDown(event);
|
||||
}
|
||||
|
||||
function onPointerMove(event) {
|
||||
if (event.pointerType === "touch") {
|
||||
if (activeTouchPoints.has(event.pointerId)) {
|
||||
activeTouchPoints.set(event.pointerId, {
|
||||
clientX: event.clientX,
|
||||
clientY: event.clientY,
|
||||
});
|
||||
}
|
||||
|
||||
if (pinchGesture && activeTouchPoints.size >= 2) {
|
||||
const [firstPoint, secondPoint] = Array.from(activeTouchPoints.values());
|
||||
const nextDistance = getTouchDistance(firstPoint, secondPoint);
|
||||
if (pinchGesture.distance > 0) {
|
||||
const scale = nextDistance / pinchGesture.distance;
|
||||
setZoomLevel(pinchGesture.startZoom * scale, camera);
|
||||
suppressNextClick = true;
|
||||
hideTooltip();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeDragPointerId === event.pointerId && isDragging) {
|
||||
const deltaX = event.clientX - previousMousePosition.x;
|
||||
const deltaY = event.clientY - previousMousePosition.y;
|
||||
pointerDragDistance = Math.max(
|
||||
pointerDragDistance,
|
||||
Math.hypot(deltaX, deltaY),
|
||||
);
|
||||
onMouseMove(event);
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (activeDragPointerId === event.pointerId && isDragging) {
|
||||
const deltaX = event.clientX - previousMousePosition.x;
|
||||
const deltaY = event.clientY - previousMousePosition.y;
|
||||
pointerDragDistance = Math.max(
|
||||
pointerDragDistance,
|
||||
Math.hypot(deltaX, deltaY),
|
||||
);
|
||||
}
|
||||
onMouseMove(event);
|
||||
}
|
||||
|
||||
function onPointerUp(event) {
|
||||
if (event.pointerType === "touch") {
|
||||
activeTouchPoints.delete(event.pointerId);
|
||||
if (activeTouchPoints.size < 2) {
|
||||
pinchGesture = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (activeDragPointerId === event.pointerId) {
|
||||
if (pointerDragDistance > DRAG_POINTER_THRESHOLD_PX) {
|
||||
suppressNextClick = true;
|
||||
isLongDrag = true;
|
||||
}
|
||||
activeDragPointerId = null;
|
||||
pointerDragDistance = 0;
|
||||
onMouseUp();
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseLeave() {
|
||||
hideTooltip();
|
||||
}
|
||||
@@ -2262,6 +2383,10 @@ function onClick(event) {
|
||||
const earth = getEarth();
|
||||
if (!earth) return;
|
||||
if (isEventOnHud(event)) return;
|
||||
if (suppressNextClick) {
|
||||
suppressNextClick = false;
|
||||
return;
|
||||
}
|
||||
|
||||
updatePointerFromEvent(event);
|
||||
|
||||
|
||||
@@ -18,17 +18,18 @@ let lastFocus = null;
|
||||
let lastFetchAt = 0;
|
||||
let lastRegionSwitchAt = 0;
|
||||
function getElements() {
|
||||
const isMobile = document.body.classList.contains("layout-mode-mobile");
|
||||
return {
|
||||
refreshBtn: document.getElementById("news-refresh"),
|
||||
openBtn: document.getElementById("news-open-external"),
|
||||
status: document.getElementById("news-board-status"),
|
||||
focusLabel: document.getElementById("news-focus-label"),
|
||||
focusCoords: document.getElementById("news-focus-coords"),
|
||||
sourceCount: document.getElementById("news-source-count"),
|
||||
refreshBtn: document.getElementById(isMobile ? "mobile-news-refresh" : "news-refresh"),
|
||||
openBtn: document.getElementById(isMobile ? "mobile-news-open-external" : "news-open-external"),
|
||||
status: document.getElementById(isMobile ? "mobile-news-board-status" : "news-board-status"),
|
||||
focusLabel: document.getElementById(isMobile ? "mobile-news-focus-label" : "news-focus-label"),
|
||||
focusCoords: document.getElementById(isMobile ? "mobile-news-focus-coords" : "news-focus-coords"),
|
||||
sourceCount: document.getElementById(isMobile ? "mobile-news-source-count" : "news-source-count"),
|
||||
regionChip: document.getElementById("news-region-chip"),
|
||||
board: document.getElementById("news-board-list"),
|
||||
empty: document.getElementById("news-board-empty"),
|
||||
feedAnchor: document.getElementById("news-feed-anchor"),
|
||||
board: document.getElementById(isMobile ? "mobile-news-board-list" : "news-board-list"),
|
||||
empty: document.getElementById(isMobile ? "mobile-news-board-empty" : "news-board-empty"),
|
||||
feedAnchor: document.getElementById(isMobile ? "mobile-news-feed-anchor" : "news-feed-anchor"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -81,18 +82,33 @@ function renderPayload(nextPayload) {
|
||||
openBtn,
|
||||
feedAnchor,
|
||||
} = getElements();
|
||||
|
||||
if (!board || !status || !focusLabel || !focusCoords || !sourceCount || !regionChip) {
|
||||
return;
|
||||
}
|
||||
|
||||
const items = Array.isArray(nextPayload?.items) ? nextPayload.items : [];
|
||||
const sources = Array.isArray(nextPayload?.sources) ? nextPayload.sources : [];
|
||||
const focus = nextPayload?.focus || {};
|
||||
|
||||
if (!board || !status || !focusLabel || !focusCoords || !sourceCount || !regionChip) {
|
||||
if (document.body.classList.contains("layout-mode-mobile")) {
|
||||
// Mobile page omits the region chip shell, but the rest of the page is still renderable.
|
||||
if (!board || !status || !focusLabel || !focusCoords || !sourceCount) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (regionChip) {
|
||||
regionChip.textContent = focus.region || "global";
|
||||
regionChip.style.setProperty("--news-accent", focus.accent || "#d6e6ff");
|
||||
}
|
||||
|
||||
if (document.body.classList.contains("layout-mode-mobile")) {
|
||||
// Mobile page does not show the compact chip row.
|
||||
} else if (!regionChip) {
|
||||
return;
|
||||
}
|
||||
|
||||
focusLabel.textContent = focus.label || "全球焦点";
|
||||
regionChip.textContent = focus.region || "global";
|
||||
regionChip.style.setProperty("--news-accent", focus.accent || "#d6e6ff");
|
||||
|
||||
if (typeof focus.lat === "number" && typeof focus.lon === "number") {
|
||||
focusCoords.textContent = `${formatCoord(focus.lat, "N", "S")} · ${formatCoord(focus.lon, "E", "W")}`;
|
||||
@@ -275,8 +291,6 @@ export function initNewsPanel() {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
|
||||
const { refreshBtn, openBtn } = getElements();
|
||||
|
||||
updateNewsToggleUI(isTVPanelVisible());
|
||||
renderEmptyState("正在准备全球态势新闻聚合源...");
|
||||
|
||||
@@ -287,16 +301,22 @@ export function initNewsPanel() {
|
||||
updateNewsToggleUI(Boolean(event.detail?.visible));
|
||||
});
|
||||
|
||||
refreshBtn?.addEventListener("click", async () => {
|
||||
try {
|
||||
await refreshNews(lastFocus?.lat, lastFocus?.lon);
|
||||
showStatusMessage("态势新闻已刷新", "info");
|
||||
} catch {
|
||||
showStatusMessage("态势新闻刷新失败", "error");
|
||||
}
|
||||
["news-refresh", "mobile-news-refresh"].forEach((id) => {
|
||||
const refreshBtn = document.getElementById(id);
|
||||
refreshBtn?.addEventListener("click", async () => {
|
||||
try {
|
||||
await refreshNews(lastFocus?.lat, lastFocus?.lon);
|
||||
showStatusMessage("态势新闻已刷新", "info");
|
||||
} catch {
|
||||
showStatusMessage("态势新闻刷新失败", "error");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
openBtn?.addEventListener("click", openCurrentSourceHomepage);
|
||||
["news-open-external", "mobile-news-open-external"].forEach((id) => {
|
||||
const openBtn = document.getElementById(id);
|
||||
openBtn?.addEventListener("click", openCurrentSourceHomepage);
|
||||
});
|
||||
|
||||
refreshNews(undefined, undefined, { silent: true }).catch(() => {});
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ let onSelectResultFn = null;
|
||||
let currentResults = [];
|
||||
let activeIndex = -1;
|
||||
let searchTimerId = null;
|
||||
let isOpen = false;
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value)
|
||||
@@ -15,14 +16,25 @@ function escapeHtml(value) {
|
||||
}
|
||||
|
||||
function getElements() {
|
||||
const isMobile = document.body.classList.contains("layout-mode-mobile");
|
||||
return {
|
||||
modal: document.getElementById("search-modal"),
|
||||
backdrop: document.getElementById("search-backdrop"),
|
||||
input: document.getElementById("earth-search-input"),
|
||||
clear: document.getElementById("earth-search-clear"),
|
||||
meta: document.getElementById("earth-search-meta"),
|
||||
results: document.getElementById("earth-search-results"),
|
||||
empty: document.getElementById("earth-search-empty"),
|
||||
input: document.getElementById(
|
||||
isMobile ? "mobile-earth-search-input" : "earth-search-input",
|
||||
),
|
||||
clear: document.getElementById(
|
||||
isMobile ? "mobile-earth-search-clear" : "earth-search-clear",
|
||||
),
|
||||
meta: document.getElementById(
|
||||
isMobile ? "mobile-earth-search-meta" : "earth-search-meta",
|
||||
),
|
||||
results: document.getElementById(
|
||||
isMobile ? "mobile-earth-search-results" : "earth-search-results",
|
||||
),
|
||||
empty: document.getElementById(
|
||||
isMobile ? "mobile-earth-search-empty" : "earth-search-empty",
|
||||
),
|
||||
close: document.getElementById("search-close"),
|
||||
};
|
||||
}
|
||||
@@ -154,7 +166,8 @@ function scheduleSearch() {
|
||||
|
||||
function handleKeydown(event) {
|
||||
const { modal, input } = getElements();
|
||||
if (!modal?.classList.contains("is-open")) return;
|
||||
const isMobile = document.body.classList.contains("layout-mode-mobile");
|
||||
if (!isMobile && !modal?.classList.contains("is-open")) return;
|
||||
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
@@ -185,15 +198,28 @@ export function initSearchPanel({ resolveResults, onSelectResult } = {}) {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
|
||||
const { input, clear, close, backdrop } = getElements();
|
||||
input?.addEventListener("input", scheduleSearch);
|
||||
input?.addEventListener("keydown", handleKeydown);
|
||||
clear?.addEventListener("click", () => {
|
||||
if (!input) return;
|
||||
input.value = "";
|
||||
input.focus();
|
||||
runSearch().catch((error) => {
|
||||
console.warn("Clearing search failed:", error);
|
||||
const inputs = ["earth-search-input", "mobile-earth-search-input"]
|
||||
.map((id) => document.getElementById(id))
|
||||
.filter((node) => node instanceof HTMLInputElement);
|
||||
const clears = ["earth-search-clear", "mobile-earth-search-clear"]
|
||||
.map((id) => document.getElementById(id))
|
||||
.filter((node) => node instanceof HTMLButtonElement);
|
||||
const close = document.getElementById("search-close");
|
||||
const backdrop = document.getElementById("search-backdrop");
|
||||
|
||||
inputs.forEach((input) => {
|
||||
input.addEventListener("input", scheduleSearch);
|
||||
input.addEventListener("keydown", handleKeydown);
|
||||
});
|
||||
clears.forEach((clear) => {
|
||||
clear.addEventListener("click", () => {
|
||||
const { input } = getElements();
|
||||
if (!input) return;
|
||||
input.value = "";
|
||||
input.focus();
|
||||
runSearch().catch((error) => {
|
||||
console.warn("Clearing search failed:", error);
|
||||
});
|
||||
});
|
||||
});
|
||||
close?.addEventListener("click", () => {
|
||||
@@ -207,9 +233,15 @@ export function initSearchPanel({ resolveResults, onSelectResult } = {}) {
|
||||
|
||||
export function openSearchPanel() {
|
||||
const { modal, input } = getElements();
|
||||
if (!modal) return;
|
||||
modal.classList.add("is-open");
|
||||
modal.setAttribute("aria-hidden", "false");
|
||||
if (!modal && !document.body.classList.contains("layout-mode-mobile")) return;
|
||||
if (isOpen) return;
|
||||
isOpen = true;
|
||||
document.body.classList.add("earth-search-open");
|
||||
modal?.classList.add("is-open");
|
||||
modal?.setAttribute("aria-hidden", "false");
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("earth:search-open-change", { detail: { open: true } }),
|
||||
);
|
||||
window.setTimeout(() => {
|
||||
input?.focus();
|
||||
input?.select();
|
||||
@@ -219,9 +251,32 @@ export function openSearchPanel() {
|
||||
}, 16);
|
||||
}
|
||||
|
||||
export function focusSearchInput({ select = false } = {}) {
|
||||
const { input } = getElements();
|
||||
if (!(input instanceof HTMLInputElement)) return;
|
||||
input.focus();
|
||||
if (select) {
|
||||
input.select();
|
||||
}
|
||||
}
|
||||
|
||||
export function refreshSearchResults() {
|
||||
return runSearch();
|
||||
}
|
||||
|
||||
export function closeSearchPanel() {
|
||||
const { modal } = getElements();
|
||||
if (!modal) return;
|
||||
modal.classList.remove("is-open");
|
||||
modal.setAttribute("aria-hidden", "true");
|
||||
if (!modal && !document.body.classList.contains("layout-mode-mobile")) return;
|
||||
if (!isOpen) return;
|
||||
isOpen = false;
|
||||
document.body.classList.remove("earth-search-open");
|
||||
modal?.classList.remove("is-open");
|
||||
modal?.setAttribute("aria-hidden", "true");
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("earth:search-open-change", { detail: { open: false } }),
|
||||
);
|
||||
}
|
||||
|
||||
export function isSearchPanelOpen() {
|
||||
return isOpen;
|
||||
}
|
||||
|
||||
@@ -56,21 +56,22 @@ const HLS_RETRY_CONFIG = {
|
||||
};
|
||||
|
||||
function getElements() {
|
||||
const isMobile = document.body.classList.contains("layout-mode-mobile");
|
||||
return {
|
||||
// Outer media shell node.
|
||||
panel: document.getElementById("media-panel"),
|
||||
toggleBtn: document.getElementById("toggle-tv"),
|
||||
select: document.getElementById("tv-source-select"),
|
||||
title: document.getElementById("tv-source-title"),
|
||||
meta: document.getElementById("tv-source-meta"),
|
||||
catalog: document.getElementById("tv-source-catalog"),
|
||||
status: document.getElementById("tv-source-status"),
|
||||
notes: document.getElementById("tv-source-notes"),
|
||||
iframe: document.getElementById("tv-iframe"),
|
||||
video: document.getElementById("tv-video"),
|
||||
empty: document.getElementById("tv-empty-state"),
|
||||
refreshBtn: document.getElementById("tv-refresh"),
|
||||
openBtn: document.getElementById("tv-open-external"),
|
||||
select: document.getElementById(isMobile ? "mobile-tv-source-select" : "tv-source-select"),
|
||||
title: document.getElementById(isMobile ? "mobile-tv-source-title" : "tv-source-title"),
|
||||
meta: document.getElementById(isMobile ? "mobile-tv-source-meta" : "tv-source-meta"),
|
||||
catalog: document.getElementById(isMobile ? "mobile-tv-source-catalog" : "tv-source-catalog"),
|
||||
status: document.getElementById(isMobile ? "mobile-tv-source-status" : "tv-source-status"),
|
||||
notes: document.getElementById(isMobile ? "mobile-tv-source-notes" : "tv-source-notes"),
|
||||
iframe: document.getElementById(isMobile ? "mobile-tv-iframe" : "tv-iframe"),
|
||||
video: document.getElementById(isMobile ? "mobile-tv-video" : "tv-video"),
|
||||
empty: document.getElementById(isMobile ? "mobile-tv-empty-state" : "tv-empty-state"),
|
||||
refreshBtn: document.getElementById(isMobile ? "mobile-tv-refresh" : "tv-refresh"),
|
||||
openBtn: document.getElementById(isMobile ? "mobile-tv-open-external" : "tv-open-external"),
|
||||
metaWrap: document.getElementById("tv-meta-wrap"),
|
||||
metaToggle: document.getElementById("tv-meta-toggle"),
|
||||
liveHeaderControls: document.getElementById("tv-header-controls-live"),
|
||||
@@ -351,6 +352,7 @@ function setPanelVisible(visible) {
|
||||
const { panel } = getElements();
|
||||
if (!panel) return;
|
||||
mediaPanel?.setVisible(visible);
|
||||
document.body.classList.toggle("earth-media-open", visible);
|
||||
updateToggleButton(visible);
|
||||
syncSettingsToggle(visible);
|
||||
window.dispatchEvent(new CustomEvent("earth:tv-visibility-change", {
|
||||
@@ -1071,12 +1073,16 @@ export function initTVPanel() {
|
||||
showStatusMessage("已切换到态势新闻", "info");
|
||||
});
|
||||
|
||||
select?.addEventListener("change", (event) => {
|
||||
const target = event.currentTarget;
|
||||
if (!(target instanceof HTMLSelectElement)) return;
|
||||
currentSourceId = target.value;
|
||||
renderSource(findSourceById(currentSourceId));
|
||||
});
|
||||
[select, document.getElementById("mobile-tv-source-select"), document.getElementById("tv-source-select")]
|
||||
.filter((element, index, array) => element && array.indexOf(element) === index)
|
||||
.forEach((selectEl) => {
|
||||
selectEl?.addEventListener("change", (event) => {
|
||||
const target = event.currentTarget;
|
||||
if (!(target instanceof HTMLSelectElement)) return;
|
||||
currentSourceId = target.value;
|
||||
renderSource(findSourceById(currentSourceId));
|
||||
});
|
||||
});
|
||||
|
||||
metaToggle?.addEventListener("click", () => {
|
||||
clearTimeout(metaAutoCollapseTimer);
|
||||
@@ -1084,9 +1090,13 @@ export function initTVPanel() {
|
||||
setMetaCollapsed(isNowCollapsed);
|
||||
});
|
||||
|
||||
refreshBtn?.addEventListener("click", () => {
|
||||
refreshTVPanel();
|
||||
});
|
||||
[refreshBtn, document.getElementById("mobile-tv-refresh"), document.getElementById("tv-refresh")]
|
||||
.filter((element, index, array) => element && array.indexOf(element) === index)
|
||||
.forEach((refreshEl) => {
|
||||
refreshEl?.addEventListener("click", () => {
|
||||
refreshTVPanel();
|
||||
});
|
||||
});
|
||||
|
||||
liveTabBtn?.addEventListener("click", () => {
|
||||
setActiveTab("live");
|
||||
@@ -1095,24 +1105,32 @@ export function initTVPanel() {
|
||||
setActiveTab("news");
|
||||
});
|
||||
|
||||
iframe?.addEventListener("load", () => {
|
||||
if (iframe.hidden) return;
|
||||
clearSourceFailed(currentSourceId);
|
||||
setPanelMessage(TV_STATUS_MESSAGE.iframeReady);
|
||||
});
|
||||
[iframe, document.getElementById("mobile-tv-iframe"), document.getElementById("tv-iframe")]
|
||||
.filter((element, index, array) => element && array.indexOf(element) === index)
|
||||
.forEach((iframeEl) => {
|
||||
iframeEl?.addEventListener("load", () => {
|
||||
if (iframeEl.hidden) return;
|
||||
clearSourceFailed(currentSourceId);
|
||||
setPanelMessage(TV_STATUS_MESSAGE.iframeReady);
|
||||
});
|
||||
});
|
||||
|
||||
video?.addEventListener("loadedmetadata", () => {
|
||||
if (video.hidden) return;
|
||||
clearSourceFailed(currentSourceId);
|
||||
setPanelMessage(TV_STATUS_MESSAGE.videoReady);
|
||||
});
|
||||
[video, document.getElementById("mobile-tv-video"), document.getElementById("tv-video")]
|
||||
.filter((element, index, array) => element && array.indexOf(element) === index)
|
||||
.forEach((videoEl) => {
|
||||
videoEl?.addEventListener("loadedmetadata", () => {
|
||||
if (videoEl.hidden) return;
|
||||
clearSourceFailed(currentSourceId);
|
||||
setPanelMessage(TV_STATUS_MESSAGE.videoReady);
|
||||
});
|
||||
|
||||
video?.addEventListener("error", () => {
|
||||
const currentSource = getCurrentSource();
|
||||
if (!showEmbeddedFallback(currentSource) && !tryFallbackSource()) {
|
||||
setPanelMessage(TV_STATUS_MESSAGE.videoError);
|
||||
}
|
||||
});
|
||||
videoEl?.addEventListener("error", () => {
|
||||
const currentSource = getCurrentSource();
|
||||
if (!showEmbeddedFallback(currentSource) && !tryFallbackSource()) {
|
||||
setPanelMessage(TV_STATUS_MESSAGE.videoError);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
setupResizeHandle();
|
||||
syncPanelActiveTab("live");
|
||||
|
||||
@@ -19,6 +19,15 @@ function getElement(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
function setTextTargets(ids, value) {
|
||||
ids.forEach((id) => {
|
||||
const element = getElement(id);
|
||||
if (element) {
|
||||
element.textContent = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setElementDisplay(element, visible, displayValue = "block") {
|
||||
if (!element) return;
|
||||
element.style.display = visible ? displayValue : "none";
|
||||
@@ -171,27 +180,13 @@ export function updateZoomDisplay(zoomLevel, distance) {
|
||||
|
||||
// Update earth stats
|
||||
export function updateEarthStats(stats) {
|
||||
const cableCountEl = getElement("cable-count");
|
||||
const landingPointCountEl = getElement("landing-point-count");
|
||||
const bgpAnomalyCountEl = getElement("bgp-anomaly-count");
|
||||
const bgpCollectorCountEl = getElement("bgp-collector-count");
|
||||
const bgpStatusSummaryEl = getElement("bgp-status-summary");
|
||||
const terrainStatusEl = getElement("terrain-status");
|
||||
const textureQualityEl = getElement("texture-quality");
|
||||
|
||||
if (cableCountEl) cableCountEl.textContent = stats.cableCount || 0;
|
||||
if (landingPointCountEl)
|
||||
landingPointCountEl.textContent = stats.landingPointCount || 0;
|
||||
if (bgpAnomalyCountEl)
|
||||
bgpAnomalyCountEl.textContent = stats.bgpAnomalyCount || 0;
|
||||
if (bgpCollectorCountEl)
|
||||
bgpCollectorCountEl.textContent = stats.bgpCollectorCount || 0;
|
||||
if (bgpStatusSummaryEl)
|
||||
bgpStatusSummaryEl.textContent = stats.bgpStatusSummary || "-";
|
||||
if (terrainStatusEl)
|
||||
terrainStatusEl.textContent = stats.terrainOn ? "开启" : "关闭";
|
||||
if (textureQualityEl)
|
||||
textureQualityEl.textContent = stats.textureQuality || "8K 卫星图";
|
||||
setTextTargets(["cable-count", "mobile-cable-count"], String(stats.cableCount || 0));
|
||||
setTextTargets(["landing-point-count", "mobile-landing-point-count"], String(stats.landingPointCount || 0));
|
||||
setTextTargets(["bgp-anomaly-count", "mobile-bgp-anomaly-count"], String(stats.bgpAnomalyCount || 0));
|
||||
setTextTargets(["bgp-collector-count"], String(stats.bgpCollectorCount || 0));
|
||||
setTextTargets(["bgp-status-summary", "mobile-bgp-status-summary"], stats.bgpStatusSummary || "-");
|
||||
setTextTargets(["terrain-status"], stats.terrainOn ? "开启" : "关闭");
|
||||
setTextTargets(["texture-quality"], stats.textureQuality || "8K 卫星图");
|
||||
}
|
||||
|
||||
// Show/hide loading via status message
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "planet"
|
||||
version = "0.34.0"
|
||||
version = "0.35.0"
|
||||
description = "智能星球计划 - 态势感知系统"
|
||||
requires-python = ">=3.14"
|
||||
dependencies = [
|
||||
|
||||
Reference in New Issue
Block a user